2020-04-20 14:35:16 -04:00
|
|
|
use std::iter::Iterator;
|
|
|
|
|
|
|
|
use codemap::{Span, Spanned};
|
|
|
|
|
|
|
|
use peekmore::PeekMoreIterator;
|
|
|
|
|
|
|
|
use crate::common::QuoteKind;
|
|
|
|
use crate::error::SassResult;
|
|
|
|
use crate::selector::Selector;
|
|
|
|
use crate::value::Value;
|
|
|
|
use crate::{Scope, Token};
|
|
|
|
|
2020-04-20 14:53:52 -04:00
|
|
|
use super::{as_hex, hex_char_for, is_name, is_name_start, parse_interpolation};
|
2020-04-20 14:35:16 -04:00
|
|
|
|
2020-04-26 00:55:38 -04:00
|
|
|
pub(crate) fn is_ident(s: &str) -> bool {
|
|
|
|
let mut chars = s.chars().peekable();
|
|
|
|
match chars.next() {
|
|
|
|
Some(c) if is_name_start(c) && !c.is_numeric() => {}
|
|
|
|
Some(..) | None => return false,
|
|
|
|
}
|
|
|
|
while let Some(c) = chars.next() {
|
|
|
|
if c == '\\' {
|
|
|
|
for _ in 0..6 {
|
|
|
|
let next = match chars.next() {
|
|
|
|
Some(t) => t,
|
|
|
|
None => return true,
|
|
|
|
};
|
|
|
|
if !next.is_ascii_hexdigit() {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
match chars.peek() {
|
|
|
|
Some(c) if c.is_whitespace() => {
|
|
|
|
chars.next();
|
|
|
|
}
|
|
|
|
_ => {}
|
|
|
|
};
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if !is_name(c) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
true
|
|
|
|
}
|
|
|
|
|
2020-04-20 14:57:41 -04:00
|
|
|
fn ident_body_no_interpolation<I: Iterator<Item = Token>>(
|
2020-04-20 14:35:16 -04:00
|
|
|
toks: &mut PeekMoreIterator<I>,
|
|
|
|
unit: bool,
|
|
|
|
mut span: Span,
|
|
|
|
) -> SassResult<Spanned<String>> {
|
|
|
|
let mut text = String::new();
|
|
|
|
while let Some(tok) = toks.peek() {
|
|
|
|
span = span.merge(tok.pos());
|
|
|
|
if unit && tok.kind == '-' {
|
|
|
|
// Disallow `-` followed by a dot or a digit digit in units.
|
|
|
|
let second = match toks.peek_forward(1) {
|
2020-04-21 18:22:26 -04:00
|
|
|
Some(v) => *v,
|
2020-04-20 14:35:16 -04:00
|
|
|
None => break,
|
|
|
|
};
|
|
|
|
|
|
|
|
toks.peek_backward(1).unwrap();
|
|
|
|
|
|
|
|
if second.kind == '.' || second.kind.is_ascii_digit() {
|
|
|
|
break;
|
|
|
|
}
|
2020-04-25 20:19:50 -04:00
|
|
|
|
2020-04-20 14:35:16 -04:00
|
|
|
toks.next();
|
|
|
|
text.push('-');
|
|
|
|
} else if is_name(tok.kind) {
|
|
|
|
text.push(toks.next().unwrap().kind);
|
|
|
|
} else if tok.kind == '\\' {
|
|
|
|
toks.next();
|
|
|
|
text.push_str(&escape(toks, false)?);
|
|
|
|
} else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(Spanned { node: text, span })
|
|
|
|
}
|
|
|
|
|
|
|
|
fn interpolated_ident_body<I: Iterator<Item = Token>>(
|
|
|
|
toks: &mut PeekMoreIterator<I>,
|
|
|
|
scope: &Scope,
|
|
|
|
super_selector: &Selector,
|
|
|
|
mut span: Span,
|
2020-05-02 11:47:49 -04:00
|
|
|
buf: &mut String,
|
|
|
|
) -> SassResult<Span> {
|
2020-04-20 14:35:16 -04:00
|
|
|
while let Some(tok) = toks.peek() {
|
2020-05-02 11:47:49 -04:00
|
|
|
match tok.kind {
|
|
|
|
'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '\u{80}'..=std::char::MAX => {
|
|
|
|
span = span.merge(tok.pos());
|
|
|
|
buf.push(toks.next().unwrap().kind);
|
2020-04-20 14:35:16 -04:00
|
|
|
}
|
2020-05-02 11:47:49 -04:00
|
|
|
'\\' => {
|
|
|
|
toks.next();
|
|
|
|
buf.push_str(&escape(toks, false)?);
|
|
|
|
}
|
|
|
|
'#' => {
|
|
|
|
toks.next();
|
|
|
|
// TODO: peekmore
|
|
|
|
let next = toks.next().unwrap();
|
|
|
|
if next.kind == '{' {
|
|
|
|
// TODO: if ident, interpolate literally
|
|
|
|
let interpolation = parse_interpolation(toks, scope, super_selector)?;
|
|
|
|
buf.push_str(&interpolation.node.to_css_string(interpolation.span)?);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_ => break,
|
2020-04-20 14:35:16 -04:00
|
|
|
}
|
|
|
|
}
|
2020-05-02 11:47:49 -04:00
|
|
|
Ok(span)
|
2020-04-20 14:35:16 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
fn escape<I: Iterator<Item = Token>>(
|
|
|
|
toks: &mut PeekMoreIterator<I>,
|
|
|
|
identifier_start: bool,
|
|
|
|
) -> SassResult<String> {
|
|
|
|
let mut value = 0;
|
|
|
|
let first = match toks.peek() {
|
|
|
|
Some(t) => t,
|
|
|
|
None => return Ok(String::new()),
|
|
|
|
};
|
2020-05-16 16:22:33 -04:00
|
|
|
let mut span = first.pos();
|
2020-04-20 14:35:16 -04:00
|
|
|
if first.kind == '\n' {
|
2020-05-16 16:22:33 -04:00
|
|
|
return Err(("Expected escape sequence.", span).into());
|
2020-04-20 14:35:16 -04:00
|
|
|
} else if first.kind.is_ascii_hexdigit() {
|
|
|
|
for _ in 0..6 {
|
|
|
|
let next = match toks.peek() {
|
|
|
|
Some(t) => t,
|
|
|
|
None => break,
|
|
|
|
};
|
|
|
|
if !next.kind.is_ascii_hexdigit() {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
value *= 16;
|
2020-05-16 16:22:33 -04:00
|
|
|
span = span.merge(next.pos());
|
2020-04-23 21:30:25 -04:00
|
|
|
value += as_hex(toks.next().unwrap().kind)
|
2020-04-20 14:35:16 -04:00
|
|
|
}
|
|
|
|
if toks.peek().is_some() && toks.peek().unwrap().kind.is_whitespace() {
|
|
|
|
toks.next();
|
|
|
|
}
|
|
|
|
} else {
|
2020-05-16 16:22:33 -04:00
|
|
|
let next = toks.next().unwrap();
|
|
|
|
span = span.merge(next.pos());
|
|
|
|
value = next.kind as u32;
|
2020-04-20 14:35:16 -04:00
|
|
|
}
|
|
|
|
|
2020-05-16 16:22:33 -04:00
|
|
|
let c = std::char::from_u32(value).ok_or(("Invalid escape sequence.", span))?;
|
2020-04-20 14:35:16 -04:00
|
|
|
if (identifier_start && is_name_start(c) && !c.is_digit(10))
|
|
|
|
|| (!identifier_start && is_name(c))
|
|
|
|
{
|
|
|
|
Ok(c.to_string())
|
|
|
|
} else if value <= 0x1F || value == 0x7F || (identifier_start && c.is_digit(10)) {
|
|
|
|
let mut buf = String::with_capacity(4);
|
|
|
|
buf.push('\\');
|
|
|
|
if value > 0xF {
|
|
|
|
buf.push(hex_char_for(value >> 4));
|
|
|
|
}
|
|
|
|
buf.push(hex_char_for(value & 0xF));
|
|
|
|
buf.push(' ');
|
|
|
|
Ok(buf)
|
|
|
|
} else {
|
|
|
|
Ok(format!("\\{}", c))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn eat_ident<I: Iterator<Item = Token>>(
|
|
|
|
toks: &mut PeekMoreIterator<I>,
|
|
|
|
scope: &Scope,
|
|
|
|
super_selector: &Selector,
|
2020-05-17 00:35:07 -04:00
|
|
|
span_before: Span,
|
2020-04-20 14:35:16 -04:00
|
|
|
) -> SassResult<Spanned<String>> {
|
2020-05-17 00:35:07 -04:00
|
|
|
let mut span = toks
|
|
|
|
.peek()
|
|
|
|
.ok_or(("Expected identifier.", span_before))?
|
|
|
|
.pos();
|
2020-04-20 14:35:16 -04:00
|
|
|
let mut text = String::new();
|
|
|
|
if toks.peek().unwrap().kind == '-' {
|
|
|
|
toks.next();
|
|
|
|
text.push('-');
|
2020-05-21 12:06:42 -04:00
|
|
|
if toks.peek().is_none() {
|
|
|
|
return Ok(Spanned { node: text, span });
|
|
|
|
}
|
2020-04-20 14:35:16 -04:00
|
|
|
if toks.peek().unwrap().kind == '-' {
|
|
|
|
toks.next();
|
|
|
|
text.push('-');
|
2020-05-02 11:47:49 -04:00
|
|
|
let body_span = interpolated_ident_body(toks, scope, super_selector, span, &mut text)?;
|
|
|
|
span = span.merge(body_span);
|
2020-04-20 14:35:16 -04:00
|
|
|
return Ok(Spanned { node: text, span });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let Token { kind: first, pos } = match toks.peek() {
|
|
|
|
Some(v) => *v,
|
|
|
|
None => return Err(("Expected identifier.", span).into()),
|
|
|
|
};
|
|
|
|
|
|
|
|
if is_name_start(first) {
|
|
|
|
text.push(toks.next().unwrap().kind);
|
|
|
|
} else if first == '\\' {
|
|
|
|
toks.next();
|
|
|
|
text.push_str(&escape(toks, true)?);
|
|
|
|
// TODO: peekmore
|
|
|
|
// (first == '#' && scanner.peekChar(1) == $lbrace)
|
|
|
|
} else if first == '#' {
|
|
|
|
toks.next();
|
|
|
|
if toks.peek().is_none() {
|
|
|
|
return Err(("Expected identifier.", pos).into());
|
|
|
|
}
|
|
|
|
let Token { kind, pos } = toks.peek().unwrap();
|
|
|
|
if kind == &'{' {
|
|
|
|
toks.next();
|
|
|
|
text.push_str(
|
|
|
|
&match parse_interpolation(toks, scope, super_selector)?.node {
|
2020-05-22 14:20:31 -04:00
|
|
|
Value::Ident(s, ..) => s,
|
2020-05-05 11:08:34 -04:00
|
|
|
v => v.to_css_string(span)?.into(),
|
2020-04-20 14:35:16 -04:00
|
|
|
},
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return Err(("Expected identifier.", *pos).into());
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return Err(("Expected identifier.", pos).into());
|
|
|
|
}
|
|
|
|
|
2020-05-02 11:47:49 -04:00
|
|
|
let body_span = interpolated_ident_body(toks, scope, super_selector, pos, &mut text)?;
|
|
|
|
span = span.merge(body_span);
|
2020-04-20 14:35:16 -04:00
|
|
|
Ok(Spanned { node: text, span })
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn eat_ident_no_interpolation<I: Iterator<Item = Token>>(
|
|
|
|
toks: &mut PeekMoreIterator<I>,
|
|
|
|
unit: bool,
|
|
|
|
) -> SassResult<Spanned<String>> {
|
|
|
|
let mut span = toks.peek().unwrap().pos();
|
|
|
|
let mut text = String::new();
|
|
|
|
if toks.peek().unwrap().kind == '-' {
|
|
|
|
toks.next();
|
|
|
|
text.push('-');
|
2020-05-21 12:06:42 -04:00
|
|
|
if toks.peek().is_none() {
|
|
|
|
return Ok(Spanned { node: text, span });
|
|
|
|
}
|
2020-04-20 14:35:16 -04:00
|
|
|
if toks.peek().unwrap().kind == '-' {
|
|
|
|
toks.next();
|
|
|
|
text.push('-');
|
2020-04-20 14:57:41 -04:00
|
|
|
text.push_str(&ident_body_no_interpolation(toks, unit, span)?.node);
|
2020-04-20 14:35:16 -04:00
|
|
|
return Ok(Spanned { node: text, span });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let first = match toks.peek() {
|
|
|
|
Some(v) => v,
|
|
|
|
None => return Err(("Expected identifier.", span).into()),
|
|
|
|
};
|
|
|
|
|
|
|
|
if is_name_start(first.kind) {
|
|
|
|
text.push(toks.next().unwrap().kind);
|
|
|
|
} else if first.kind == '\\' {
|
|
|
|
toks.next();
|
|
|
|
text.push_str(&escape(toks, true)?);
|
|
|
|
} else {
|
|
|
|
return Err(("Expected identifier.", first.pos()).into());
|
|
|
|
}
|
|
|
|
|
2020-04-20 14:57:41 -04:00
|
|
|
let body = ident_body_no_interpolation(toks, unit, span)?;
|
2020-04-20 14:35:16 -04:00
|
|
|
span = span.merge(body.span);
|
|
|
|
text.push_str(&body.node);
|
|
|
|
Ok(Spanned { node: text, span })
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn parse_quoted_string<I: Iterator<Item = Token>>(
|
|
|
|
toks: &mut PeekMoreIterator<I>,
|
|
|
|
scope: &Scope,
|
|
|
|
q: char,
|
|
|
|
super_selector: &Selector,
|
|
|
|
) -> SassResult<Spanned<Value>> {
|
|
|
|
let mut s = String::new();
|
|
|
|
let mut span = if let Some(tok) = toks.peek() {
|
|
|
|
tok.pos()
|
|
|
|
} else {
|
|
|
|
todo!()
|
|
|
|
};
|
|
|
|
while let Some(tok) = toks.next() {
|
|
|
|
span = span.merge(tok.pos());
|
|
|
|
match tok.kind {
|
|
|
|
'"' if q == '"' => break,
|
|
|
|
'\'' if q == '\'' => break,
|
|
|
|
'#' => {
|
|
|
|
if toks.peek().unwrap().kind == '{' {
|
|
|
|
toks.next();
|
|
|
|
let interpolation = parse_interpolation(toks, scope, super_selector)?;
|
|
|
|
s.push_str(&match interpolation.node {
|
2020-05-22 14:20:31 -04:00
|
|
|
Value::Ident(s, ..) => s,
|
2020-05-05 11:08:34 -04:00
|
|
|
v => v.to_css_string(interpolation.span)?.into(),
|
2020-04-20 14:35:16 -04:00
|
|
|
});
|
|
|
|
continue;
|
|
|
|
} else {
|
|
|
|
s.push('#');
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
'\n' => return Err(("Expected \".", tok.pos()).into()),
|
|
|
|
'\\' => {
|
|
|
|
let first = match toks.peek() {
|
|
|
|
Some(c) => c,
|
|
|
|
None => {
|
|
|
|
s.push('\u{FFFD}');
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
if first.kind == '\n' {
|
2020-05-16 17:20:53 -04:00
|
|
|
toks.next();
|
|
|
|
continue;
|
2020-04-20 14:35:16 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if first.kind.is_ascii_hexdigit() {
|
|
|
|
let mut value = 0;
|
|
|
|
for _ in 0..6 {
|
|
|
|
let next = match toks.peek() {
|
|
|
|
Some(c) => c,
|
|
|
|
None => break,
|
|
|
|
};
|
|
|
|
if !next.kind.is_ascii_hexdigit() {
|
|
|
|
break;
|
|
|
|
}
|
2020-04-23 21:30:25 -04:00
|
|
|
value = (value << 4) + as_hex(toks.next().unwrap().kind);
|
2020-04-20 14:35:16 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if toks.peek().is_some() && toks.peek().unwrap().kind.is_ascii_whitespace() {
|
|
|
|
toks.next();
|
|
|
|
}
|
|
|
|
|
2020-04-21 18:22:26 -04:00
|
|
|
if value == 0 || (value >= 0xD800 && value <= 0xDFFF) || value >= 0x0010_FFFF {
|
2020-04-20 14:35:16 -04:00
|
|
|
s.push('\u{FFFD}');
|
|
|
|
} else {
|
|
|
|
s.push(std::char::from_u32(value).unwrap());
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
s.push(toks.next().unwrap().kind);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_ => s.push(tok.kind),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(Spanned {
|
2020-05-22 14:20:31 -04:00
|
|
|
node: Value::Ident(s, QuoteKind::Quoted),
|
2020-04-20 14:35:16 -04:00
|
|
|
span,
|
|
|
|
})
|
|
|
|
}
|