diff --git a/crates/css-var-kit/src/parser/css.rs b/crates/css-var-kit/src/parser/css.rs index b44d992..479470c 100644 --- a/crates/css-var-kit/src/parser/css.rs +++ b/crates/css-var-kit/src/parser/css.rs @@ -9,6 +9,7 @@ struct Scanner<'a> { pos: usize, line: u32, col: u32, + scss: bool, } struct AtPropertyParts { @@ -23,12 +24,18 @@ struct AtPropertyParts { } impl<'a> Scanner<'a> { - fn new_with_offset(css: &'a OwnedStr, line_offset: u32, column_offset: u32) -> Self { + fn new_with_offset( + css: &'a OwnedStr, + line_offset: u32, + column_offset: u32, + scss: bool, + ) -> Self { Self { bytes: css.as_bytes(), pos: 0, line: line_offset, col: column_offset, + scss, } } @@ -118,6 +125,22 @@ impl<'a> Scanner<'a> { } } + fn skip_line_comment(&mut self) { + self.advance(2); // skip // + while !self.is_eof() && !matches!(self.bytes[self.pos], b'\n' | b'\r') { + self.advance(1); + } + } + + fn scan_line_comment(&mut self, css: &'a OwnedStr) -> OwnedStr { + self.advance(2); // skip // + let content_start = self.pos; + while !self.is_eof() && !matches!(self.bytes[self.pos], b'\n' | b'\r') { + self.advance(1); + } + css.map(|s| s[content_start..self.pos].trim()) + } + fn scan_comment(&mut self, css: &'a OwnedStr) -> OwnedStr { self.advance(2); // skip /* let content_start = self.pos; @@ -143,10 +166,25 @@ impl<'a> Scanner<'a> { } fn skip_at_rule_prelude(&mut self) { + let mut paren_depth = 0i32; while !self.is_eof() { match self.bytes[self.pos] { b'"' | b'\'' => self.skip_string_literal(), b'/' if self.peek_at(1) == Some(b'*') => self.skip_comment(), + // SCSS only: `//` outside parens is a line comment. Inside + // parens it may be a protocol-relative `url(//cdn/…)`, so we + // leave the bytes as-is. + b'/' if self.scss && self.peek_at(1) == Some(b'/') && paren_depth <= 0 => { + self.skip_line_comment(); + } + b'(' => { + paren_depth += 1; + self.advance(1); + } + b')' => { + paren_depth -= 1; + self.advance(1); + } b';' => { self.advance(1); return; @@ -210,6 +248,9 @@ impl<'a> Scanner<'a> { match self.bytes[self.pos] { b'"' | b'\'' => self.skip_string_literal(), b'/' if self.peek_at(1) == Some(b'*') => self.skip_comment(), + b'/' if self.scss && self.peek_at(1) == Some(b'/') => { + self.skip_line_comment(); + } b'{' => { depth += 1; self.advance(1); @@ -266,6 +307,7 @@ impl<'a> Scanner<'a> { fn scan_value_end(&mut self) -> usize { let mut paren_depth = 0i32; + let mut interp_depth = 0i32; while !self.is_eof() { match self.bytes[self.pos] { @@ -281,7 +323,17 @@ impl<'a> Scanner<'a> { paren_depth -= 1; self.advance(1); } - b';' if paren_depth <= 0 => { + b'#' if self.peek_at(1) == Some(b'{') => { + // SCSS interpolation — treat `#{ … }` as a balanced group + // so the inner `}` doesn't terminate the value. + interp_depth += 1; + self.advance(2); + } + b'}' if interp_depth > 0 => { + interp_depth -= 1; + self.advance(1); + } + b';' if paren_depth <= 0 && interp_depth <= 0 => { let end = self.pos; self.advance(1); // skip ';' return end; @@ -290,7 +342,7 @@ impl<'a> Scanner<'a> { // Don't consume '}', let the main loop handle brace_depth return self.pos; } - b'/' if self.peek_at(1) == Some(b'*') && paren_depth <= 0 => { + b'/' if self.peek_at(1) == Some(b'*') && paren_depth <= 0 && interp_depth <= 0 => { // Comment starts — value ends here let end = self.pos; // Skip the comment @@ -314,7 +366,29 @@ impl<'a> Scanner<'a> { } return end; } - b'\n' | b'\r' if paren_depth <= 0 => { + b'/' if self.peek_at(1) == Some(b'*') && interp_depth > 0 => { + // Inside `#{ … }` a block comment must be skipped wholesale, + // otherwise a `}` inside it would be mistaken for the + // interpolation closer and pop interp_depth prematurely. + self.skip_comment(); + } + b'/' if self.scss + && self.peek_at(1) == Some(b'/') + && paren_depth <= 0 + && interp_depth <= 0 => + { + // SCSS line comment outside parens/interp — terminates value. + let end = self.pos; + self.skip_line_comment(); + return end; + } + b'/' if self.scss && self.peek_at(1) == Some(b'/') && interp_depth > 0 => { + // Inside `#{ … }` a `}` in the comment would otherwise pop + // interp_depth; skip to EOL instead. (Inside parens we keep + // bytes as-is so `url(//cdn/…)` survives.) + self.skip_line_comment(); + } + b'\n' | b'\r' if paren_depth <= 0 && interp_depth <= 0 => { // Check if the next non-whitespace looks like a new property let mut skip = 1; // For \r\n, skip both bytes @@ -347,7 +421,7 @@ impl<'a> Scanner<'a> { } pub fn parse(css: &OwnedStr, file_path: &Rc) -> ParseResult { - parse_impl(css, css, file_path, 0, 0, 0, 0) + parse_impl(css, css, file_path, 0, 0, 0, is_scss_path(file_path)) } /// Used when parsing `