From 8e08a5de4f79bd2bc26fb0a177dc9f8449a21240 Mon Sep 17 00:00:00 2001 From: Connor Skees Date: Mon, 12 Jul 2021 01:59:30 -0400 Subject: [PATCH] support special fns inside min and max --- src/lexer.rs | 21 ++- src/parse/ident.rs | 38 ++++- src/parse/value/css_function.rs | 279 ++++++++++++++++++++++---------- src/parse/value/parse.rs | 41 ++--- src/selector/parse.rs | 62 ++----- tests/extend.rs | 11 ++ tests/min-max.rs | 40 +++++ tests/selectors.rs | 5 + 8 files changed, 329 insertions(+), 168 deletions(-) diff --git a/src/lexer.rs b/src/lexer.rs index 3af8eed..c11006a 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -30,10 +30,6 @@ impl Lexer { self.amt_peeked += 1; } - pub fn move_cursor_back(&mut self) { - self.amt_peeked = self.amt_peeked.saturating_sub(1); - } - pub fn peek_next(&mut self) -> Option { self.amt_peeked += 1; @@ -41,7 +37,7 @@ impl Lexer { } pub fn peek_previous(&mut self) -> Option { - self.buf.get(self.peek_cursor() - 1).copied() + self.buf.get(self.peek_cursor().checked_sub(1)?).copied() } pub fn peek_forward(&mut self, n: usize) -> Option { @@ -50,6 +46,11 @@ impl Lexer { self.peek() } + /// Peeks `n` from current peeked position without modifying cursor + pub fn peek_n(&self, n: usize) -> Option { + self.buf.get(self.peek_cursor() + n).copied() + } + pub fn peek_backward(&mut self, n: usize) -> Option { self.amt_peeked = self.amt_peeked.checked_sub(n)?; @@ -60,6 +61,16 @@ impl Lexer { self.cursor += self.amt_peeked; self.amt_peeked = 0; } + + /// Set cursor to position and reset peek + pub fn set_cursor(&mut self, cursor: usize) { + self.cursor = cursor; + self.amt_peeked = 0; + } + + pub fn cursor(&self) -> usize { + self.cursor + } } impl Iterator for Lexer { diff --git a/src/parse/ident.rs b/src/parse/ident.rs index dcf74e0..0be4155 100644 --- a/src/parse/ident.rs +++ b/src/parse/ident.rs @@ -36,7 +36,7 @@ impl<'a> Parser<'a> { text.push(self.toks.next().unwrap().kind); } else if tok.kind == '\\' { self.toks.next(); - text.push_str(&self.escape(false)?); + text.push_str(&self.parse_escape(false)?); } else { break; } @@ -56,7 +56,7 @@ impl<'a> Parser<'a> { } '\\' => { self.toks.next(); - buf.push_str(&self.escape(false)?); + buf.push_str(&self.parse_escape(false)?); } '#' => { if let Some(Token { kind: '{', .. }) = self.toks.peek_forward(1) { @@ -76,7 +76,7 @@ impl<'a> Parser<'a> { Ok(()) } - fn escape(&mut self, identifier_start: bool) -> SassResult { + pub(crate) fn parse_escape(&mut self, identifier_start: bool) -> SassResult { let mut value = 0; let first = match self.toks.peek() { Some(t) => t, @@ -172,7 +172,7 @@ impl<'a> Parser<'a> { } '\\' => { self.toks.next(); - text.push_str(&self.escape(true)?); + text.push_str(&self.parse_escape(true)?); } '#' if matches!(self.toks.peek_forward(1), Some(Token { kind: '{', .. })) => { self.toks.next(); @@ -228,7 +228,7 @@ impl<'a> Parser<'a> { if is_name_start(first.kind) { text.push(first.kind); } else if first.kind == '\\' { - text.push_str(&self.escape(true)?); + text.push_str(&self.parse_escape(true)?); } else { return Err(("Expected identifier.", first.pos).into()); } @@ -325,4 +325,32 @@ impl<'a> Parser<'a> { } Err((format!("Expected {}.", q), span).into()) } + + /// Returns whether the scanner is immediately before a plain CSS identifier. + /// + // todo: foward arg + /// If `forward` is passed, this looks that many characters forward instead. + /// + /// This is based on [the CSS algorithm][], but it assumes all backslashes + /// start escapes. + /// + /// [the CSS algorithm]: https://drafts.csswg.org/css-syntax-3/#would-start-an-identifier + pub fn looking_at_identifier(&mut self) -> bool { + match self.toks.peek() { + Some(Token { kind, .. }) if is_name_start(kind) || kind == '\\' => return true, + Some(Token { kind: '-', .. }) => {} + Some(..) | None => return false, + } + + match self.toks.peek_forward(1) { + Some(Token { kind, .. }) if is_name_start(kind) || kind == '-' || kind == '\\' => { + self.toks.reset_cursor(); + true + } + Some(..) | None => { + self.toks.reset_cursor(); + false + } + } + } } diff --git a/src/parse/value/css_function.rs b/src/parse/value/css_function.rs index 7c5b432..0a3260e 100644 --- a/src/parse/value/css_function.rs +++ b/src/parse/value/css_function.rs @@ -5,8 +5,8 @@ use codemap::Spanned; use crate::{ error::SassResult, utils::{ - as_hex, hex_char_for, is_name, peek_ident_no_interpolation, peek_until_closing_curly_brace, - peek_whitespace, + as_hex, hex_char_for, is_name, peek_until_closing_curly_brace, peek_whitespace, + IsWhitespace, }, value::Value, Token, @@ -144,28 +144,23 @@ impl<'a> Parser<'a> { } else { String::new() }; - peek_whitespace(self.toks); + + self.whitespace(); + while let Some(tok) = self.toks.peek() { let kind = tok.kind; match kind { '+' | '-' | '0'..='9' => { - self.toks.advance_cursor(); - if let Some(number) = self.peek_number()? { - buf.push(kind); - buf.push_str(&number); - } else { - return Ok(None); - } + let number = self.parse_dimension(&|_| false)?; + buf.push_str(&number.node.to_css_string(number.span)?); } '#' => { - self.toks.advance_cursor(); + self.toks.next(); if let Some(Token { kind: '{', .. }) = self.toks.peek() { - self.toks.advance_cursor(); - let interpolation = self.peek_interpolation()?; - match interpolation.node { - Value::String(ref s, ..) => buf.push_str(s), - v => buf.push_str(v.to_css_string(interpolation.span)?.borrow()), - }; + self.toks.next(); + let interpolation = self.parse_interpolation_as_string()?; + + buf.push_str(&interpolation); } else { return Ok(None); } @@ -192,7 +187,7 @@ impl<'a> Parser<'a> { } } '(' => { - self.toks.advance_cursor(); + self.toks.next(); buf.push('('); if let Some(val) = self.try_parse_min_max(fn_name, false)? { buf.push_str(&val); @@ -201,10 +196,10 @@ impl<'a> Parser<'a> { } } 'm' | 'M' => { - self.toks.advance_cursor(); + self.toks.next(); let inner_fn_name = match self.toks.peek() { Some(Token { kind: 'i', .. }) | Some(Token { kind: 'I', .. }) => { - self.toks.advance_cursor(); + self.toks.next(); if !matches!( self.toks.peek(), Some(Token { kind: 'n', .. }) | Some(Token { kind: 'N', .. }) @@ -215,7 +210,7 @@ impl<'a> Parser<'a> { "min" } Some(Token { kind: 'a', .. }) | Some(Token { kind: 'A', .. }) => { - self.toks.advance_cursor(); + self.toks.next(); if !matches!( self.toks.peek(), Some(Token { kind: 'x', .. }) | Some(Token { kind: 'X', .. }) @@ -228,13 +223,13 @@ impl<'a> Parser<'a> { _ => return Ok(None), }; - self.toks.advance_cursor(); + self.toks.next(); if !matches!(self.toks.peek(), Some(Token { kind: '(', .. })) { return Ok(None); } - self.toks.advance_cursor(); + self.toks.next(); if let Some(val) = self.try_parse_min_max(inner_fn_name, true)? { buf.push_str(&val); @@ -245,7 +240,7 @@ impl<'a> Parser<'a> { _ => return Ok(None), } - peek_whitespace(self.toks); + self.whitespace(); let next = match self.toks.peek() { Some(tok) => tok, @@ -254,104 +249,218 @@ impl<'a> Parser<'a> { match next.kind { ')' => { - self.toks.advance_cursor(); + self.toks.next(); buf.push(')'); return Ok(Some(buf)); } '+' | '-' | '*' | '/' => { + self.toks.next(); buf.push(' '); buf.push(next.kind); buf.push(' '); - self.toks.advance_cursor(); } ',' => { if !allow_comma { return Ok(None); } - self.toks.advance_cursor(); + self.toks.next(); buf.push(','); buf.push(' '); } _ => return Ok(None), } - peek_whitespace(self.toks); + self.whitespace(); } Ok(Some(buf)) } - #[allow(dead_code, unused_mut, unused_variables, unused_assignments)] fn try_parse_min_max_function(&mut self, fn_name: &'static str) -> SassResult> { - let mut ident = peek_ident_no_interpolation(self.toks, false, self.span_before)?.node; + let mut ident = self.parse_identifier_no_interpolation(false)?.node; ident.make_ascii_lowercase(); + if ident != fn_name { return Ok(None); } + if !matches!(self.toks.peek(), Some(Token { kind: '(', .. })) { return Ok(None); } - self.toks.advance_cursor(); + + self.toks.next(); ident.push('('); - todo!("special functions inside `min()` or `max()`") + + let value = self.declaration_value(true, false, true)?; + + if !matches!(self.toks.peek(), Some(Token { kind: ')', .. })) { + return Ok(None); + } + + self.toks.next(); + + ident.push_str(&value); + + ident.push(')'); + + Ok(Some(ident)) + } + + pub(crate) fn declaration_value( + &mut self, + allow_empty: bool, + allow_semicolon: bool, + allow_colon: bool, + ) -> SassResult { + let mut buffer = String::new(); + + let mut brackets = Vec::new(); + let mut wrote_newline = false; + + while let Some(tok) = self.toks.peek() { + match tok.kind { + '\\' => { + self.toks.next(); + buffer.push_str(&self.parse_escape(true)?); + wrote_newline = false; + } + q @ ('"' | '\'') => { + self.toks.next(); + let s = self.parse_quoted_string(q)?; + buffer.push_str(&s.node.to_css_string(s.span)?); + wrote_newline = false; + } + '/' => { + if matches!(self.toks.peek_n(1), Some(Token { kind: '*', .. })) { + todo!() + } else { + buffer.push('/'); + self.toks.next(); + } + + wrote_newline = false; + } + '#' => { + if matches!(self.toks.peek_n(1), Some(Token { kind: '{', .. })) { + let s = self.parse_identifier()?; + buffer.push_str(&s.node); + } else { + buffer.push('#'); + self.toks.next(); + } + + wrote_newline = false; + } + c @ (' ' | '\t') => { + if wrote_newline + || !self + .toks + .peek_n(1) + .map_or(false, |tok| tok.is_whitespace()) + { + buffer.push(c); + } + + self.toks.next(); + } + '\n' | '\r' => { + if !wrote_newline { + buffer.push('\n'); + } + + wrote_newline = true; + + self.toks.next(); + } + + '[' | '(' | '{' => { + buffer.push(tok.kind); + + self.toks.next(); + + match tok.kind { + '[' => brackets.push(']'), + '(' => brackets.push(')'), + '{' => brackets.push('}'), + _ => unreachable!(), + } + + wrote_newline = false; + } + ']' | ')' | '}' => { + if let Some(end) = brackets.pop() { + self.expect_char(end)?; + } else { + break; + } + + wrote_newline = false; + } + ';' => { + if !allow_semicolon && brackets.is_empty() { + break; + } + + self.toks.next(); + buffer.push(';'); + wrote_newline = false; + } + ':' => { + if !allow_colon && brackets.is_empty() { + break; + } + + self.toks.next(); + buffer.push(':'); + wrote_newline = false; + } + 'u' | 'U' => { + let before_url = self.toks.cursor(); + + if !self.scan_identifier("url") { + buffer.push(tok.kind); + self.toks.next(); + wrote_newline = false; + continue; + } + + if let Some(contents) = self.try_parse_url()? { + buffer.push_str(&contents); + } else { + self.toks.set_cursor(before_url); + buffer.push(tok.kind); + self.toks.next(); + } + + wrote_newline = false; + } + c => { + if self.looking_at_identifier() { + buffer.push_str(&self.parse_identifier()?.node); + } else { + self.toks.next(); + buffer.push(c); + } + + wrote_newline = false; + } + } + } + + if let Some(last) = brackets.pop() { + self.expect_char(last)?; + } + + if !allow_empty && buffer.is_empty() { + return Err(("Expected token.", self.span_before).into()); + } + + Ok(buffer) } } /// Methods required to do arbitrary lookahead impl<'a> Parser<'a> { - fn peek_number(&mut self) -> SassResult> { - let mut buf = String::new(); - - let num = self.peek_whole_number(); - buf.push_str(&num); - - self.toks.advance_cursor(); - - if let Some(Token { kind: '.', .. }) = self.toks.peek() { - self.toks.advance_cursor(); - let num = self.peek_whole_number(); - if num.is_empty() { - return Ok(None); - } - buf.push_str(&num); - } else { - self.toks.move_cursor_back(); - } - - let next = match self.toks.peek() { - Some(tok) => tok, - None => return Ok(Some(buf)), - }; - - match next.kind { - 'a'..='z' | 'A'..='Z' | '-' | '_' | '\\' => { - let unit = peek_ident_no_interpolation(self.toks, true, self.span_before)?.node; - - buf.push_str(&unit); - } - '%' => { - self.toks.advance_cursor(); - buf.push('%'); - } - _ => {} - } - - Ok(Some(buf)) - } - - fn peek_whole_number(&mut self) -> String { - let mut buf = String::new(); - while let Some(tok) = self.toks.peek() { - if tok.kind.is_ascii_digit() { - buf.push(tok.kind); - self.toks.advance_cursor(); - } else { - return buf; - } - } - buf - } - fn peek_interpolation(&mut self) -> SassResult> { let vec = peek_until_closing_curly_brace(self.toks)?; self.toks.advance_cursor(); diff --git a/src/parse/value/parse.rs b/src/parse/value/parse.rs index 3509a7f..00f4cbb 100644 --- a/src/parse/value/parse.rs +++ b/src/parse/value/parse.rs @@ -235,16 +235,16 @@ impl<'a> Parser<'a> { lower: String, ) -> SassResult> { if lower == "min" || lower == "max" { + let start = self.toks.cursor(); match self.try_parse_min_max(&lower, true)? { Some(val) => { - self.toks.truncate_iterator_to_cursor(); return Ok(IntermediateValue::Value(HigherIntermediateValue::Literal( Value::String(val, QuoteKind::None), )) .span(self.span_before)); } None => { - self.toks.reset_cursor(); + self.toks.set_cursor(start); } } } @@ -468,10 +468,19 @@ impl<'a> Parser<'a> { }) } - fn parse_dimension( + fn parse_intermediate_value_dimension( &mut self, predicate: &dyn Fn(&mut Lexer) -> bool, ) -> SassResult> { + let Spanned { node, span } = self.parse_dimension(predicate)?; + + Ok(IntermediateValue::Value(HigherIntermediateValue::Literal(node)).span(span)) + } + + pub(crate) fn parse_dimension( + &mut self, + predicate: &dyn Fn(&mut Lexer) -> bool, + ) -> SassResult> { let Spanned { node: val, mut span, @@ -513,28 +522,19 @@ impl<'a> Parser<'a> { let n = if val.dec_len == 0 { if val.num.len() <= 18 && val.times_ten.is_empty() { let n = Rational64::new_raw(parse_i64(&val.num), 1); - return Ok(IntermediateValue::Value(HigherIntermediateValue::Literal( - Value::Dimension(Some(Number::new_small(n)), unit, false), - )) - .span(span)); + return Ok(Value::Dimension(Some(Number::new_small(n)), unit, false).span(span)); } BigRational::new_raw(val.num.parse::().unwrap(), BigInt::one()) } else { if val.num.len() <= 18 && val.times_ten.is_empty() { let n = Rational64::new(parse_i64(&val.num), pow(10, val.dec_len)); - return Ok(IntermediateValue::Value(HigherIntermediateValue::Literal( - Value::Dimension(Some(Number::new_small(n)), unit, false), - )) - .span(span)); + return Ok(Value::Dimension(Some(Number::new_small(n)), unit, false).span(span)); } BigRational::new(val.num.parse().unwrap(), pow(BigInt::from(10), val.dec_len)) }; if val.times_ten.is_empty() { - return Ok(IntermediateValue::Value(HigherIntermediateValue::Literal( - Value::Dimension(Some(Number::new_big(n)), unit, false), - )) - .span(span)); + return Ok(Value::Dimension(Some(Number::new_big(n)), unit, false).span(span)); } let times_ten = pow( @@ -552,14 +552,7 @@ impl<'a> Parser<'a> { BigRational::new(BigInt::one(), times_ten) }; - Ok( - IntermediateValue::Value(HigherIntermediateValue::Literal(Value::Dimension( - Some(Number::new_big(n * times_ten)), - unit, - false, - ))) - .span(span), - ) + Ok(Value::Dimension(Some(Number::new_big(n * times_ten)), unit, false).span(span)) } fn parse_paren(&mut self) -> SassResult> { @@ -807,7 +800,7 @@ impl<'a> Parser<'a> { } return Some(self.parse_ident_value(predicate)); } - '0'..='9' | '.' => return Some(self.parse_dimension(predicate)), + '0'..='9' | '.' => return Some(self.parse_intermediate_value_dimension(predicate)), '(' => { self.toks.next(); return Some(self.parse_paren()); diff --git a/src/selector/parse.rs b/src/selector/parse.rs index 01b37b0..3f7e472 100644 --- a/src/selector/parse.rs +++ b/src/selector/parse.rs @@ -1,12 +1,6 @@ use codemap::Span; -use crate::{ - common::unvendor, - error::SassResult, - parse::Parser, - utils::{is_name, is_name_start, read_until_closing_paren}, - Token, -}; +use crate::{common::unvendor, error::SassResult, parse::Parser, utils::is_name, Token}; use super::{ Attribute, Combinator, ComplexSelector, ComplexSelectorComponent, CompoundSelector, Namespace, @@ -170,7 +164,7 @@ impl<'a, 'b> SelectorParser<'a, 'b> { } } Some(..) => { - if !self.looking_at_identifier() { + if !self.parser.looking_at_identifier() { break; } components.push(ComplexSelectorComponent::Compound( @@ -208,34 +202,6 @@ impl<'a, 'b> SelectorParser<'a, 'b> { Ok(CompoundSelector { components }) } - /// Returns whether the scanner is immediately before a plain CSS identifier. - /// - // todo: foward arg - /// If `forward` is passed, this looks that many characters forward instead. - /// - /// This is based on [the CSS algorithm][], but it assumes all backslashes - /// start escapes. - /// - /// [the CSS algorithm]: https://drafts.csswg.org/css-syntax-3/#would-start-an-identifier - fn looking_at_identifier(&mut self) -> bool { - match self.parser.toks.peek() { - Some(Token { kind, .. }) if is_name_start(kind) || kind == '\\' => return true, - Some(Token { kind: '-', .. }) => {} - Some(..) | None => return false, - } - - match self.parser.toks.peek_forward(1) { - Some(Token { kind, .. }) if is_name_start(kind) || kind == '-' || kind == '\\' => { - self.parser.toks.reset_cursor(); - true - } - Some(..) | None => { - self.parser.toks.reset_cursor(); - false - } - } - } - fn looking_at_identifier_body(&mut self) -> bool { matches!(self.parser.toks.peek(), Some(t) if is_name(t.kind) || t.kind == '\\') } @@ -323,10 +289,15 @@ impl<'a, 'b> SelectorParser<'a, 'b> { if SELECTOR_PSEUDO_ELEMENTS.contains(&unvendored) { selector = Some(Box::new(self.parse_selector_list()?)); self.parser.whitespace(); - self.parser.expect_char(')')?; } else { - argument = Some(self.declaration_value()?.into_boxed_str()); + argument = Some( + self.parser + .declaration_value(true, false, true)? + .into_boxed_str(), + ); } + + self.parser.expect_char(')')?; } else if SELECTOR_PSEUDO_CLASSES.contains(&unvendored) { selector = Some(Box::new(self.parse_selector_list()?)); self.parser.whitespace(); @@ -349,11 +320,14 @@ impl<'a, 'b> SelectorParser<'a, 'b> { argument = Some(this_arg.into_boxed_str()); } else { argument = Some( - self.declaration_value()? + self.parser + .declaration_value(true, false, true)? .trim_end() .to_owned() .into_boxed_str(), ); + + self.parser.expect_char(')')?; } Ok(SimpleSelector::Pseudo(Pseudo { @@ -527,16 +501,6 @@ impl<'a, 'b> SelectorParser<'a, 'b> { Ok(buf) } - fn declaration_value(&mut self) -> SassResult { - // todo: this consumes the closing paren - let mut tmp = read_until_closing_paren(self.parser.toks)?; - if let Some(Token { kind: ')', .. }) = tmp.pop() { - } else { - return Err(("expected \")\".", self.span).into()); - } - Ok(tmp.into_iter().map(|t| t.kind).collect::()) - } - fn expect_identifier(&mut self, s: &str) -> SassResult<()> { let mut ident = self.parser.parse_identifier_no_interpolation(false)?.node; ident.make_ascii_lowercase(); diff --git a/tests/extend.rs b/tests/extend.rs index b448853..917965a 100644 --- a/tests/extend.rs +++ b/tests/extend.rs @@ -1897,6 +1897,17 @@ test!( }", ".a .b, .a .a.mod5, .a .a.mod6, .a .a.mod3, .a .a.mod4, .a .a.mod1, .a .a.mod2 {\n c: d;\n}\n" ); +test!( + parent_selector_as_value_ignores_extend, + "a { + color: &; + } + + b { + @extend a; + }", + "a, b {\n color: a;\n}\n" +); error!( extend_optional_keyword_not_complete, "a { diff --git a/tests/min-max.rs b/tests/min-max.rs index 66d8a24..2c6725d 100644 --- a/tests/min-max.rs +++ b/tests/min-max.rs @@ -130,3 +130,43 @@ test!( "a {\n color: min(max(min(max(min(min(1), max(2))))), min(max(min(3))));\n}\n", "a {\n color: min(max(min(max(min(min(1), max(2))))), min(max(min(3))));\n}\n" ); +test!( + decimal_without_leading_integer_is_evaluated, + "a {\n color: min(.2, .4);\n}\n", + "a {\n color: 0.2;\n}\n" +); +test!( + decimal_with_leading_integer_is_not_evaluated, + "a {\n color: min(0.2, 0.4);\n}\n", + "a {\n color: min(0.2, 0.4);\n}\n" +); +test!( + min_conains_special_fn_env, + "a {\n color: min(env(\"foo\"));\n}\n", + "a {\n color: min(env(\"foo\"));\n}\n" +); +test!( + min_conains_special_fn_calc_with_div_and_spaces, + "a {\n color: min(calc(1 / 2));\n}\n", + "a {\n color: min(calc(1 / 2));\n}\n" +); +test!( + min_conains_special_fn_calc_with_div_without_spaces, + "a {\n color: min(calc(1/2));\n}\n", + "a {\n color: min(calc(1/2));\n}\n" +); +test!( + min_conains_special_fn_calc_with_plus_only, + "a {\n color: min(calc(+));\n}\n", + "a {\n color: min(calc(+));\n}\n" +); +test!( + min_conains_special_fn_calc_space_separated_list, + "a {\n color: min(calc(1 2));\n}\n", + "a {\n color: min(calc(1 2));\n}\n" +); +test!( + min_conains_special_fn_var, + "a {\n color: min(1, var(--foo));\n}\n", + "a {\n color: min(1, var(--foo));\n}\n" +); diff --git a/tests/selectors.rs b/tests/selectors.rs index 4b8717d..70e061d 100644 --- a/tests/selectors.rs +++ b/tests/selectors.rs @@ -807,6 +807,11 @@ test!( "#{inspect(&)} {\n color: &;\n}\n", "null {\n color: null;\n}\n" ); +test!( + nth_of_type_mutliple_spaces_inside_parens_are_collapsed, + ":nth-of-type(2 n - --1) {\n color: red;\n}\n", + ":nth-of-type(2 n - --1) {\n color: red;\n}\n" +); test!( #[ignore = "we do not yet have a good way of consuming a string without converting \\a to a newline"] silent_comment_in_quoted_attribute_value,