correctly parse the function filter syntax

This commit is contained in:
Connor Skees 2020-07-28 18:09:23 -04:00
parent ace9757897
commit 6631b8ae0e
11 changed files with 512 additions and 246 deletions

View File

@ -5,15 +5,84 @@ use crate::{
value::Value, 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"));
}
}
fn alpha(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { fn alpha(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; if args.len() <= 1 {
match args.get_err(0, "color")? { match args.get_err(0, "color")? {
Value::Color(c) => Ok(Value::Dimension(c.alpha(), Unit::None, true)), Value::Color(c) => Ok(Value::Dimension(c.alpha(), Unit::None, true)),
v => Err(( Value::String(s, QuoteKind::None) if is_ms_filter(&s) => {
format!("$color: {} is not a color.", v.inspect(args.span())?), Ok(Value::String(format!("alpha({})", s), QuoteKind::None))
args.span(), }
) v => Err((
.into()), 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::<SassResult<Vec<String>>>()?;
Ok(Value::String(
format!("alpha({})", args.join(", "),),
QuoteKind::None,
))
} }
} }

View File

@ -4,12 +4,10 @@ use codemap::{Span, Spanned};
use crate::{ use crate::{
args::{CallArg, CallArgs, FuncArg, FuncArgs}, args::{CallArg, CallArgs, FuncArg, FuncArgs},
common::QuoteKind,
error::SassResult, error::SassResult,
scope::Scope, scope::Scope,
utils::{ utils::{peek_ident_no_interpolation, peek_whitespace_or_comment, read_until_closing_paren},
peek_ident_no_interpolation, peek_whitespace_or_comment, read_until_closing_paren,
read_until_closing_quote, read_until_closing_square_brace,
},
value::Value, value::Value,
Token, Token,
}; };
@ -132,143 +130,226 @@ impl<'a> Parser<'a> {
let mut args = HashMap::new(); let mut args = HashMap::new();
self.whitespace_or_comment(); self.whitespace_or_comment();
let mut name = String::new(); let mut name = String::new();
let mut val: Vec<Token> = Vec::new();
let mut span = self let mut span = self
.toks .toks
.peek() .peek()
.ok_or(("expected \")\".", self.span_before))? .ok_or(("expected \")\".", self.span_before))?
.pos(); .pos();
loop { 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(); self.whitespace_or_comment();
let mut is_splat = false; if matches!(self.toks.peek(), Some(Token { kind: ')', .. })) {
self.toks.next();
while let Some(tok) = self.toks.next() { return Ok(CallArgs(args, span));
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 is_splat { if let Some(Token { kind: '$', pos }) = self.toks.peek() {
let val = self.parse_value_from_vec(mem::take(&mut val), true)?; span = span.merge(*pos);
match val.node { self.toks.advance_cursor();
Value::ArgList(v) => {
for arg in v { let v = peek_ident_no_interpolation(self.toks, false, self.span_before)?;
args.insert(CallArg::Positional(args.len()), Ok(arg));
} peek_whitespace_or_comment(self.toks);
}
Value::List(v, ..) => { if let Some(Token { kind: ':', .. }) = self.toks.peek() {
for arg in v { self.toks.truncate_iterator_to_cursor();
args.insert(CallArg::Positional(args.len()), Ok(arg.span(val.span))); self.toks.next();
} name = v.node;
} } else {
Value::Map(v) => { self.toks.reset_cursor();
// NOTE: we clone the map here because it is used name.clear();
// 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));
}
} }
} else { } else {
args.insert( name.clear();
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),
);
} }
self.whitespace_or_comment(); self.whitespace_or_comment();
if self.toks.peek().is_none() { let value = self.parse_value(true, &|c| match c.peek() {
return Err(("expected \")\".", span).into()); 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()),
} }
} }
} }

View File

@ -22,7 +22,7 @@ impl<'a> Parser<'a> {
let mut found_true = false; let mut found_true = false;
let mut body = Vec::new(); let mut body = Vec::new();
let init_cond = self.parse_value(true, &|_| true)?.node; let init_cond = self.parse_value(true, &|_| false)?.node;
// consume the open curly brace // consume the open curly brace
let span_before = match self.toks.next() { let span_before = match self.toks.next() {
@ -86,7 +86,7 @@ impl<'a> Parser<'a> {
self.throw_away_until_open_curly_brace()?; self.throw_away_until_open_curly_brace()?;
false false
} else { } else {
let v = self.parse_value(true, &|_| true)?.node.is_true(); let v = self.parse_value(true, &|_| false)?.node.is_true();
match self.toks.next() { match self.toks.next() {
Some(Token { kind: '{', .. }) => {} Some(Token { kind: '{', .. }) => {}
Some(..) | None => { Some(..) | None => {
@ -255,7 +255,7 @@ impl<'a> Parser<'a> {
} }
}; };
let to_val = self.parse_value(true, &|_| true)?; let to_val = self.parse_value(true, &|_| false)?;
let to = match to_val.node { let to = match to_val.node {
Value::Dimension(n, ..) => match n.to_integer().to_isize() { Value::Dimension(n, ..) => match n.to_integer().to_isize() {
Some(v) => v, Some(v) => v,

View File

@ -123,7 +123,7 @@ impl<'a> Parser<'a> {
let Spanned { let Spanned {
node: file_name_as_value, node: file_name_as_value,
span, span,
} = self.parse_value(true, &|_| true)?; } = self.parse_value(true, &|_| false)?;
match file_name_as_value { match file_name_as_value {
Value::String(s, QuoteKind::Quoted) => { Value::String(s, QuoteKind::Quoted) => {

View File

@ -143,7 +143,7 @@ impl<'a> Parser<'a> {
let Spanned { let Spanned {
node: message, node: message,
span, span,
} = self.parse_value(false, &|_| true)?; } = self.parse_value(false, &|_| false)?;
return Err(( return Err((
message.inspect(span)?.to_string(), message.inspect(span)?.to_string(),
@ -155,7 +155,7 @@ impl<'a> Parser<'a> {
let Spanned { let Spanned {
node: message, node: message,
span, span,
} = self.parse_value(false, &|_| true)?; } = self.parse_value(false, &|_| false)?;
span.merge(kind_string.span); span.merge(kind_string.span);
if let Some(Token { kind: ';', pos }) = self.toks.peek() { if let Some(Token { kind: ';', pos }) = self.toks.peek() {
kind_string.span.merge(*pos); kind_string.span.merge(*pos);
@ -170,7 +170,7 @@ impl<'a> Parser<'a> {
let Spanned { let Spanned {
node: message, node: message,
span, span,
} = self.parse_value(false, &|_| true)?; } = self.parse_value(false, &|_| false)?;
span.merge(kind_string.span); span.merge(kind_string.span);
if let Some(Token { kind: ';', pos }) = self.toks.peek() { if let Some(Token { kind: ';', pos }) = self.toks.peek() {
kind_string.span.merge(*pos); kind_string.span.merge(*pos);
@ -434,7 +434,7 @@ impl<'a> Parser<'a> {
} }
pub fn parse_interpolation(&mut self) -> SassResult<Spanned<Value>> { pub fn parse_interpolation(&mut self) -> SassResult<Spanned<Value>> {
let val = self.parse_value(true, &|_| true)?; let val = self.parse_value(true, &|_| false)?;
match self.toks.next() { match self.toks.next() {
Some(Token { kind: '}', .. }) => {} Some(Token { kind: '}', .. }) => {}
Some(..) | None => return Err(("expected \"}\".", val.span).into()), Some(..) | None => return Err(("expected \"}\".", val.span).into()),

View File

@ -173,7 +173,7 @@ impl<'a> Parser<'a> {
} }
fn parse_style_value(&mut self) -> SassResult<Spanned<Value>> { fn parse_style_value(&mut self) -> SassResult<Spanned<Value>> {
self.parse_value(false, &|_| true) self.parse_value(false, &|_| false)
} }
pub(super) fn parse_style_group( pub(super) fn parse_style_group(

View File

@ -1,4 +1,4 @@
use std::{iter::Iterator, mem}; use std::{iter::Iterator, mem, vec::IntoIter};
use num_bigint::BigInt; use num_bigint::BigInt;
use num_rational::{BigRational, Rational64}; use num_rational::{BigRational, Rational64};
@ -6,7 +6,7 @@ use num_traits::{pow, One, ToPrimitive};
use codemap::{Span, Spanned}; use codemap::{Span, Spanned};
use peekmore::PeekMore; use peekmore::{PeekMore, PeekMoreIterator};
use crate::{ use crate::{
builtin::GLOBAL_FUNCTIONS, builtin::GLOBAL_FUNCTIONS,
@ -15,8 +15,8 @@ use crate::{
error::SassResult, error::SassResult,
unit::Unit, unit::Unit,
utils::{ utils::{
devour_whitespace, eat_number, read_until_closing_paren, read_until_closing_square_brace, devour_whitespace, eat_whole_number, read_until_closing_paren,
IsWhitespace, read_until_closing_square_brace, IsWhitespace, ParsedNumber,
}, },
value::{Number, SassFunction, SassMap, Value}, value::{Number, SassFunction, SassMap, Value},
Token, Token,
@ -54,16 +54,14 @@ impl IsWhitespace for IntermediateValue {
impl<'a> Parser<'a> { impl<'a> Parser<'a> {
/// Parse a value from a stream of tokens /// Parse a value from a stream of tokens
/// ///
/// This function will cease parsing if the predicate returns false. /// This function will cease parsing if the predicate returns true.
///
/// E.g. A predicate of `|c| c != 'a'` will cease parsing as soon as an
/// identifier beginning with `'a'` is encountered.
pub(crate) fn parse_value( pub(crate) fn parse_value(
&mut self, &mut self,
in_paren: bool, in_paren: bool,
predicate: &dyn Fn(char) -> bool, predicate: &dyn Fn(&mut PeekMoreIterator<IntoIter<Token>>) -> bool,
) -> SassResult<Spanned<Value>> { ) -> SassResult<Spanned<Value>> {
self.whitespace(); self.whitespace();
let span = match self.toks.peek() { let span = match self.toks.peek() {
Some(Token { kind: '}', .. }) Some(Token { kind: '}', .. })
| Some(Token { kind: ';', .. }) | Some(Token { kind: ';', .. })
@ -71,6 +69,11 @@ impl<'a> Parser<'a> {
| None => return Err(("Expected expression.", self.span_before).into()), | None => return Err(("Expected expression.", self.span_before).into()),
Some(Token { pos, .. }) => *pos, Some(Token { pos, .. }) => *pos,
}; };
if predicate(self.toks) {
return Err(("Expected expression.", span).into());
}
let mut last_was_whitespace = false; let mut last_was_whitespace = false;
let mut space_separated = Vec::new(); let mut space_separated = Vec::new();
let mut comma_separated = Vec::new(); let mut comma_separated = Vec::new();
@ -194,9 +197,9 @@ impl<'a> Parser<'a> {
fn parse_value_with_body( fn parse_value_with_body(
&mut self, &mut self,
toks: &mut peekmore::PeekMoreIterator<std::vec::IntoIter<Token>>, toks: &mut PeekMoreIterator<IntoIter<Token>>,
in_paren: bool, in_paren: bool,
predicate: &dyn Fn(char) -> bool, predicate: &dyn Fn(&mut PeekMoreIterator<IntoIter<Token>>) -> bool,
) -> SassResult<Spanned<Value>> { ) -> SassResult<Spanned<Value>> {
Parser { Parser {
toks, toks,
@ -238,7 +241,7 @@ impl<'a> Parser<'a> {
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
} }
.parse_value(in_paren, &|_| true) .parse_value(in_paren, &|_| false)
} }
fn parse_ident_value(&mut self) -> SassResult<Spanned<IntermediateValue>> { fn parse_ident_value(&mut self) -> SassResult<Spanned<IntermediateValue>> {
@ -375,17 +378,87 @@ impl<'a> Parser<'a> {
} }
} }
fn parse_number(
&mut self,
predicate: &dyn Fn(&mut PeekMoreIterator<IntoIter<Token>>) -> bool,
) -> SassResult<Spanned<ParsedNumber>> {
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( fn parse_intermediate_value(
&mut self, &mut self,
predicate: &dyn Fn(char) -> bool, predicate: &dyn Fn(&mut PeekMoreIterator<IntoIter<Token>>) -> bool,
) -> Option<SassResult<Spanned<IntermediateValue>>> { ) -> Option<SassResult<Spanned<IntermediateValue>>> {
if predicate(self.toks) {
return None;
}
let (kind, span) = match self.toks.peek() { let (kind, span) = match self.toks.peek() {
Some(v) => { Some(v) => (v.kind, v.pos()),
if !predicate(v.kind) {
return None;
}
(v.kind, v.pos())
}
None => return None, None => return None,
}; };
@ -411,7 +484,7 @@ impl<'a> Parser<'a> {
let Spanned { let Spanned {
node: val, node: val,
mut span, mut span,
} = match eat_number(self.toks) { } = match self.parse_number(predicate) {
Ok(v) => v, Ok(v) => v,
Err(e) => return Some(Err(e)), Err(e) => return Some(Err(e)),
}; };
@ -763,7 +836,7 @@ impl<'a> Parser<'a> {
struct IntermediateValueIterator<'a, 'b: 'a> { struct IntermediateValueIterator<'a, 'b: 'a> {
parser: &'a mut Parser<'b>, parser: &'a mut Parser<'b>,
peek: Option<SassResult<Spanned<IntermediateValue>>>, peek: Option<SassResult<Spanned<IntermediateValue>>>,
predicate: &'a dyn Fn(char) -> bool, predicate: &'a dyn Fn(&mut PeekMoreIterator<IntoIter<Token>>) -> bool,
} }
impl<'a, 'b: 'a> Iterator for IntermediateValueIterator<'a, 'b> { impl<'a, 'b: 'a> Iterator for IntermediateValueIterator<'a, 'b> {
@ -778,7 +851,10 @@ impl<'a, 'b: 'a> Iterator for IntermediateValueIterator<'a, 'b> {
} }
impl<'a, 'b: 'a> IntermediateValueIterator<'a, 'b> { impl<'a, 'b: 'a> IntermediateValueIterator<'a, 'b> {
pub fn new(parser: &'a mut Parser<'b>, predicate: &'a dyn Fn(char) -> bool) -> Self { pub fn new(
parser: &'a mut Parser<'b>,
predicate: &'a dyn Fn(&mut PeekMoreIterator<IntoIter<Token>>) -> bool,
) -> Self {
Self { Self {
parser, parser,
peek: None, peek: None,
@ -1110,9 +1186,9 @@ impl<'a, 'b: 'a> IntermediateValueIterator<'a, 'b> {
let paren_toks = &mut t.node.into_iter().peekmore(); let paren_toks = &mut t.node.into_iter().peekmore();
let mut map = SassMap::new(); let mut map = SassMap::new();
let key = self let key = self.parser.parse_value_with_body(paren_toks, true, &|c| {
.parser matches!(c.peek(), Some(Token { kind: ':', .. }))
.parse_value_with_body(paren_toks, true, &|c| c != ':')?; })?;
if let Some(Token { kind: ':', .. }) = paren_toks.peek() { if let Some(Token { kind: ':', .. }) = paren_toks.peek() {
paren_toks.next(); paren_toks.next();
@ -1127,9 +1203,9 @@ impl<'a, 'b: 'a> IntermediateValueIterator<'a, 'b> {
}); });
} }
let val = self let val = self.parser.parse_value_with_body(paren_toks, true, &|c| {
.parser matches!(c.peek(), Some(Token { kind: ',', .. }))
.parse_value_with_body(paren_toks, true, &|c| c != ',')?; })?;
if let Some(Token { kind: ',', .. }) = paren_toks.peek() { if let Some(Token { kind: ',', .. }) = paren_toks.peek() {
paren_toks.next(); paren_toks.next();
@ -1149,18 +1225,18 @@ impl<'a, 'b: 'a> IntermediateValueIterator<'a, 'b> {
let mut span = key.span; let mut span = key.span;
loop { loop {
let key = self let key = self.parser.parse_value_with_body(paren_toks, true, &|c| {
.parser matches!(c.peek(), Some(Token { kind: ':', .. }))
.parse_value_with_body(paren_toks, true, &|c| c != ':')?; })?;
if let Some(Token { kind: ':', .. }) = paren_toks.peek() { if let Some(Token { kind: ':', .. }) = paren_toks.peek() {
paren_toks.next(); paren_toks.next();
} }
devour_whitespace(paren_toks); devour_whitespace(paren_toks);
let val = self let val = self.parser.parse_value_with_body(paren_toks, true, &|c| {
.parser matches!(c.peek(), Some(Token { kind: ',', .. }))
.parse_value_with_body(paren_toks, true, &|c| c != ',')?; })?;
if let Some(Token { kind: ',', .. }) = paren_toks.peek() { if let Some(Token { kind: ',', .. }) = paren_toks.peek() {
paren_toks.next(); paren_toks.next();

View File

@ -1,9 +1,8 @@
use std::vec::IntoIter; use std::vec::IntoIter;
use codemap::Spanned;
use peekmore::PeekMoreIterator; use peekmore::PeekMoreIterator;
use crate::{error::SassResult, Token}; use crate::Token;
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct ParsedNumber { pub(crate) struct ParsedNumber {
@ -47,73 +46,6 @@ impl ParsedNumber {
} }
} }
pub(crate) fn eat_number(
toks: &mut PeekMoreIterator<IntoIter<Token>>,
) -> SassResult<Spanned<ParsedNumber>> {
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<IntoIter<Token>>) -> String { pub(crate) fn eat_whole_number(toks: &mut PeekMoreIterator<IntoIter<Token>>) -> String {
let mut buf = String::new(); let mut buf = String::new();
while let Some(c) = toks.peek() { while let Some(c) = toks.peek() {

View File

@ -99,3 +99,72 @@ test!(
}", }",
"a {\n color: red;\n}\n" "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 \")\"."
);

View File

@ -638,3 +638,39 @@ test!(
"a {\n color: hsla(0deg, 100%, 50%);\n}\n", "a {\n color: hsla(0deg, 100%, 50%);\n}\n",
"a {\n color: red;\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."
);

View File

@ -169,7 +169,10 @@ error!(
); );
error!(unclosed_dbl_quote, "@if true \" {}", "Error: Expected \"."); error!(unclosed_dbl_quote, "@if true \" {}", "Error: Expected \".");
error!(unclosed_sgl_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!(nothing_after_div, "@if a/", "Error: Expected expression.");
error!(multiline_error, "@if \"\n\"{}", "Error: Expected \"."); error!(multiline_error, "@if \"\n\"{}", "Error: Expected \".");
error!( error!(