support special fns inside min and max

This commit is contained in:
Connor Skees 2021-07-12 01:59:30 -04:00
parent 6d0eaef9c0
commit 8e08a5de4f
8 changed files with 329 additions and 168 deletions

View File

@ -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<Token> {
self.amt_peeked += 1;
@ -41,7 +37,7 @@ impl Lexer {
}
pub fn peek_previous(&mut self) -> Option<Token> {
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<Token> {
@ -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<Token> {
self.buf.get(self.peek_cursor() + n).copied()
}
pub fn peek_backward(&mut self, n: usize) -> Option<Token> {
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 {

View File

@ -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<String> {
pub(crate) fn parse_escape(&mut self, identifier_start: bool) -> SassResult<String> {
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
}
}
}
}

View File

@ -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<Option<String>> {
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<String> {
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<Option<String>> {
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<Spanned<Value>> {
let vec = peek_until_closing_curly_brace(self.toks)?;
self.toks.advance_cursor();

View File

@ -235,16 +235,16 @@ impl<'a> Parser<'a> {
lower: String,
) -> SassResult<Spanned<IntermediateValue>> {
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<Spanned<IntermediateValue>> {
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<Spanned<Value>> {
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::<BigInt>().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<Spanned<IntermediateValue>> {
@ -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());

View File

@ -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<String> {
// 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::<String>())
}
fn expect_identifier(&mut self, s: &str) -> SassResult<()> {
let mut ident = self.parser.parse_identifier_no_interpolation(false)?.node;
ident.make_ascii_lowercase();

View File

@ -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 {

View File

@ -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"
);

View File

@ -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,