diff --git a/.gitignore b/.gitignore index fd9190f..e5dd1f6 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,8 @@ flamegraph.svg susy bulma* bootstrap* +materialize +uikit +bourbon +foundation-sites +sassline diff --git a/src/atrule/keyframes.rs b/src/atrule/keyframes.rs index e48b56a..ea7e246 100644 --- a/src/atrule/keyframes.rs +++ b/src/atrule/keyframes.rs @@ -2,6 +2,10 @@ use crate::parse::Stmt; #[derive(Debug, Clone)] pub(crate) struct Keyframes { + /// `@keyframes` can contain a browser prefix, + /// e.g. `@-webkit-keyframes { ... }`, and therefore + /// we cannot be certain of the name of the at-rule + pub rule: String, pub name: String, pub body: Vec, } diff --git a/src/atrule/kind.rs b/src/atrule/kind.rs index 9938452..3e63f32 100644 --- a/src/atrule/kind.rs +++ b/src/atrule/kind.rs @@ -2,7 +2,7 @@ use std::convert::TryFrom; use codemap::Spanned; -use crate::error::SassError; +use crate::{common::unvendor, error::SassError}; #[derive(Debug)] pub enum AtRuleKind { @@ -72,31 +72,35 @@ pub enum AtRuleKind { impl TryFrom<&Spanned> for AtRuleKind { type Error = Box; fn try_from(c: &Spanned) -> Result> { - Ok(match c.node.as_str() { - "use" => Self::Use, - "forward" => Self::Forward, - "import" => Self::Import, - "mixin" => Self::Mixin, - "include" => Self::Include, - "function" => Self::Function, - "return" => Self::Return, - "extend" => Self::Extend, - "at-root" => Self::AtRoot, - "error" => Self::Error, - "warn" => Self::Warn, - "debug" => Self::Debug, - "if" => Self::If, - "each" => Self::Each, - "for" => Self::For, - "while" => Self::While, - "charset" => Self::Charset, - "supports" => Self::Supports, - "keyframes" => Self::Keyframes, - "content" => Self::Content, - "media" => Self::Media, + match c.node.as_str() { + "use" => return Ok(Self::Use), + "forward" => return Ok(Self::Forward), + "import" => return Ok(Self::Import), + "mixin" => return Ok(Self::Mixin), + "include" => return Ok(Self::Include), + "function" => return Ok(Self::Function), + "return" => return Ok(Self::Return), + "extend" => return Ok(Self::Extend), + "at-root" => return Ok(Self::AtRoot), + "error" => return Ok(Self::Error), + "warn" => return Ok(Self::Warn), + "debug" => return Ok(Self::Debug), + "if" => return Ok(Self::If), + "each" => return Ok(Self::Each), + "for" => return Ok(Self::For), + "while" => return Ok(Self::While), + "charset" => return Ok(Self::Charset), + "supports" => return Ok(Self::Supports), + "content" => return Ok(Self::Content), + "media" => return Ok(Self::Media), "else" => return Err(("This at-rule is not allowed here.", c.span).into()), "" => return Err(("Expected identifier.", c.span).into()), - s => Self::Unknown(s.to_owned()), + _ => {} + } + + Ok(match unvendor(&c.node) { + "keyframes" => Self::Keyframes, + _ => Self::Unknown(c.node.to_owned()), }) } } diff --git a/src/builtin/functions/color/opacity.rs b/src/builtin/functions/color/opacity.rs index b6f82eb..2314132 100644 --- a/src/builtin/functions/color/opacity.rs +++ b/src/builtin/functions/color/opacity.rs @@ -5,16 +5,84 @@ use crate::{ value::Value, }; +fn is_ms_filter(s: &str) -> bool { + let mut chars = s.chars(); + + if let Some(c) = chars.next() { + if !matches!(c, 'a'..='z' | 'A'..='Z') { + return false; + } + } else { + return false; + } + + for c in &mut chars { + match c { + ' ' | '\t' | '\n' => break, + 'a'..='z' | 'A'..='Z' => continue, + '=' => return true, + _ => return false, + } + } + + for c in chars { + match c { + ' ' | '\t' | '\n' => continue, + '=' => return true, + _ => return false, + } + } + + false +} + +#[cfg(test)] +mod test { + use super::is_ms_filter; + #[test] + fn test_is_ms_filter() { + assert!(is_ms_filter("a=a")); + assert!(is_ms_filter("a=")); + assert!(is_ms_filter("a \t\n =a")); + assert!(!is_ms_filter("a \t\n a=a")); + assert!(!is_ms_filter("aa")); + assert!(!is_ms_filter(" aa")); + assert!(!is_ms_filter("=a")); + assert!(!is_ms_filter("1=a")); + } +} + pub(crate) fn alpha(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { - args.max_args(1)?; - match args.get_err(0, "color")? { - Value::Color(c) => Ok(Value::Dimension(Some(c.alpha()), Unit::None, true)), - Value::Dimension(None, ..) => todo!(), - v => Err(( - format!("$color: {} is not a color.", v.inspect(args.span())?), - args.span(), - ) - .into()), + if args.len() <= 1 { + match args.get_err(0, "color")? { + Value::Color(c) => Ok(Value::Dimension(Some(c.alpha()), Unit::None, true)), + Value::String(s, QuoteKind::None) if is_ms_filter(&s) => { + Ok(Value::String(format!("alpha({})", s), QuoteKind::None)) + } + v => Err(( + format!("$color: {} is not a color.", v.inspect(args.span())?), + args.span(), + ) + .into()), + } + } else { + let err = args.max_args(1); + let args = args + .get_variadic()? + .into_iter() + .map(|arg| match arg.node { + Value::String(s, QuoteKind::None) if is_ms_filter(&s) => Ok(s), + _ => { + err.clone()?; + unreachable!() + } + }) + .collect::>>()?; + + Ok(Value::String( + format!("alpha({})", args.join(", "),), + QuoteKind::None, + )) } } diff --git a/src/output.rs b/src/output.rs index 0a14ada..e6a08c3 100644 --- a/src/output.rs +++ b/src/output.rs @@ -157,8 +157,12 @@ impl Css { })? } Stmt::Keyframes(k) => { - let Keyframes { name, body } = *k; - vals.push(Toplevel::Keyframes(Box::new(Keyframes { name, body }))) + let Keyframes { rule, name, body } = *k; + vals.push(Toplevel::Keyframes(Box::new(Keyframes { + rule, + name, + body, + }))) } k @ Stmt::KeyframesRuleSet(..) => { unreachable!("@keyframes ruleset {:?}", k) @@ -324,13 +328,13 @@ impl Css { writeln!(buf, "{}}}", padding)?; } Toplevel::Keyframes(k) => { - let Keyframes { name, body } = *k; + let Keyframes { rule, name, body } = *k; if should_emit_newline { should_emit_newline = false; writeln!(buf)?; } - write!(buf, "{}@keyframes", padding)?; + write!(buf, "{}@{}", padding, rule)?; if !name.is_empty() { write!(buf, " {}", name)?; diff --git a/src/parse/args.rs b/src/parse/args.rs index b569a16..865c11a 100644 --- a/src/parse/args.rs +++ b/src/parse/args.rs @@ -4,12 +4,10 @@ use codemap::Span; use crate::{ args::{CallArg, CallArgs, FuncArg, FuncArgs}, + common::QuoteKind, error::SassResult, scope::Scope, - utils::{ - peek_ident_no_interpolation, peek_whitespace_or_comment, read_until_closing_paren, - read_until_closing_quote, read_until_closing_square_brace, - }, + utils::{peek_ident_no_interpolation, peek_whitespace_or_comment, read_until_closing_paren}, value::Value, Token, }; @@ -48,6 +46,7 @@ impl<'a> Parser<'a> { match &tok.kind { ',' => { self.toks.next(); + self.whitespace_or_comment(); args.push(FuncArg { name: name.node.into(), default: Some(default), @@ -131,143 +130,226 @@ impl<'a> Parser<'a> { let mut args = HashMap::new(); self.whitespace_or_comment(); let mut name = String::new(); - let mut val: Vec = Vec::new(); + let mut span = self .toks .peek() .ok_or(("expected \")\".", self.span_before))? .pos(); + loop { - match self.toks.peek().cloned() { - Some(Token { kind: '$', pos }) => { - span = span.merge(pos); - self.toks.next(); - let v = peek_ident_no_interpolation(self.toks, false, self.span_before)?; - - peek_whitespace_or_comment(self.toks); - - if let Some(Token { kind: ':', .. }) = self.toks.peek() { - self.toks.truncate_iterator_to_cursor(); - self.toks.next(); - name = v.node; - } else { - val.push(Token::new(pos, '$')); - self.toks.reset_cursor(); - name.clear(); - } - } - Some(Token { kind: ')', .. }) => { - self.toks.next(); - return Ok(CallArgs(args, span)); - } - Some(..) | None => name.clear(), - } self.whitespace_or_comment(); - let mut is_splat = false; - - while let Some(tok) = self.toks.next() { - match tok.kind { - ')' => { - args.insert( - if name.is_empty() { - CallArg::Positional(args.len()) - } else { - CallArg::Named(mem::take(&mut name).into()) - }, - self.parse_value_from_vec(val, true), - ); - span = span.merge(tok.pos()); - return Ok(CallArgs(args, span)); - } - ',' => break, - '[' => { - val.push(tok); - val.append(&mut read_until_closing_square_brace(self.toks)?); - } - '(' => { - val.push(tok); - val.append(&mut read_until_closing_paren(self.toks)?); - } - '"' | '\'' => { - val.push(tok); - val.append(&mut read_until_closing_quote(self.toks, tok.kind)?); - } - '.' => { - if let Some(Token { kind: '.', pos }) = self.toks.peek().cloned() { - if !name.is_empty() { - return Err(("expected \")\".", pos).into()); - } - self.toks.next(); - if let Some(Token { kind: '.', .. }) = self.toks.peek() { - self.toks.next(); - is_splat = true; - break; - } else { - return Err(("expected \".\".", pos).into()); - } - } else { - val.push(tok); - } - } - _ => val.push(tok), - } + if matches!(self.toks.peek(), Some(Token { kind: ')', .. })) { + self.toks.next(); + return Ok(CallArgs(args, span)); } - if is_splat { - let val = self.parse_value_from_vec(mem::take(&mut val), true)?; - match val.node { - Value::ArgList(v) => { - for arg in v { - args.insert(CallArg::Positional(args.len()), Ok(arg)); - } - } - Value::List(v, ..) => { - for arg in v { - args.insert(CallArg::Positional(args.len()), Ok(arg.span(val.span))); - } - } - Value::Map(v) => { - // NOTE: we clone the map here because it is used - // later for error reporting. perhaps there is - // some way around this? - for (name, arg) in v.clone().entries() { - let name = match name { - Value::String(s, ..) => s, - _ => { - return Err(( - format!( - "{} is not a string in {}.", - name.inspect(val.span)?, - Value::Map(v).inspect(val.span)? - ), - val.span, - ) - .into()) - } - }; - args.insert(CallArg::Named(name.into()), Ok(arg.span(val.span))); - } - } - _ => { - args.insert(CallArg::Positional(args.len()), Ok(val)); - } + if let Some(Token { kind: '$', pos }) = self.toks.peek() { + span = span.merge(*pos); + self.toks.advance_cursor(); + + let v = peek_ident_no_interpolation(self.toks, false, self.span_before)?; + + peek_whitespace_or_comment(self.toks); + + if let Some(Token { kind: ':', .. }) = self.toks.peek() { + self.toks.truncate_iterator_to_cursor(); + self.toks.next(); + name = v.node; + } else { + self.toks.reset_cursor(); + name.clear(); } } else { - args.insert( - if name.is_empty() { - CallArg::Positional(args.len()) - } else { - CallArg::Named(mem::take(&mut name).into()) - }, - self.parse_value_from_vec(mem::take(&mut val), true), - ); + name.clear(); } self.whitespace_or_comment(); - if self.toks.peek().is_none() { - return Err(("expected \")\".", span).into()); + let value = self.parse_value(true, &|c| match c.peek() { + Some(Token { kind: ')', .. }) | Some(Token { kind: ',', .. }) => true, + Some(Token { kind: '.', .. }) => { + if matches!(c.peek_next(), Some(Token { kind: '.', .. })) { + c.reset_cursor(); + true + } else { + c.reset_cursor(); + false + } + } + Some(Token { kind: '=', .. }) => { + if matches!(c.peek_next(), Some(Token { kind: '=', .. })) { + c.reset_cursor(); + false + } else { + c.reset_cursor(); + true + } + } + Some(..) | None => false, + }); + + match self.toks.peek() { + Some(Token { kind: ')', .. }) => { + self.toks.next(); + args.insert( + if name.is_empty() { + CallArg::Positional(args.len()) + } else { + CallArg::Named(mem::take(&mut name).into()) + }, + value, + ); + return Ok(CallArgs(args, span)); + } + Some(Token { kind: ',', .. }) => { + self.toks.next(); + args.insert( + if name.is_empty() { + CallArg::Positional(args.len()) + } else { + CallArg::Named(mem::take(&mut name).into()) + }, + value, + ); + self.whitespace_or_comment(); + continue; + } + Some(Token { kind: '.', pos }) => { + let pos = *pos; + self.toks.next(); + + if let Some(Token { kind: '.', pos }) = self.toks.peek().cloned() { + if !name.is_empty() { + return Err(("expected \")\".", pos).into()); + } + self.toks.next(); + if let Some(Token { kind: '.', .. }) = self.toks.peek() { + self.toks.next(); + } else { + return Err(("expected \".\".", pos).into()); + } + } else { + return Err(("expected \")\".", pos).into()); + } + + let val = value?; + match val.node { + Value::ArgList(v) => { + for arg in v { + args.insert(CallArg::Positional(args.len()), Ok(arg)); + } + } + Value::List(v, ..) => { + for arg in v { + args.insert( + CallArg::Positional(args.len()), + Ok(arg.span(val.span)), + ); + } + } + Value::Map(v) => { + // NOTE: we clone the map here because it is used + // later for error reporting. perhaps there is + // some way around this? + for (name, arg) in v.clone().entries() { + let name = match name { + Value::String(s, ..) => s, + _ => { + return Err(( + format!( + "{} is not a string in {}.", + name.inspect(val.span)?, + Value::Map(v).inspect(val.span)? + ), + val.span, + ) + .into()) + } + }; + args.insert(CallArg::Named(name.into()), Ok(arg.span(val.span))); + } + } + _ => { + args.insert(CallArg::Positional(args.len()), Ok(val)); + } + } + } + Some(Token { kind: '=', .. }) => { + self.toks.next(); + let left = value?; + + let right = self.parse_value(true, &|c| match c.peek() { + Some(Token { kind: ')', .. }) | Some(Token { kind: ',', .. }) => true, + Some(Token { kind: '.', .. }) => { + if matches!(c.peek_next(), Some(Token { kind: '.', .. })) { + c.reset_cursor(); + true + } else { + c.reset_cursor(); + false + } + } + Some(..) | None => false, + })?; + + let value_span = left.span.merge(right.span); + span = span.merge(value_span); + + let value = format!( + "{}={}", + left.node.to_css_string(left.span)?, + right.node.to_css_string(right.span)? + ); + + args.insert( + if name.is_empty() { + CallArg::Positional(args.len()) + } else { + CallArg::Named(mem::take(&mut name).into()) + }, + Ok(Value::String(value, QuoteKind::None).span(value_span)), + ); + + match self.toks.peek() { + Some(Token { kind: ')', .. }) => { + self.toks.next(); + return Ok(CallArgs(args, span)); + } + Some(Token { kind: ',', pos }) => { + span = span.merge(*pos); + self.toks.next(); + self.whitespace_or_comment(); + continue; + } + Some(Token { kind: '.', pos }) => { + let pos = *pos; + self.toks.next(); + + if let Some(Token { kind: '.', pos }) = self.toks.peek().cloned() { + if !name.is_empty() { + return Err(("expected \")\".", pos).into()); + } + self.toks.next(); + if let Some(Token { kind: '.', .. }) = self.toks.peek() { + self.toks.next(); + } else { + return Err(("expected \".\".", pos).into()); + } + } else { + return Err(("expected \")\".", pos).into()); + } + } + Some(..) => unreachable!(), + None => return Err(("expected \")\".", span).into()), + } + } + Some(..) => { + value?; + unreachable!() + } + None => return Err(("expected \")\".", span).into()), } } } diff --git a/src/parse/control_flow.rs b/src/parse/control_flow.rs index 0293664..babbd43 100644 --- a/src/parse/control_flow.rs +++ b/src/parse/control_flow.rs @@ -22,7 +22,7 @@ impl<'a> Parser<'a> { let mut found_true = false; let mut body = Vec::new(); - let init_cond = self.parse_value(true)?.node; + let init_cond = self.parse_value(true, &|_| false)?.node; // consume the open curly brace let span_before = match self.toks.next() { @@ -87,7 +87,7 @@ impl<'a> Parser<'a> { self.throw_away_until_open_curly_brace()?; false } else { - let v = self.parse_value(true)?.node.is_true(); + let v = self.parse_value(true, &|_| false)?.node.is_true(); match self.toks.next() { Some(Token { kind: '{', .. }) => {} Some(..) | None => { @@ -259,7 +259,7 @@ impl<'a> Parser<'a> { } }; - let to_val = self.parse_value(true)?; + let to_val = self.parse_value(true, &|_| false)?; let to = match to_val.node { Value::Dimension(Some(n), ..) => match n.to_integer().to_isize() { Some(v) => v, diff --git a/src/parse/import.rs b/src/parse/import.rs index eaa0fd0..5863e3c 100644 --- a/src/parse/import.rs +++ b/src/parse/import.rs @@ -124,7 +124,7 @@ impl<'a> Parser<'a> { let Spanned { node: file_name_as_value, span, - } = self.parse_value(true)?; + } = self.parse_value(true, &|_| false)?; match file_name_as_value { Value::String(s, QuoteKind::Quoted) => { diff --git a/src/parse/keyframes.rs b/src/parse/keyframes.rs index 8494a5e..21ae99b 100644 --- a/src/parse/keyframes.rs +++ b/src/parse/keyframes.rs @@ -55,7 +55,14 @@ impl<'a, 'b> KeyframesSelectorParser<'a, 'b> { } } '0'..='9' => { - let num = eat_whole_number(self.parser.toks); + let mut num = eat_whole_number(self.parser.toks); + + if let Some(Token { kind: '.', .. }) = self.parser.toks.peek() { + self.parser.toks.next(); + num.push('.'); + num.push_str(&eat_whole_number(self.parser.toks)); + } + if !matches!(self.parser.toks.next(), Some(Token { kind: '%', .. })) { return Err(("expected \"%\".", tok.pos).into()); } @@ -178,7 +185,7 @@ impl<'a> Parser<'a> { Err(("expected \"{\".", span).into()) } - pub(super) fn parse_keyframes(&mut self) -> SassResult { + pub(super) fn parse_keyframes(&mut self, rule: String) -> SassResult { let name = self.parse_keyframes_name()?; self.whitespace(); @@ -202,6 +209,6 @@ impl<'a> Parser<'a> { } .parse_stmt()?; - Ok(Stmt::Keyframes(Box::new(Keyframes { name, body }))) + Ok(Stmt::Keyframes(Box::new(Keyframes { rule, name, body }))) } } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index b6a9456..80c3759 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -282,7 +282,7 @@ impl<'a> Parser<'a> { let Spanned { node: message, span, - } = self.parse_value(false)?; + } = self.parse_value(false, &|_| false)?; return Err(( message.inspect(span)?.to_string(), @@ -294,7 +294,7 @@ impl<'a> Parser<'a> { let Spanned { node: message, span, - } = self.parse_value(false)?; + } = self.parse_value(false, &|_| false)?; span.merge(kind_string.span); if let Some(Token { kind: ';', pos }) = self.toks.peek() { kind_string.span.merge(*pos); @@ -309,7 +309,7 @@ impl<'a> Parser<'a> { let Spanned { node: message, span, - } = self.parse_value(false)?; + } = self.parse_value(false, &|_| false)?; span.merge(kind_string.span); if let Some(Token { kind: ';', pos }) = self.toks.peek() { kind_string.span.merge(*pos); @@ -345,7 +345,9 @@ impl<'a> Parser<'a> { AtRuleKind::Forward => todo!("@forward not yet implemented"), AtRuleKind::Extend => self.parse_extend()?, AtRuleKind::Supports => stmts.push(self.parse_supports()?), - AtRuleKind::Keyframes => stmts.push(self.parse_keyframes()?), + AtRuleKind::Keyframes => { + stmts.push(self.parse_keyframes(kind_string.node)?) + } } } '$' => self.parse_variable_declaration()?, @@ -580,7 +582,7 @@ impl<'a> Parser<'a> { } pub fn parse_interpolation(&mut self) -> SassResult> { - let val = self.parse_value(true)?; + let val = self.parse_value(true, &|_| false)?; match self.toks.next() { Some(Token { kind: '}', .. }) => {} Some(..) | None => return Err(("expected \"}\".", val.span).into()), diff --git a/src/parse/style.rs b/src/parse/style.rs index d4052b1..0c40a66 100644 --- a/src/parse/style.rs +++ b/src/parse/style.rs @@ -173,7 +173,7 @@ impl<'a> Parser<'a> { } fn parse_style_value(&mut self) -> SassResult> { - self.parse_value(false) + self.parse_value(false, &|_| false) } pub(super) fn parse_style_group( diff --git a/src/parse/value/parse.rs b/src/parse/value/parse.rs index 6fd9324..8723d67 100644 --- a/src/parse/value/parse.rs +++ b/src/parse/value/parse.rs @@ -1,4 +1,4 @@ -use std::{iter::Iterator, mem}; +use std::{iter::Iterator, mem, vec::IntoIter}; use num_bigint::BigInt; use num_rational::{BigRational, Rational64}; @@ -6,7 +6,7 @@ use num_traits::{pow, One, ToPrimitive}; use codemap::{Span, Spanned}; -use peekmore::PeekMore; +use peekmore::{PeekMore, PeekMoreIterator}; use crate::{ builtin::GLOBAL_FUNCTIONS, @@ -15,8 +15,8 @@ use crate::{ error::SassResult, unit::Unit, utils::{ - devour_whitespace, eat_number, read_until_char, read_until_closing_paren, - read_until_closing_square_brace, IsWhitespace, + devour_whitespace, eat_whole_number, read_until_closing_paren, + read_until_closing_square_brace, IsWhitespace, ParsedNumber, }, value::{Number, SassFunction, SassMap, Value}, Token, @@ -52,8 +52,16 @@ impl IsWhitespace for IntermediateValue { } impl<'a> Parser<'a> { - pub(crate) fn parse_value(&mut self, in_paren: bool) -> SassResult> { + /// Parse a value from a stream of tokens + /// + /// This function will cease parsing if the predicate returns true. + pub(crate) fn parse_value( + &mut self, + in_paren: bool, + predicate: &dyn Fn(&mut PeekMoreIterator>) -> bool, + ) -> SassResult> { self.whitespace(); + let span = match self.toks.peek() { Some(Token { kind: '}', .. }) | Some(Token { kind: ';', .. }) @@ -61,10 +69,15 @@ impl<'a> Parser<'a> { | None => return Err(("Expected expression.", self.span_before).into()), Some(Token { pos, .. }) => *pos, }; + + if predicate(self.toks) { + return Err(("Expected expression.", span).into()); + } + let mut last_was_whitespace = false; let mut space_separated = Vec::new(); let mut comma_separated = Vec::new(); - let mut iter = IntermediateValueIterator::new(self); + let mut iter = IntermediateValueIterator::new(self, &predicate); while let Some(val) = iter.next() { let val = val?; match val.node { @@ -182,6 +195,32 @@ impl<'a> Parser<'a> { }) } + fn parse_value_with_body( + &mut self, + toks: &mut PeekMoreIterator>, + in_paren: bool, + predicate: &dyn Fn(&mut PeekMoreIterator>) -> bool, + ) -> SassResult> { + Parser { + toks, + map: self.map, + path: self.path, + scopes: self.scopes, + global_scope: self.global_scope, + super_selectors: self.super_selectors, + span_before: self.span_before, + content: self.content, + flags: self.flags, + at_root: self.at_root, + at_root_has_selector: self.at_root_has_selector, + extender: self.extender, + content_scopes: self.content_scopes, + options: self.options, + modules: self.modules, + } + .parse_value(in_paren, predicate) + } + pub(crate) fn parse_value_from_vec( &mut self, toks: Vec, @@ -204,7 +243,7 @@ impl<'a> Parser<'a> { options: self.options, modules: self.modules, } - .parse_value(in_paren) + .parse_value(in_paren, &|_| false) } #[allow(clippy::eval_order_dependence)] @@ -400,7 +439,85 @@ impl<'a> Parser<'a> { } } - fn parse_intermediate_value(&mut self) -> Option>> { + fn parse_number( + &mut self, + predicate: &dyn Fn(&mut PeekMoreIterator>) -> bool, + ) -> SassResult> { + let mut span = self.toks.peek().unwrap().pos; + let mut whole = eat_whole_number(self.toks); + + if self.toks.peek().is_none() || predicate(self.toks) { + return Ok(Spanned { + node: ParsedNumber::new(whole, 0, String::new(), true), + span, + }); + } + + let next_tok = *self.toks.peek().unwrap(); + + let dec_len = if next_tok.kind == '.' { + self.toks.next(); + + let dec = eat_whole_number(self.toks); + if dec.is_empty() { + return Err(("Expected digit.", next_tok.pos()).into()); + } + + whole.push_str(&dec); + + dec.len() + } else { + 0 + }; + + let mut times_ten = String::new(); + let mut times_ten_is_postive = true; + if let Some(Token { kind: 'e', .. }) | Some(Token { kind: 'E', .. }) = self.toks.peek() { + if let Some(&tok) = self.toks.peek_next() { + if tok.kind == '-' { + self.toks.next(); + times_ten_is_postive = false; + + self.toks.next(); + times_ten = eat_whole_number(self.toks); + + if times_ten.is_empty() { + return Err( + ("Expected digit.", self.toks.peek().unwrap_or(&tok).pos).into() + ); + } + } else if matches!(tok.kind, '0'..='9') { + self.toks.next(); + times_ten = eat_whole_number(self.toks); + + if times_ten.len() > 2 { + return Err( + ("Exponent too large.", self.toks.peek().unwrap_or(&tok).pos).into(), + ); + } + } + } + } + + if let Ok(Some(Token { pos, .. })) = self.toks.peek_previous() { + span = span.merge(*pos); + } + + self.toks.reset_cursor(); + + Ok(Spanned { + node: ParsedNumber::new(whole, dec_len, times_ten, times_ten_is_postive), + span, + }) + } + + fn parse_intermediate_value( + &mut self, + predicate: &dyn Fn(&mut PeekMoreIterator>) -> bool, + ) -> Option>> { + if predicate(self.toks) { + return None; + } let (kind, span) = match self.toks.peek() { Some(v) => (v.kind, v.pos()), None => return None, @@ -428,7 +545,7 @@ impl<'a> Parser<'a> { let Spanned { node: val, mut span, - } = match eat_number(self.toks) { + } = match self.parse_number(predicate) { Ok(v) => v, Err(e) => return Some(Err(e)), }; @@ -780,6 +897,7 @@ impl<'a> Parser<'a> { struct IntermediateValueIterator<'a, 'b: 'a> { parser: &'a mut Parser<'b>, peek: Option>>, + predicate: &'a dyn Fn(&mut PeekMoreIterator>) -> bool, } impl<'a, 'b: 'a> Iterator for IntermediateValueIterator<'a, 'b> { @@ -788,14 +906,21 @@ impl<'a, 'b: 'a> Iterator for IntermediateValueIterator<'a, 'b> { if self.peek.is_some() { self.peek.take() } else { - self.parser.parse_intermediate_value() + self.parser.parse_intermediate_value(self.predicate) } } } impl<'a, 'b: 'a> IntermediateValueIterator<'a, 'b> { - pub fn new(parser: &'a mut Parser<'b>) -> Self { - Self { parser, peek: None } + pub fn new( + parser: &'a mut Parser<'b>, + predicate: &'a dyn Fn(&mut PeekMoreIterator>) -> bool, + ) -> Self { + Self { + parser, + peek: None, + predicate, + } } fn peek(&mut self) -> &Option>> { @@ -1122,9 +1247,13 @@ impl<'a, 'b: 'a> IntermediateValueIterator<'a, 'b> { let paren_toks = &mut t.node.into_iter().peekmore(); let mut map = SassMap::new(); - let key = self - .parser - .parse_value_from_vec(read_until_char(paren_toks, ':')?, true)?; + let key = self.parser.parse_value_with_body(paren_toks, true, &|c| { + matches!(c.peek(), Some(Token { kind: ':', .. })) + })?; + + if let Some(Token { kind: ':', .. }) = paren_toks.peek() { + paren_toks.next(); + } if paren_toks.peek().is_none() { return Ok(Spanned { @@ -1135,9 +1264,13 @@ impl<'a, 'b: 'a> IntermediateValueIterator<'a, 'b> { }); } - let val = self - .parser - .parse_value_from_vec(read_until_char(paren_toks, ',')?, true)?; + let val = self.parser.parse_value_with_body(paren_toks, true, &|c| { + matches!(c.peek(), Some(Token { kind: ',', .. })) + })?; + + if let Some(Token { kind: ',', .. }) = paren_toks.peek() { + paren_toks.next(); + } map.insert(key.node, val.node); @@ -1153,16 +1286,25 @@ impl<'a, 'b: 'a> IntermediateValueIterator<'a, 'b> { let mut span = key.span; loop { - let key = self - .parser - .parse_value_from_vec(read_until_char(paren_toks, ':')?, true)?; + let key = self.parser.parse_value_with_body(paren_toks, true, &|c| { + matches!(c.peek(), Some(Token { kind: ':', .. })) + })?; + + if let Some(Token { kind: ':', .. }) = paren_toks.peek() { + paren_toks.next(); + } + devour_whitespace(paren_toks); - let val = self - .parser - .parse_value_from_vec(read_until_char(paren_toks, ',')?, true)?; + let val = self.parser.parse_value_with_body(paren_toks, true, &|c| { + matches!(c.peek(), Some(Token { kind: ',', .. })) + })?; + + if let Some(Token { kind: ',', .. }) = paren_toks.peek() { + paren_toks.next(); + } span = span.merge(val.span); devour_whitespace(paren_toks); - if map.insert(key.node, val.node) { + if map.insert(key.node.clone(), val.node) { return Err(("Duplicate key.", key.span).into()); } if paren_toks.peek().is_none() { diff --git a/src/utils/chars.rs b/src/utils/chars.rs index 68efb60..d8af874 100644 --- a/src/utils/chars.rs +++ b/src/utils/chars.rs @@ -1,37 +1,3 @@ -use std::vec::IntoIter; - -use peekmore::PeekMoreIterator; - -use crate::{error::SassResult, Token}; - -use super::{read_until_closing_paren, read_until_closing_quote}; -/// Reads until the char is found, consuming the char, -/// or until the end of the iterator is hit -pub(crate) fn read_until_char( - toks: &mut PeekMoreIterator>, - c: char, -) -> SassResult> { - let mut v = Vec::new(); - while let Some(tok) = toks.next() { - match tok.kind { - '"' | '\'' => { - v.push(tok); - v.extend(read_until_closing_quote(toks, tok.kind)?); - continue; - } - '(' => { - v.push(tok); - v.extend(read_until_closing_paren(toks)?); - continue; - } - t if t == c => break, - _ => {} - } - v.push(tok) - } - Ok(v) -} - pub(crate) fn hex_char_for(number: u32) -> char { debug_assert!(number < 0x10); std::char::from_u32(if number < 0xA { diff --git a/src/utils/number.rs b/src/utils/number.rs index a51c754..1332f37 100644 --- a/src/utils/number.rs +++ b/src/utils/number.rs @@ -1,9 +1,8 @@ use std::vec::IntoIter; -use codemap::Spanned; use peekmore::PeekMoreIterator; -use crate::{error::SassResult, Token}; +use crate::Token; #[derive(Debug)] pub(crate) struct ParsedNumber { @@ -47,73 +46,6 @@ impl ParsedNumber { } } -pub(crate) fn eat_number( - toks: &mut PeekMoreIterator>, -) -> SassResult> { - let mut span = toks.peek().unwrap().pos; - let mut whole = eat_whole_number(toks); - - if toks.peek().is_none() { - return Ok(Spanned { - node: ParsedNumber::new(whole, 0, String::new(), true), - span, - }); - } - - let next_tok = *toks.peek().unwrap(); - - let dec_len = if next_tok.kind == '.' { - toks.next(); - - let dec = eat_whole_number(toks); - if dec.is_empty() { - return Err(("Expected digit.", next_tok.pos()).into()); - } - - whole.push_str(&dec); - - dec.len() - } else { - 0 - }; - - let mut times_ten = String::new(); - let mut times_ten_is_postive = true; - if let Some(Token { kind: 'e', .. }) | Some(Token { kind: 'E', .. }) = toks.peek() { - if let Some(&tok) = toks.peek_next() { - if tok.kind == '-' { - toks.next(); - times_ten_is_postive = false; - - toks.next(); - times_ten = eat_whole_number(toks); - - if times_ten.is_empty() { - return Err(("Expected digit.", toks.peek().unwrap_or(&tok).pos).into()); - } - } else if matches!(tok.kind, '0'..='9') { - toks.next(); - times_ten = eat_whole_number(toks); - - if times_ten.len() > 2 { - return Err(("Exponent too large.", toks.peek().unwrap_or(&tok).pos).into()); - } - } - } - } - - if let Ok(Some(Token { pos, .. })) = toks.peek_previous() { - span = span.merge(*pos); - } - - toks.reset_cursor(); - - Ok(Spanned { - node: ParsedNumber::new(whole, dec_len, times_ten, times_ten_is_postive), - span, - }) -} - pub(crate) fn eat_whole_number(toks: &mut PeekMoreIterator>) -> String { let mut buf = String::new(); while let Some(c) = toks.peek() { diff --git a/tests/args.rs b/tests/args.rs index 0afd994..6c8de5f 100644 --- a/tests/args.rs +++ b/tests/args.rs @@ -86,3 +86,85 @@ test!( }", "a {\n color: red;\n}\n" ); +test!( + comment_after_comma_in_func_args, + "@mixin a( + $foo,//foo + ) { + color: $foo; + } + + a { + @include a(red); + }", + "a {\n color: red;\n}\n" +); +test!( + filter_one_arg, + "a {\n color: foo(a=a);\n}\n", + "a {\n color: foo(a=a);\n}\n" +); +test!( + filter_two_args, + "a {\n color: foo(a=a, b=b);\n}\n", + "a {\n color: foo(a=a, b=b);\n}\n" +); +test!( + filter_whitespace, + "a {\n color: foo( a = a );\n}\n", + "a {\n color: foo(a=a);\n}\n" +); +test!( + filter_whitespace_list, + "a {\n color: foo( A a = a );\n}\n", + "a {\n color: foo(A a=a);\n}\n" +); +test!( + filter_function_call, + "a {\n color: foo(hue(green)=hue(green));\n}\n", + "a {\n color: foo(120deg=120deg);\n}\n" +); +test!( + filter_addition, + "a {\n color: foo(1+1=1+1);\n}\n", + "a {\n color: foo(2=2);\n}\n" +); +test!( + filter_splat_of_single_value, + "a {\n color: foo(a=a...);\n}\n", + "a {\n color: foo(a=a);\n}\n" +); +test!( + filter_splat_of_list, + "a {\n color: foo(a=[a, b]...);\n}\n", + "a {\n color: foo(a=[a, b]);\n}\n" +); +test!( + filter_both_null, + "a {\n color: foo(null=null);\n}\n", + "a {\n color: foo(=);\n}\n" +); +error!( + filter_splat_missing_third_period, + "a {\n color: foo(1 + 1 = a..);\n}\n", "Error: expected \".\"." +); +error!( + filter_invalid_css_value, + "a {\n color: foo((a: b)=a);\n}\n", "Error: (a: b) isn't a valid CSS value." +); +error!( + filter_nothing_before_equal, + "a {\n color: foo(=a);\n}\n", "Error: Expected expression." +); +error!( + filter_nothing_after_equal, + "a {\n color: foo(a=);\n}\n", "Error: Expected expression." +); +error!( + filter_equal_is_last_char, + "a {\n color: foo(a=", "Error: Expected expression." +); +error!( + filter_value_after_equal_is_last_char, + "a {\n color: foo(a=a", "Error: expected \")\"." +); diff --git a/tests/color.rs b/tests/color.rs index 8e4add5..0afa92a 100644 --- a/tests/color.rs +++ b/tests/color.rs @@ -638,3 +638,39 @@ test!( "a {\n color: hsla(0deg, 100%, 50%);\n}\n", "a {\n color: red;\n}\n" ); +test!( + alpha_filter_one_arg, + "a {\n color: alpha(a=a);\n}\n", + "a {\n color: alpha(a=a);\n}\n" +); +test!( + alpha_filter_multiple_args, + "a {\n color: alpha(a=a, b=b, c=d, d=d);\n}\n", + "a {\n color: alpha(a=a, b=b, c=d, d=d);\n}\n" +); +test!( + alpha_filter_whitespace, + "a {\n color: alpha(a = a);\n}\n", + "a {\n color: alpha(a=a);\n}\n" +); +test!( + alpha_filter_named, + "a {\n color: alpha($color: a=a);\n}\n", + "a {\n color: alpha(a=a);\n}\n" +); +error!( + alpha_filter_both_null, + "a {\n color: alpha(null=null);\n}\n", "Error: $color: = is not a color." +); +error!( + alpha_filter_multiple_args_one_not_valid_filter, + "a {\n color: alpha(a=a, b);\n}\n", "Error: Only 1 argument allowed, but 2 were passed." +); +error!( + alpha_filter_invalid_from_whitespace, + "a {\n color: alpha( A a = a );\n}\n", "Error: $color: A a=a is not a color." +); +error!( + alpha_filter_invalid_non_alphabetic_start, + "a {\n color: alpha(1=a);\n}\n", "Error: $color: 1=a is not a color." +); diff --git a/tests/if.rs b/tests/if.rs index 008021d..ae9d4a3 100644 --- a/tests/if.rs +++ b/tests/if.rs @@ -169,7 +169,10 @@ error!( ); error!(unclosed_dbl_quote, "@if true \" {}", "Error: Expected \"."); error!(unclosed_sgl_quote, "@if true ' {}", "Error: Expected '."); -error!(unclosed_call_args, "@if a({}", "Error: expected \")\"."); +error!( + unclosed_call_args, + "@if a({}", "Error: Expected expression." +); error!(nothing_after_div, "@if a/", "Error: Expected expression."); error!(multiline_error, "@if \"\n\"{}", "Error: Expected \"."); error!( diff --git a/tests/keyframes.rs b/tests/keyframes.rs index ea22d8b..1bd77ad 100644 --- a/tests/keyframes.rs +++ b/tests/keyframes.rs @@ -121,3 +121,21 @@ test!( }", "@keyframes {\n to {\n color: red;\n }\n from {\n color: green;\n }\n}\n" ); +test!( + keyframes_vendor_prefix, + "@-webkit-keyframes foo { + 0% { + color: red; + } + }", + "@-webkit-keyframes foo {\n 0% {\n color: red;\n }\n}\n" +); +test!( + keyframes_allow_decimal_selector, + "@keyframes foo { + 12.5% { + color: red; + } + }", + "@-webkit-keyframes foo {\n 0% {\n color: red;\n }\n}\n" +); diff --git a/tests/map.rs b/tests/map.rs index 67d4c6d..65c5ac0 100644 --- a/tests/map.rs +++ b/tests/map.rs @@ -197,3 +197,11 @@ test!( "a {\n color: (a: b)==(a: c);\n}\n", "a {\n color: false;\n}\n" ); +test!( + empty_with_single_line_comments, + "$foo: (\n \n // :/a.b\n \n ); + a { + color: inspect($foo); + }", + "a {\n color: ();\n}\n" +);