support special fns inside min and max
This commit is contained in:
parent
6d0eaef9c0
commit
8e08a5de4f
21
src/lexer.rs
21
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<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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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());
|
||||
|
@ -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();
|
||||
|
@ -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 {
|
||||
|
@ -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"
|
||||
);
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user