diff --git a/src/atrule/kind.rs b/src/atrule/kind.rs index f4734ea..9aebbaa 100644 --- a/src/atrule/kind.rs +++ b/src/atrule/kind.rs @@ -4,7 +4,7 @@ use codemap::Spanned; use crate::{common::unvendor, error::SassError}; -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum AtRuleKind { // Sass specific @rules /// Loads mixins, functions, and variables from other Sass diff --git a/src/parse/media.rs b/src/parse/media.rs index 63ec8e2..2830fa5 100644 --- a/src/parse/media.rs +++ b/src/parse/media.rs @@ -1,6 +1,6 @@ use crate::{ error::SassResult, - utils::{is_name_start, peek_ident_no_interpolation}, + utils::is_name_start, {Cow, Token}, }; @@ -10,23 +10,25 @@ impl<'a> Parser<'a> { /// Peeks to see if the `ident` is at the current position. If it is, /// consume the identifier pub fn scan_identifier(&mut self, ident: &'static str, case_insensitive: bool) -> bool { - let mut peeked_identifier = - match peek_ident_no_interpolation(self.toks, false, self.span_before) { - Ok(v) => v.node, - Err(..) => return false, - }; + let start = self.toks.cursor(); + + let mut peeked_identifier = match self.parse_identifier_no_interpolation(false) { + Ok(v) => v.node, + Err(..) => { + self.toks.set_cursor(start); + return false; + } + }; if case_insensitive { peeked_identifier.make_ascii_lowercase(); } if peeked_identifier == ident { - self.toks.truncate_iterator_to_cursor(); - self.toks.next(); return true; } - self.toks.reset_cursor(); + self.toks.set_cursor(start); false } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 617e7ad..475c4b4 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -136,12 +136,14 @@ impl<'a> Parser<'a> { pub fn expect_identifier(&mut self, ident: &'static str) -> SassResult<()> { let this_ident = self.parse_identifier_no_interpolation(false)?; + self.span_before = this_ident.span; + if this_ident.node == ident { return Ok(()); } - Err((format!("Expected \"{}\".", ident), self.span_before).into()) + Err((format!("Expected \"{}\".", ident), this_ident.span).into()) } fn parse_stmt(&mut self) -> SassResult> { diff --git a/src/parse/module.rs b/src/parse/module.rs index 638ace6..5ff9844 100644 --- a/src/parse/module.rs +++ b/src/parse/module.rs @@ -13,79 +13,89 @@ use crate::{ lexer::Lexer, parse::{common::Comment, Parser, Stmt, VariableValue}, scope::Scope, - utils::peek_ident_no_interpolation, Token, }; impl<'a> Parser<'a> { fn parse_module_alias(&mut self) -> SassResult> { - if let Some(Token { kind: 'a', .. }) | Some(Token { kind: 'A', .. }) = self.toks.peek() { - let mut ident = peek_ident_no_interpolation(self.toks, false, self.span_before)?; - ident.node.make_ascii_lowercase(); - if ident.node != "as" { - return Err(("expected \";\".", ident.span).into()); - } - - self.whitespace_or_comment(); - - if let Some(Token { kind: '*', .. }) = self.toks.peek() { - self.toks.next(); - return Ok(Some('*'.to_string())); - } - - let name = self.parse_identifier_no_interpolation(false)?; - - return Ok(Some(name.node)); + if !matches!( + self.toks.peek(), + Some(Token { kind: 'a', .. }) | Some(Token { kind: 'A', .. }) + ) { + return Ok(None); } - Ok(None) + let mut ident = self.parse_identifier_no_interpolation(false)?; + + ident.node.make_ascii_lowercase(); + + if ident.node != "as" { + return Err(("expected \";\".", ident.span).into()); + } + + self.whitespace_or_comment(); + + if let Some(Token { kind: '*', .. }) = self.toks.peek() { + self.toks.next(); + return Ok(Some('*'.to_string())); + } + + let name = self.parse_identifier_no_interpolation(false)?; + + Ok(Some(name.node)) } fn parse_module_config(&mut self) -> SassResult { let mut config = ModuleConfig::default(); - if let Some(Token { kind: 'w', .. }) | Some(Token { kind: 'W', .. }) = self.toks.peek() { - let mut ident = peek_ident_no_interpolation(self.toks, false, self.span_before)?; - ident.node.make_ascii_lowercase(); - if ident.node != "with" { - return Err(("expected \";\".", ident.span).into()); - } + if !matches!( + self.toks.peek(), + Some(Token { kind: 'w', .. }) | Some(Token { kind: 'W', .. }) + ) { + return Ok(config); + } + + let mut ident = self.parse_identifier_no_interpolation(false)?; + + ident.node.make_ascii_lowercase(); + if ident.node != "with" { + return Err(("expected \";\".", ident.span).into()); + } + + self.whitespace_or_comment(); + + self.span_before = ident.span; + + self.expect_char('(')?; + + loop { + self.whitespace_or_comment(); + self.expect_char('$')?; + + let name = self.parse_identifier_no_interpolation(false)?; + self.whitespace_or_comment(); + self.expect_char(':')?; self.whitespace_or_comment(); - self.span_before = ident.span; + let value = self.parse_value(false, &|toks| { + matches!( + toks.peek(), + Some(Token { kind: ',', .. }) | Some(Token { kind: ')', .. }) + ) + })?; - self.expect_char('(')?; + config.insert(name.map_node(|n| n.into()), value)?; - loop { - self.whitespace_or_comment(); - self.expect_char('$')?; - - let name = self.parse_identifier_no_interpolation(false)?; - - self.whitespace_or_comment(); - self.expect_char(':')?; - self.whitespace_or_comment(); - - let value = self.parse_value(false, &|toks| { - matches!( - toks.peek(), - Some(Token { kind: ',', .. }) | Some(Token { kind: ')', .. }) - ) - })?; - - config.insert(name.map_node(|n| n.into()), value)?; - - match self.toks.next() { - Some(Token { kind: ',', .. }) => { - continue; - } - Some(Token { kind: ')', .. }) => { - break; - } - Some(..) | None => { - return Err(("expected \")\".", self.span_before).into()); - } + match self.toks.next() { + Some(Token { kind: ',', .. }) => { + continue; + } + Some(Token { kind: ')', .. }) => { + break; + } + Some(..) | None => { + return Err(("expected \")\".", self.span_before).into()); } } } @@ -157,27 +167,25 @@ impl<'a> Parser<'a> { loop { self.whitespace(); + match self.toks.peek() { Some(Token { kind: '@', .. }) => { - self.toks.advance_cursor(); + let start = self.toks.cursor(); + + self.toks.next(); if let Some(Token { kind, .. }) = self.toks.peek() { - if !matches!(kind, 'a'..='z' | 'A'..='Z' | '\\') { + if !matches!(kind, 'u' | 'U' | '\\') { + self.toks.set_cursor(start); break; } } - match AtRuleKind::try_from(&peek_ident_no_interpolation( - self.toks, - false, - self.span_before, - )?)? { - AtRuleKind::Use => { - self.toks.truncate_iterator_to_cursor(); - } - _ => { - break; - } + let ident = self.parse_identifier_no_interpolation(false)?; + + if AtRuleKind::try_from(&ident)? != AtRuleKind::Use { + self.toks.set_cursor(start); + break; } self.whitespace_or_comment(); diff --git a/tests/if.rs b/tests/if.rs index 1d0bd07..0d9f3a2 100644 --- a/tests/if.rs +++ b/tests/if.rs @@ -231,3 +231,8 @@ error!( invalid_toplevel_selector, "@if true { & { } }", "Error: Top-level selectors may not contain the parent selector \"&\"." ); +error!( + #[ignore = "unsure what the exact rule is here wrt denying interpolation (@media allows this)"] + denies_interpolated_at_rule, + "@#{if} true { a { color: red; } }", "Error: expected \"(\"." +); diff --git a/tests/media.rs b/tests/media.rs index 8e3a939..a05573a 100644 --- a/tests/media.rs +++ b/tests/media.rs @@ -161,6 +161,15 @@ test!( }", "@media foo {\n a {\n color: red;\n }\n}\n@media bar {\n a {\n color: green;\n }\n}\n" ); +test!( + allows_interpolated_at_rule, + "@#{media} (true) { + a { + color: red; + } + }", + "@media (true) {\n a {\n color: red;\n }\n}\n" +); error!( media_feature_missing_closing_paren,