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;
|
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> {
|
pub fn peek_next(&mut self) -> Option<Token> {
|
||||||
self.amt_peeked += 1;
|
self.amt_peeked += 1;
|
||||||
|
|
||||||
@ -41,7 +37,7 @@ impl Lexer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn peek_previous(&mut self) -> Option<Token> {
|
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> {
|
pub fn peek_forward(&mut self, n: usize) -> Option<Token> {
|
||||||
@ -50,6 +46,11 @@ impl Lexer {
|
|||||||
self.peek()
|
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> {
|
pub fn peek_backward(&mut self, n: usize) -> Option<Token> {
|
||||||
self.amt_peeked = self.amt_peeked.checked_sub(n)?;
|
self.amt_peeked = self.amt_peeked.checked_sub(n)?;
|
||||||
|
|
||||||
@ -60,6 +61,16 @@ impl Lexer {
|
|||||||
self.cursor += self.amt_peeked;
|
self.cursor += self.amt_peeked;
|
||||||
self.amt_peeked = 0;
|
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 {
|
impl Iterator for Lexer {
|
||||||
|
@ -36,7 +36,7 @@ impl<'a> Parser<'a> {
|
|||||||
text.push(self.toks.next().unwrap().kind);
|
text.push(self.toks.next().unwrap().kind);
|
||||||
} else if tok.kind == '\\' {
|
} else if tok.kind == '\\' {
|
||||||
self.toks.next();
|
self.toks.next();
|
||||||
text.push_str(&self.escape(false)?);
|
text.push_str(&self.parse_escape(false)?);
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -56,7 +56,7 @@ impl<'a> Parser<'a> {
|
|||||||
}
|
}
|
||||||
'\\' => {
|
'\\' => {
|
||||||
self.toks.next();
|
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) {
|
if let Some(Token { kind: '{', .. }) = self.toks.peek_forward(1) {
|
||||||
@ -76,7 +76,7 @@ impl<'a> Parser<'a> {
|
|||||||
Ok(())
|
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 mut value = 0;
|
||||||
let first = match self.toks.peek() {
|
let first = match self.toks.peek() {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
@ -172,7 +172,7 @@ impl<'a> Parser<'a> {
|
|||||||
}
|
}
|
||||||
'\\' => {
|
'\\' => {
|
||||||
self.toks.next();
|
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: '{', .. })) => {
|
'#' if matches!(self.toks.peek_forward(1), Some(Token { kind: '{', .. })) => {
|
||||||
self.toks.next();
|
self.toks.next();
|
||||||
@ -228,7 +228,7 @@ impl<'a> Parser<'a> {
|
|||||||
if is_name_start(first.kind) {
|
if is_name_start(first.kind) {
|
||||||
text.push(first.kind);
|
text.push(first.kind);
|
||||||
} else if first.kind == '\\' {
|
} else if first.kind == '\\' {
|
||||||
text.push_str(&self.escape(true)?);
|
text.push_str(&self.parse_escape(true)?);
|
||||||
} else {
|
} else {
|
||||||
return Err(("Expected identifier.", first.pos).into());
|
return Err(("Expected identifier.", first.pos).into());
|
||||||
}
|
}
|
||||||
@ -325,4 +325,32 @@ impl<'a> Parser<'a> {
|
|||||||
}
|
}
|
||||||
Err((format!("Expected {}.", q), span).into())
|
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::{
|
use crate::{
|
||||||
error::SassResult,
|
error::SassResult,
|
||||||
utils::{
|
utils::{
|
||||||
as_hex, hex_char_for, is_name, peek_ident_no_interpolation, peek_until_closing_curly_brace,
|
as_hex, hex_char_for, is_name, peek_until_closing_curly_brace, peek_whitespace,
|
||||||
peek_whitespace,
|
IsWhitespace,
|
||||||
},
|
},
|
||||||
value::Value,
|
value::Value,
|
||||||
Token,
|
Token,
|
||||||
@ -144,28 +144,23 @@ impl<'a> Parser<'a> {
|
|||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
peek_whitespace(self.toks);
|
|
||||||
|
self.whitespace();
|
||||||
|
|
||||||
while let Some(tok) = self.toks.peek() {
|
while let Some(tok) = self.toks.peek() {
|
||||||
let kind = tok.kind;
|
let kind = tok.kind;
|
||||||
match kind {
|
match kind {
|
||||||
'+' | '-' | '0'..='9' => {
|
'+' | '-' | '0'..='9' => {
|
||||||
self.toks.advance_cursor();
|
let number = self.parse_dimension(&|_| false)?;
|
||||||
if let Some(number) = self.peek_number()? {
|
buf.push_str(&number.node.to_css_string(number.span)?);
|
||||||
buf.push(kind);
|
|
||||||
buf.push_str(&number);
|
|
||||||
} else {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
'#' => {
|
'#' => {
|
||||||
self.toks.advance_cursor();
|
self.toks.next();
|
||||||
if let Some(Token { kind: '{', .. }) = self.toks.peek() {
|
if let Some(Token { kind: '{', .. }) = self.toks.peek() {
|
||||||
self.toks.advance_cursor();
|
self.toks.next();
|
||||||
let interpolation = self.peek_interpolation()?;
|
let interpolation = self.parse_interpolation_as_string()?;
|
||||||
match interpolation.node {
|
|
||||||
Value::String(ref s, ..) => buf.push_str(s),
|
buf.push_str(&interpolation);
|
||||||
v => buf.push_str(v.to_css_string(interpolation.span)?.borrow()),
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
@ -192,7 +187,7 @@ impl<'a> Parser<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
'(' => {
|
'(' => {
|
||||||
self.toks.advance_cursor();
|
self.toks.next();
|
||||||
buf.push('(');
|
buf.push('(');
|
||||||
if let Some(val) = self.try_parse_min_max(fn_name, false)? {
|
if let Some(val) = self.try_parse_min_max(fn_name, false)? {
|
||||||
buf.push_str(&val);
|
buf.push_str(&val);
|
||||||
@ -201,10 +196,10 @@ impl<'a> Parser<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
'm' | 'M' => {
|
'm' | 'M' => {
|
||||||
self.toks.advance_cursor();
|
self.toks.next();
|
||||||
let inner_fn_name = match self.toks.peek() {
|
let inner_fn_name = match self.toks.peek() {
|
||||||
Some(Token { kind: 'i', .. }) | Some(Token { kind: 'I', .. }) => {
|
Some(Token { kind: 'i', .. }) | Some(Token { kind: 'I', .. }) => {
|
||||||
self.toks.advance_cursor();
|
self.toks.next();
|
||||||
if !matches!(
|
if !matches!(
|
||||||
self.toks.peek(),
|
self.toks.peek(),
|
||||||
Some(Token { kind: 'n', .. }) | Some(Token { kind: 'N', .. })
|
Some(Token { kind: 'n', .. }) | Some(Token { kind: 'N', .. })
|
||||||
@ -215,7 +210,7 @@ impl<'a> Parser<'a> {
|
|||||||
"min"
|
"min"
|
||||||
}
|
}
|
||||||
Some(Token { kind: 'a', .. }) | Some(Token { kind: 'A', .. }) => {
|
Some(Token { kind: 'a', .. }) | Some(Token { kind: 'A', .. }) => {
|
||||||
self.toks.advance_cursor();
|
self.toks.next();
|
||||||
if !matches!(
|
if !matches!(
|
||||||
self.toks.peek(),
|
self.toks.peek(),
|
||||||
Some(Token { kind: 'x', .. }) | Some(Token { kind: 'X', .. })
|
Some(Token { kind: 'x', .. }) | Some(Token { kind: 'X', .. })
|
||||||
@ -228,13 +223,13 @@ impl<'a> Parser<'a> {
|
|||||||
_ => return Ok(None),
|
_ => return Ok(None),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.toks.advance_cursor();
|
self.toks.next();
|
||||||
|
|
||||||
if !matches!(self.toks.peek(), Some(Token { kind: '(', .. })) {
|
if !matches!(self.toks.peek(), Some(Token { kind: '(', .. })) {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.toks.advance_cursor();
|
self.toks.next();
|
||||||
|
|
||||||
if let Some(val) = self.try_parse_min_max(inner_fn_name, true)? {
|
if let Some(val) = self.try_parse_min_max(inner_fn_name, true)? {
|
||||||
buf.push_str(&val);
|
buf.push_str(&val);
|
||||||
@ -245,7 +240,7 @@ impl<'a> Parser<'a> {
|
|||||||
_ => return Ok(None),
|
_ => return Ok(None),
|
||||||
}
|
}
|
||||||
|
|
||||||
peek_whitespace(self.toks);
|
self.whitespace();
|
||||||
|
|
||||||
let next = match self.toks.peek() {
|
let next = match self.toks.peek() {
|
||||||
Some(tok) => tok,
|
Some(tok) => tok,
|
||||||
@ -254,104 +249,218 @@ impl<'a> Parser<'a> {
|
|||||||
|
|
||||||
match next.kind {
|
match next.kind {
|
||||||
')' => {
|
')' => {
|
||||||
self.toks.advance_cursor();
|
self.toks.next();
|
||||||
buf.push(')');
|
buf.push(')');
|
||||||
return Ok(Some(buf));
|
return Ok(Some(buf));
|
||||||
}
|
}
|
||||||
'+' | '-' | '*' | '/' => {
|
'+' | '-' | '*' | '/' => {
|
||||||
|
self.toks.next();
|
||||||
buf.push(' ');
|
buf.push(' ');
|
||||||
buf.push(next.kind);
|
buf.push(next.kind);
|
||||||
buf.push(' ');
|
buf.push(' ');
|
||||||
self.toks.advance_cursor();
|
|
||||||
}
|
}
|
||||||
',' => {
|
',' => {
|
||||||
if !allow_comma {
|
if !allow_comma {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
self.toks.advance_cursor();
|
self.toks.next();
|
||||||
buf.push(',');
|
buf.push(',');
|
||||||
buf.push(' ');
|
buf.push(' ');
|
||||||
}
|
}
|
||||||
_ => return Ok(None),
|
_ => return Ok(None),
|
||||||
}
|
}
|
||||||
|
|
||||||
peek_whitespace(self.toks);
|
self.whitespace();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Some(buf))
|
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>> {
|
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();
|
ident.make_ascii_lowercase();
|
||||||
|
|
||||||
if ident != fn_name {
|
if ident != fn_name {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !matches!(self.toks.peek(), Some(Token { kind: '(', .. })) {
|
if !matches!(self.toks.peek(), Some(Token { kind: '(', .. })) {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
self.toks.advance_cursor();
|
|
||||||
|
self.toks.next();
|
||||||
ident.push('(');
|
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
|
/// Methods required to do arbitrary lookahead
|
||||||
impl<'a> Parser<'a> {
|
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>> {
|
fn peek_interpolation(&mut self) -> SassResult<Spanned<Value>> {
|
||||||
let vec = peek_until_closing_curly_brace(self.toks)?;
|
let vec = peek_until_closing_curly_brace(self.toks)?;
|
||||||
self.toks.advance_cursor();
|
self.toks.advance_cursor();
|
||||||
|
@ -235,16 +235,16 @@ impl<'a> Parser<'a> {
|
|||||||
lower: String,
|
lower: String,
|
||||||
) -> SassResult<Spanned<IntermediateValue>> {
|
) -> SassResult<Spanned<IntermediateValue>> {
|
||||||
if lower == "min" || lower == "max" {
|
if lower == "min" || lower == "max" {
|
||||||
|
let start = self.toks.cursor();
|
||||||
match self.try_parse_min_max(&lower, true)? {
|
match self.try_parse_min_max(&lower, true)? {
|
||||||
Some(val) => {
|
Some(val) => {
|
||||||
self.toks.truncate_iterator_to_cursor();
|
|
||||||
return Ok(IntermediateValue::Value(HigherIntermediateValue::Literal(
|
return Ok(IntermediateValue::Value(HigherIntermediateValue::Literal(
|
||||||
Value::String(val, QuoteKind::None),
|
Value::String(val, QuoteKind::None),
|
||||||
))
|
))
|
||||||
.span(self.span_before));
|
.span(self.span_before));
|
||||||
}
|
}
|
||||||
None => {
|
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,
|
&mut self,
|
||||||
predicate: &dyn Fn(&mut Lexer) -> bool,
|
predicate: &dyn Fn(&mut Lexer) -> bool,
|
||||||
) -> SassResult<Spanned<IntermediateValue>> {
|
) -> 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 {
|
let Spanned {
|
||||||
node: val,
|
node: val,
|
||||||
mut span,
|
mut span,
|
||||||
@ -513,28 +522,19 @@ impl<'a> Parser<'a> {
|
|||||||
let n = if val.dec_len == 0 {
|
let n = if val.dec_len == 0 {
|
||||||
if val.num.len() <= 18 && val.times_ten.is_empty() {
|
if val.num.len() <= 18 && val.times_ten.is_empty() {
|
||||||
let n = Rational64::new_raw(parse_i64(&val.num), 1);
|
let n = Rational64::new_raw(parse_i64(&val.num), 1);
|
||||||
return Ok(IntermediateValue::Value(HigherIntermediateValue::Literal(
|
return Ok(Value::Dimension(Some(Number::new_small(n)), unit, false).span(span));
|
||||||
Value::Dimension(Some(Number::new_small(n)), unit, false),
|
|
||||||
))
|
|
||||||
.span(span));
|
|
||||||
}
|
}
|
||||||
BigRational::new_raw(val.num.parse::<BigInt>().unwrap(), BigInt::one())
|
BigRational::new_raw(val.num.parse::<BigInt>().unwrap(), BigInt::one())
|
||||||
} else {
|
} else {
|
||||||
if val.num.len() <= 18 && val.times_ten.is_empty() {
|
if val.num.len() <= 18 && val.times_ten.is_empty() {
|
||||||
let n = Rational64::new(parse_i64(&val.num), pow(10, val.dec_len));
|
let n = Rational64::new(parse_i64(&val.num), pow(10, val.dec_len));
|
||||||
return Ok(IntermediateValue::Value(HigherIntermediateValue::Literal(
|
return Ok(Value::Dimension(Some(Number::new_small(n)), unit, false).span(span));
|
||||||
Value::Dimension(Some(Number::new_small(n)), unit, false),
|
|
||||||
))
|
|
||||||
.span(span));
|
|
||||||
}
|
}
|
||||||
BigRational::new(val.num.parse().unwrap(), pow(BigInt::from(10), val.dec_len))
|
BigRational::new(val.num.parse().unwrap(), pow(BigInt::from(10), val.dec_len))
|
||||||
};
|
};
|
||||||
|
|
||||||
if val.times_ten.is_empty() {
|
if val.times_ten.is_empty() {
|
||||||
return Ok(IntermediateValue::Value(HigherIntermediateValue::Literal(
|
return Ok(Value::Dimension(Some(Number::new_big(n)), unit, false).span(span));
|
||||||
Value::Dimension(Some(Number::new_big(n)), unit, false),
|
|
||||||
))
|
|
||||||
.span(span));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let times_ten = pow(
|
let times_ten = pow(
|
||||||
@ -552,14 +552,7 @@ impl<'a> Parser<'a> {
|
|||||||
BigRational::new(BigInt::one(), times_ten)
|
BigRational::new(BigInt::one(), times_ten)
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(
|
Ok(Value::Dimension(Some(Number::new_big(n * times_ten)), unit, false).span(span))
|
||||||
IntermediateValue::Value(HigherIntermediateValue::Literal(Value::Dimension(
|
|
||||||
Some(Number::new_big(n * times_ten)),
|
|
||||||
unit,
|
|
||||||
false,
|
|
||||||
)))
|
|
||||||
.span(span),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_paren(&mut self) -> SassResult<Spanned<IntermediateValue>> {
|
fn parse_paren(&mut self) -> SassResult<Spanned<IntermediateValue>> {
|
||||||
@ -807,7 +800,7 @@ impl<'a> Parser<'a> {
|
|||||||
}
|
}
|
||||||
return Some(self.parse_ident_value(predicate));
|
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();
|
self.toks.next();
|
||||||
return Some(self.parse_paren());
|
return Some(self.parse_paren());
|
||||||
|
@ -1,12 +1,6 @@
|
|||||||
use codemap::Span;
|
use codemap::Span;
|
||||||
|
|
||||||
use crate::{
|
use crate::{common::unvendor, error::SassResult, parse::Parser, utils::is_name, Token};
|
||||||
common::unvendor,
|
|
||||||
error::SassResult,
|
|
||||||
parse::Parser,
|
|
||||||
utils::{is_name, is_name_start, read_until_closing_paren},
|
|
||||||
Token,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
Attribute, Combinator, ComplexSelector, ComplexSelectorComponent, CompoundSelector, Namespace,
|
Attribute, Combinator, ComplexSelector, ComplexSelectorComponent, CompoundSelector, Namespace,
|
||||||
@ -170,7 +164,7 @@ impl<'a, 'b> SelectorParser<'a, 'b> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(..) => {
|
Some(..) => {
|
||||||
if !self.looking_at_identifier() {
|
if !self.parser.looking_at_identifier() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
components.push(ComplexSelectorComponent::Compound(
|
components.push(ComplexSelectorComponent::Compound(
|
||||||
@ -208,34 +202,6 @@ impl<'a, 'b> SelectorParser<'a, 'b> {
|
|||||||
Ok(CompoundSelector { components })
|
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 {
|
fn looking_at_identifier_body(&mut self) -> bool {
|
||||||
matches!(self.parser.toks.peek(), Some(t) if is_name(t.kind) || t.kind == '\\')
|
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) {
|
if SELECTOR_PSEUDO_ELEMENTS.contains(&unvendored) {
|
||||||
selector = Some(Box::new(self.parse_selector_list()?));
|
selector = Some(Box::new(self.parse_selector_list()?));
|
||||||
self.parser.whitespace();
|
self.parser.whitespace();
|
||||||
self.parser.expect_char(')')?;
|
|
||||||
} else {
|
} 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) {
|
} else if SELECTOR_PSEUDO_CLASSES.contains(&unvendored) {
|
||||||
selector = Some(Box::new(self.parse_selector_list()?));
|
selector = Some(Box::new(self.parse_selector_list()?));
|
||||||
self.parser.whitespace();
|
self.parser.whitespace();
|
||||||
@ -349,11 +320,14 @@ impl<'a, 'b> SelectorParser<'a, 'b> {
|
|||||||
argument = Some(this_arg.into_boxed_str());
|
argument = Some(this_arg.into_boxed_str());
|
||||||
} else {
|
} else {
|
||||||
argument = Some(
|
argument = Some(
|
||||||
self.declaration_value()?
|
self.parser
|
||||||
|
.declaration_value(true, false, true)?
|
||||||
.trim_end()
|
.trim_end()
|
||||||
.to_owned()
|
.to_owned()
|
||||||
.into_boxed_str(),
|
.into_boxed_str(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
self.parser.expect_char(')')?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(SimpleSelector::Pseudo(Pseudo {
|
Ok(SimpleSelector::Pseudo(Pseudo {
|
||||||
@ -527,16 +501,6 @@ impl<'a, 'b> SelectorParser<'a, 'b> {
|
|||||||
Ok(buf)
|
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<()> {
|
fn expect_identifier(&mut self, s: &str) -> SassResult<()> {
|
||||||
let mut ident = self.parser.parse_identifier_no_interpolation(false)?.node;
|
let mut ident = self.parser.parse_identifier_no_interpolation(false)?.node;
|
||||||
ident.make_ascii_lowercase();
|
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"
|
".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!(
|
error!(
|
||||||
extend_optional_keyword_not_complete,
|
extend_optional_keyword_not_complete,
|
||||||
"a {
|
"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",
|
||||||
"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",
|
"#{inspect(&)} {\n color: &;\n}\n",
|
||||||
"null {\n color: null;\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!(
|
test!(
|
||||||
#[ignore = "we do not yet have a good way of consuming a string without converting \\a to a newline"]
|
#[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,
|
silent_comment_in_quoted_attribute_value,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user