simplify lookahead for @use and @media

This commit is contained in:
Connor Skees 2021-07-19 20:50:12 -04:00
parent c2e84e3854
commit a34aa32128
6 changed files with 106 additions and 80 deletions

View File

@ -4,7 +4,7 @@ use codemap::Spanned;
use crate::{common::unvendor, error::SassError}; use crate::{common::unvendor, error::SassError};
#[derive(Debug)] #[derive(Debug, PartialEq, Eq)]
pub enum AtRuleKind { pub enum AtRuleKind {
// Sass specific @rules // Sass specific @rules
/// Loads mixins, functions, and variables from other Sass /// Loads mixins, functions, and variables from other Sass

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
error::SassResult, error::SassResult,
utils::{is_name_start, peek_ident_no_interpolation}, utils::is_name_start,
{Cow, Token}, {Cow, Token},
}; };
@ -10,23 +10,25 @@ impl<'a> Parser<'a> {
/// Peeks to see if the `ident` is at the current position. If it is, /// Peeks to see if the `ident` is at the current position. If it is,
/// consume the identifier /// consume the identifier
pub fn scan_identifier(&mut self, ident: &'static str, case_insensitive: bool) -> bool { pub fn scan_identifier(&mut self, ident: &'static str, case_insensitive: bool) -> bool {
let mut peeked_identifier = let start = self.toks.cursor();
match peek_ident_no_interpolation(self.toks, false, self.span_before) {
Ok(v) => v.node, let mut peeked_identifier = match self.parse_identifier_no_interpolation(false) {
Err(..) => return false, Ok(v) => v.node,
}; Err(..) => {
self.toks.set_cursor(start);
return false;
}
};
if case_insensitive { if case_insensitive {
peeked_identifier.make_ascii_lowercase(); peeked_identifier.make_ascii_lowercase();
} }
if peeked_identifier == ident { if peeked_identifier == ident {
self.toks.truncate_iterator_to_cursor();
self.toks.next();
return true; return true;
} }
self.toks.reset_cursor(); self.toks.set_cursor(start);
false false
} }

View File

@ -136,12 +136,14 @@ impl<'a> Parser<'a> {
pub fn expect_identifier(&mut self, ident: &'static str) -> SassResult<()> { pub fn expect_identifier(&mut self, ident: &'static str) -> SassResult<()> {
let this_ident = self.parse_identifier_no_interpolation(false)?; let this_ident = self.parse_identifier_no_interpolation(false)?;
self.span_before = this_ident.span; self.span_before = this_ident.span;
if this_ident.node == ident { if this_ident.node == ident {
return Ok(()); return Ok(());
} }
Err((format!("Expected \"{}\".", ident), self.span_before).into()) Err((format!("Expected \"{}\".", ident), this_ident.span).into())
} }
fn parse_stmt(&mut self) -> SassResult<Vec<Stmt>> { fn parse_stmt(&mut self) -> SassResult<Vec<Stmt>> {

View File

@ -13,79 +13,89 @@ use crate::{
lexer::Lexer, lexer::Lexer,
parse::{common::Comment, Parser, Stmt, VariableValue}, parse::{common::Comment, Parser, Stmt, VariableValue},
scope::Scope, scope::Scope,
utils::peek_ident_no_interpolation,
Token, Token,
}; };
impl<'a> Parser<'a> { impl<'a> Parser<'a> {
fn parse_module_alias(&mut self) -> SassResult<Option<String>> { fn parse_module_alias(&mut self) -> SassResult<Option<String>> {
if let Some(Token { kind: 'a', .. }) | Some(Token { kind: 'A', .. }) = self.toks.peek() { if !matches!(
let mut ident = peek_ident_no_interpolation(self.toks, false, self.span_before)?; self.toks.peek(),
ident.node.make_ascii_lowercase(); Some(Token { kind: 'a', .. }) | Some(Token { kind: 'A', .. })
if ident.node != "as" { ) {
return Err(("expected \";\".", ident.span).into()); return Ok(None);
}
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));
} }
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<ModuleConfig> { fn parse_module_config(&mut self) -> SassResult<ModuleConfig> {
let mut config = ModuleConfig::default(); let mut config = ModuleConfig::default();
if let Some(Token { kind: 'w', .. }) | Some(Token { kind: 'W', .. }) = self.toks.peek() { if !matches!(
let mut ident = peek_ident_no_interpolation(self.toks, false, self.span_before)?; self.toks.peek(),
ident.node.make_ascii_lowercase(); Some(Token { kind: 'w', .. }) | Some(Token { kind: 'W', .. })
if ident.node != "with" { ) {
return Err(("expected \";\".", ident.span).into()); 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.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 { match self.toks.next() {
self.whitespace_or_comment(); Some(Token { kind: ',', .. }) => {
self.expect_char('$')?; continue;
}
let name = self.parse_identifier_no_interpolation(false)?; Some(Token { kind: ')', .. }) => {
break;
self.whitespace_or_comment(); }
self.expect_char(':')?; Some(..) | None => {
self.whitespace_or_comment(); return Err(("expected \")\".", self.span_before).into());
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());
}
} }
} }
} }
@ -157,27 +167,25 @@ impl<'a> Parser<'a> {
loop { loop {
self.whitespace(); self.whitespace();
match self.toks.peek() { match self.toks.peek() {
Some(Token { kind: '@', .. }) => { Some(Token { kind: '@', .. }) => {
self.toks.advance_cursor(); let start = self.toks.cursor();
self.toks.next();
if let Some(Token { kind, .. }) = self.toks.peek() { 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; break;
} }
} }
match AtRuleKind::try_from(&peek_ident_no_interpolation( let ident = self.parse_identifier_no_interpolation(false)?;
self.toks,
false, if AtRuleKind::try_from(&ident)? != AtRuleKind::Use {
self.span_before, self.toks.set_cursor(start);
)?)? { break;
AtRuleKind::Use => {
self.toks.truncate_iterator_to_cursor();
}
_ => {
break;
}
} }
self.whitespace_or_comment(); self.whitespace_or_comment();

View File

@ -231,3 +231,8 @@ error!(
invalid_toplevel_selector, invalid_toplevel_selector,
"@if true { & { } }", "Error: Top-level selectors may not contain the parent 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 \"(\"."
);

View File

@ -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" "@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!( error!(
media_feature_missing_closing_paren, media_feature_missing_closing_paren,