use std::rc::Rc;
use crate::ast::{
BinOp, CallArg, Callable, ConfigEntry, Conjunction, CssCustomItem, CssCustomValue, CustomDecl,
Declaration, Expr, ForwardMember, IfBranch, IfClause, IfCond, ImportArg, ImportModifier, MediaFeature,
MediaInParens, MediaQuery, MediaQueryList, Param, ParamList, PropertySet, Rule, SrcLines, Stmt,
Stylesheet, SupportsCondition, SupportsValue, TplPiece, UnOp, VarDecl,
};
use crate::error::Error;
use crate::scanner::{Mark, Pos, Scanner};
use crate::value::{named_color, Color, ListSep};
mod at_rules;
mod control_flow;
mod statements;
mod value;
enum NextKind {
Rule,
Declaration,
}
#[derive(Clone, Copy, PartialEq)]
enum CommentMode {
Keep,
Strip,
StripTopLevel,
UnknownPrelude,
DeclName,
}
enum MessageKind {
Warn,
Debug,
Error,
}
fn trim_prelude(pieces: Vec<TplPiece>) -> Vec<TplPiece> {
let mut pieces = pieces;
if let Some(TplPiece::Lit(first)) = pieces.first_mut() {
let trimmed = first.trim_start().to_string();
*first = trimmed;
if first.is_empty() {
pieces.remove(0);
}
}
if let Some(TplPiece::Lit(last)) = pieces.last_mut() {
let trimmed = last.trim_end().to_string();
*last = trimmed;
if last.is_empty() {
pieces.pop();
}
}
pieces
}
fn media_ident_is(pieces: &[TplPiece], kw: &str) -> bool {
match media_ident_plain(pieces) {
Some(s) => s.eq_ignore_ascii_case(kw),
None => false,
}
}
fn media_ident_plain(pieces: &[TplPiece]) -> Option<&str> {
match pieces {
[TplPiece::Lit(s)] => Some(s),
[] => Some(""),
_ => None,
}
}
fn tpl_plain(pieces: &[TplPiece]) -> Option<String> {
let mut s = String::new();
for p in pieces {
match p {
TplPiece::Lit(t) => s.push_str(t),
TplPiece::Interp(_) => return None,
}
}
Some(s)
}
fn tpl_single_interp(pieces: Vec<TplPiece>) -> Option<Expr> {
if pieces.len() == 1 {
if let Some(TplPiece::Interp(e)) = pieces.into_iter().next() {
return Some(e);
}
}
None
}
fn expr_is_custom_property(name: &Expr) -> bool {
match name {
Expr::Ident(pieces) => match pieces.first() {
Some(TplPiece::Lit(s)) => s.starts_with("--"),
_ => false,
},
_ => false,
}
}
fn range_ops_compatible(op1: &str, op2: &str) -> bool {
let dir = |op: &str| match op {
"<" | "<=" => Some(true),
">" | ">=" => Some(false),
_ => None,
};
match (dir(op1), dir(op2)) {
(Some(a), Some(b)) => a == b,
_ => false,
}
}
struct Parser {
sc: Scanner,
calc_depth: u32,
pending_unicode_split: bool,
block_depth: u32,
collect_interp_spans: bool,
interp_spans: Vec<(u32, u32, u32)>,
seen_non_module_stmt: bool,
plain_css: bool,
}
pub(crate) fn parse(src: &str) -> Result<Stylesheet, Error> {
parse_inner(src, false)
}
pub(crate) fn parse_plain_css(src: &str) -> Result<Stylesheet, Error> {
parse_inner(src, true)
}
fn parse_inner(src: &str, plain_css: bool) -> Result<Stylesheet, Error> {
let mut p = Parser {
sc: Scanner::new(src),
calc_depth: 0,
pending_unicode_split: false,
block_depth: 0,
collect_interp_spans: false,
interp_spans: Vec::new(),
seen_non_module_stmt: false,
plain_css,
};
let stmts = p.parse_statements(true)?;
Ok(Stylesheet { stmts })
}
fn is_ident_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '-' || c == '_' || (c as u32) >= 0x80
}
fn reject_at_rules_in(stmts: &[Stmt]) -> Result<(), Error> {
for s in stmts {
match s {
Stmt::AtRule { .. } | Stmt::InterpAtRule { .. } => {
return Err(Error::unpositioned("This at-rule is not allowed here."));
}
Stmt::If(branches) => {
for b in branches {
reject_at_rules_in(&b.body)?;
}
}
Stmt::For { body, .. } | Stmt::Each { body, .. } | Stmt::While { body, .. } => {
reject_at_rules_in(body)?;
}
_ => {}
}
}
Ok(())
}
fn is_sass_only_function(name: &str) -> bool {
matches!(
name,
"index" | "nth" | "set-nth" | "join" | "append" | "zip" | "list-separator"
| "is-bracketed" | "length"
| "map-get" | "map-merge" | "map-remove" | "map-keys" | "map-values" | "map-has-key"
| "type-of" | "unit" | "unitless" | "comparable" | "inspect" | "keywords"
| "feature-exists" | "variable-exists" | "global-variable-exists" | "function-exists"
| "mixin-exists" | "content-exists" | "get-function" | "call" | "get-mixin"
| "str-length" | "str-insert" | "str-index" | "str-slice" | "to-upper-case"
| "to-lower-case" | "unique-id"
| "mix" | "adjust-hue" | "lighten" | "darken" | "desaturate" | "opacify"
| "transparentize" | "fade-in" | "fade-out" | "scale-color" | "adjust-color"
| "change-color" | "ie-hex-str"
| "selector-nest" | "selector-append" | "selector-replace" | "selector-unify"
| "is-superselector" | "simple-selectors" | "selector-parse" | "selector-extend"
)
}
fn is_private_member(name: &str) -> bool {
name.starts_with('-') || name.starts_with('_')
}
fn stmt_allowed_before_use(stmt: &Stmt) -> bool {
match stmt {
Stmt::VarDecl(_) | Stmt::Comment(..) | Stmt::Use { .. } | Stmt::Forward { .. } => true,
Stmt::AtRule { name, .. } => name.eq_ignore_ascii_case("charset"),
_ => false,
}
}
fn is_name_start_codepoint(c: char) -> bool {
c.is_ascii_alphabetic() || c == '_' || (c as u32) >= 0x80
}
fn is_name_codepoint(c: char) -> bool {
is_name_start_codepoint(c) || c.is_ascii_digit() || c == '-'
}
fn push_ident_escape(out: &mut String, c: char, identifier_start: bool) {
let cp = c as u32;
let literal_ok = if identifier_start {
is_name_start_codepoint(c)
} else {
is_name_codepoint(c)
};
if literal_ok {
out.push(c);
} else if cp <= 0x1F || cp == 0x7F || (identifier_start && c.is_ascii_digit()) {
out.push('\\');
out.push_str(&format!("{cp:x}"));
out.push(' ');
} else {
out.push('\\');
out.push(c);
}
}
fn special_function_name(name: &str) -> Option<String> {
let lower = name.to_ascii_lowercase();
let unprefixed = strip_vendor_prefix(&lower);
match unprefixed {
"calc" | "element" | "expression" => Some(lower),
"type" if unprefixed == lower => Some(lower),
_ => None,
}
}
fn strip_vendor_prefix(lower: &str) -> &str {
let bytes = lower.as_bytes();
if bytes.first() != Some(&b'-') {
return lower;
}
if let Some(rel) = lower[1..].find('-') {
if rel >= 1 {
return &lower[1 + rel + 1..];
}
}
lower
}
fn is_url_function(name: &str) -> bool {
let lower = name.to_ascii_lowercase();
strip_vendor_prefix(&lower) == "url"
}
fn is_progid_name(name: &str) -> bool {
let lower = name.to_ascii_lowercase();
strip_vendor_prefix(&lower) == "progid"
}
fn validate_if_cond(cond: &IfCond) -> Result<(bool, bool), Error> {
let (sass, multi) = match cond {
IfCond::Sass(_) => (true, false),
IfCond::Raw { multi, .. } => (false, *multi),
IfCond::Not(inner) => validate_if_cond(inner)?,
IfCond::Paren(inner) => {
let (s, _) = validate_if_cond(inner)?;
(s, false)
}
IfCond::And(items) | IfCond::Or(items) => {
let mut sass = false;
let mut multi = false;
for it in items {
let (s, m) = validate_if_cond(it)?;
sass |= s;
multi |= m;
}
(sass, multi)
}
};
if sass && multi {
return Err(Error::unpositioned(
"if() conditions with arbitrary substitutions may not contain sass() expressions.",
));
}
Ok((sass, multi))
}
fn import_url_is_css(pieces: &[TplPiece]) -> bool {
let mut head = "";
if let Some(TplPiece::Lit(s)) = pieces.first() {
head = s.as_str();
}
let mut tail = String::new();
if let Some(TplPiece::Lit(s)) = pieces.last() {
tail = s.clone();
}
tail.ends_with(".css")
|| head.starts_with("http://")
|| head.starts_with("https://")
|| head.starts_with("//")
}
fn push_lit(pieces: &mut Vec<TplPiece>, c: char) {
if let Some(TplPiece::Lit(s)) = pieces.last_mut() {
s.push(c);
} else {
pieces.push(TplPiece::Lit(c.to_string()));
}
}
fn is_slash_operand(expr: &Expr) -> bool {
match expr {
Expr::Number(_, _) => true,
Expr::Calc { .. } => true,
Expr::Div { slash, .. } => *slash,
Expr::Unary {
op: UnOp::Neg,
operand,
} => is_slash_operand(operand),
_ => false,
}
}
fn expr_is_quoted_string(expr: &Expr) -> bool {
matches!(expr, Expr::QuotedString(_))
}
fn unvendor(name: &str) -> &str {
let bytes = name.as_bytes();
if bytes.first() != Some(&b'-') || bytes.get(1) == Some(&b'-') {
return name;
}
for (i, &c) in bytes.iter().enumerate().skip(1) {
if c == b'-' {
return &name[i + 1..];
}
if !c.is_ascii_alphabetic() {
return name;
}
}
name
}
fn is_reserved_function_name(name: &str) -> bool {
matches!(name, "and" | "or" | "not" | "url" | "expression")
|| unvendor(name) == "element"
|| name.eq_ignore_ascii_case("type")
}
fn property_is_literal_custom(property: &[TplPiece]) -> bool {
match property.first() {
Some(TplPiece::Lit(s)) => s.trim_start().starts_with("--"),
_ => false,
}
}
fn value_is_only_comments(span: &[char]) -> bool {
let mut i = 0;
while i < span.len() {
let c = span[i];
if c.is_whitespace() {
i += 1;
} else if c == '/' && span.get(i + 1) == Some(&'*') {
i += 2;
while i + 1 < span.len() && !(span[i] == '*' && span[i + 1] == '/') {
i += 1;
}
i += 2;
} else if c == '/' && span.get(i + 1) == Some(&'/') {
while i < span.len() && span[i] != '\n' {
i += 1;
}
} else {
return false;
}
}
true
}
impl Parser {
fn skip_ws_inline(&mut self) -> bool {
let mut any = false;
loop {
match self.sc.peek() {
Some(c) if c.is_whitespace() => {
self.sc.bump();
any = true;
}
Some('/') if self.sc.peek_at(1) == Some('*') => {
self.sc.bump();
self.sc.bump();
while let Some(c) = self.sc.peek() {
if c == '*' && self.sc.peek_at(1) == Some('/') {
self.sc.bump();
self.sc.bump();
break;
}
self.sc.bump();
}
any = true;
}
Some('/') if self.sc.peek_at(1) == Some('/') && !self.plain_css => {
while let Some(c) = self.sc.peek() {
if c == '\n' {
break;
}
self.sc.bump();
}
any = true;
}
_ => break,
}
}
any
}
fn parse_braced_body(&mut self) -> Result<Vec<Stmt>, Error> {
Ok(self.parse_braced_body_lines()?.0)
}
fn parse_braced_body_lines(&mut self) -> Result<(Vec<Stmt>, SrcLines), Error> {
self.skip_ws_inline();
let brace_line = self.sc.position().line as u32;
if !self.sc.eat('{') {
return Err(Error::at("expected \"{\".", self.sc.position()));
}
self.block_depth += 1;
let body = self.parse_statements(false);
self.block_depth -= 1;
let body = body?;
if !self.sc.eat('}') {
return Err(Error::at("expected \"}\"", self.sc.position()));
}
let lines = SrcLines {
file: 0,
start: brace_line,
end: self.sc.position().line as u32,
col: 0,
start_col: 0,
map_file: 0,
map_line: 0,
};
Ok((body, lines))
}
fn parse_template(&mut self, stops: &[char]) -> Result<Vec<TplPiece>, Error> {
self.parse_template_mode(stops, CommentMode::Keep)
}
fn consume_loud_comment(&mut self) -> String {
let mut s = String::from("/*");
self.sc.bump();
self.sc.bump();
loop {
match self.sc.peek() {
None => break,
Some('*') if self.sc.peek_at(1) == Some('/') => {
self.sc.bump();
self.sc.bump();
s.push_str("*/");
break;
}
Some(c) => {
s.push(c);
self.sc.bump();
}
}
}
s
}
fn consume_silent_comment(&mut self) {
while let Some(c) = self.sc.peek() {
if c == '\n' {
break;
}
self.sc.bump();
}
}
fn reject_plain_css_interp(&self) -> Result<(), Error> {
if self.plain_css {
return Err(Error::at(
"Interpolation isn't allowed in plain CSS.",
self.sc.position(),
));
}
Ok(())
}
fn read_interp(&mut self) -> Result<Expr, Error> {
self.sc.bump(); self.sc.bump(); let e = self.parse_value()?;
self.skip_ws_inline();
if !self.sc.eat('}') {
return Err(Error::at("expected \"}\"", self.sc.position()));
}
Ok(e)
}
fn parse_template_mode(&mut self, stops: &[char], comments: CommentMode) -> Result<Vec<TplPiece>, Error> {
let mut pieces = Vec::new();
let mut lit = String::new();
let mut paren = 0i32;
let mut bracket = 0i32;
let mut glued = false;
while let Some(c) = self.sc.peek() {
if c == '\\' {
lit.push(c);
self.sc.bump();
if let Some(n) = self.sc.bump() {
lit.push(n);
}
continue;
}
if paren == 0 && bracket == 0 && stops.contains(&c) {
break;
}
if c == '/' && comments != CommentMode::Keep {
let top = paren == 0 && bracket == 0;
let strip_here = match comments {
CommentMode::Strip | CommentMode::DeclName => true,
CommentMode::StripTopLevel | CommentMode::UnknownPrelude => top,
CommentMode::Keep => false,
};
if strip_here {
match self.sc.peek_at(1) {
Some('*') => {
let glue_to_name = comments == CommentMode::DeclName
&& !glued
&& lit
.chars()
.last()
.map_or(!pieces.is_empty(), |p| !p.is_whitespace());
if comments == CommentMode::UnknownPrelude || glue_to_name {
let text = self.consume_loud_comment();
lit.push_str(&text);
glued = true;
} else {
let _ = self.consume_loud_comment();
lit.push(' ');
}
continue;
}
Some('/') => {
self.consume_silent_comment();
if comments != CommentMode::UnknownPrelude {
lit.push(' ');
}
continue;
}
_ => {}
}
}
}
if comments == CommentMode::UnknownPrelude && c.is_whitespace() {
let mut run = String::new();
while matches!(self.sc.peek(), Some(w) if w.is_whitespace()) {
if let Some(w) = self.sc.bump() {
run.push(w);
}
}
match run.find('\n') {
Some(nl) => lit.push_str(&run[nl..]),
None => lit.push(' '),
}
continue;
}
match c {
'#' if self.sc.peek_at(1) == Some('{') => {
if self.plain_css {
return Err(Error::at(
"Interpolation isn't allowed in plain CSS.",
self.sc.position(),
));
}
if !lit.is_empty() {
pieces.push(TplPiece::Lit(std::mem::take(&mut lit)));
}
self.sc.bump();
self.sc.bump();
let span_start = self.sc.position();
let collect = std::mem::replace(&mut self.collect_interp_spans, false);
let e = self.parse_value()?;
self.skip_ws_inline();
self.collect_interp_spans = collect;
if collect {
let end = self.sc.position();
self.interp_spans.push((
span_start.line as u32,
span_start.col as u32,
end.col as u32,
));
}
if !self.sc.eat('}') {
return Err(Error::at("expected \"}\"", self.sc.position()));
}
pieces.push(TplPiece::Interp(e));
}
'"' | '\'' => {
lit.push(c);
self.sc.bump();
while let Some(ch) = self.sc.peek() {
if ch == '\\' {
lit.push(ch);
self.sc.bump();
if let Some(n) = self.sc.bump() {
lit.push(n);
}
continue;
}
if ch == '#' && self.sc.peek_at(1) == Some('{') {
if self.plain_css {
return Err(Error::at(
"Interpolation isn't allowed in plain CSS.",
self.sc.position(),
));
}
if !lit.is_empty() {
pieces.push(TplPiece::Lit(std::mem::take(&mut lit)));
}
self.sc.bump();
self.sc.bump();
let e = self.parse_value()?;
self.skip_ws_inline();
if !self.sc.eat('}') {
return Err(Error::at("expected \"}\"", self.sc.position()));
}
pieces.push(TplPiece::Interp(e));
continue;
}
lit.push(ch);
self.sc.bump();
if ch == c {
break;
}
}
}
'(' => {
paren += 1;
lit.push(c);
self.sc.bump();
}
')' => {
paren -= 1;
lit.push(c);
self.sc.bump();
}
'[' => {
bracket += 1;
lit.push(c);
self.sc.bump();
}
']' => {
bracket -= 1;
lit.push(c);
self.sc.bump();
}
_ => {
lit.push(c);
self.sc.bump();
}
}
}
if !lit.is_empty() {
pieces.push(TplPiece::Lit(lit));
}
Ok(pieces)
}
fn consume_escape(&mut self) -> Result<Option<char>, Error> {
let pos = self.sc.position();
self.sc.bump(); match self.sc.peek() {
Some('\n') => {
self.sc.bump();
Ok(None)
}
Some('\r') => {
self.sc.bump();
self.sc.eat('\n'); Ok(None)
}
Some('\u{c}') => {
self.sc.bump();
Ok(None)
}
Some(c) if c.is_ascii_hexdigit() => {
let mut value: u32 = 0;
let mut digits = 0;
while digits < 6 {
match self.sc.peek() {
Some(h) if h.is_ascii_hexdigit() => {
value = value * 16 + h.to_digit(16).unwrap_or(0);
self.sc.bump();
digits += 1;
}
_ => break,
}
}
match self.sc.peek() {
Some(' ' | '\t' | '\n' | '\u{c}') => {
self.sc.bump();
}
Some('\r') => {
self.sc.bump();
self.sc.eat('\n');
}
_ => {}
}
if value > 0x10_FFFF {
return Err(Error::at("Invalid Unicode code point.", pos));
}
match char::from_u32(value) {
Some(ch) => Ok(Some(ch)),
None => Ok(Some('\u{FFFD}')),
}
}
Some(c) => {
self.sc.bump();
Ok(Some(c))
}
None => Ok(Some('\u{FFFD}')),
}
}
fn read_variable_name(&mut self) -> Result<String, Error> {
let name = self.read_ident_name()?;
if name.contains('_') {
Ok(name.replace('_', "-"))
} else {
Ok(name)
}
}
fn read_ident_name(&mut self) -> Result<String, Error> {
let mut s = String::new();
loop {
match self.sc.peek() {
Some(c) if is_ident_char(c) => {
self.sc.bump();
s.push(c);
}
Some('\\') => s.push(self.read_escape_char()?),
_ => break,
}
}
if s.is_empty() {
return Err(Error::at("expected an identifier", self.sc.position()));
}
Ok(s)
}
fn read_namespace_ident(&mut self) -> Result<String, Error> {
if matches!(self.sc.peek(), Some(c) if c.is_ascii_digit()) {
return Err(Error::at("Expected identifier.", self.sc.position()));
}
self.read_ident_name()
}
fn read_escape_char(&mut self) -> Result<char, Error> {
self.sc.bump(); let first = match self.sc.peek() {
None | Some('\n') | Some('\r') => {
return Err(Error::at("Expected escape sequence.", self.sc.position()))
}
Some(c) => c,
};
if first.is_ascii_hexdigit() {
let mut value: u32 = 0;
for _ in 0..6 {
match self.sc.peek() {
Some(c) if c.is_ascii_hexdigit() => {
value = (value << 4) + c.to_digit(16).unwrap();
self.sc.bump();
}
_ => break,
}
}
if matches!(self.sc.peek(), Some(' ' | '\t' | '\n' | '\r' | '\u{c}')) {
self.sc.bump();
}
if (0xD800..=0xDFFF).contains(&value) || value > 0x10FFFF {
return Ok('\u{FFFD}');
}
Ok(char::from_u32(value).unwrap_or('\u{FFFD}'))
} else {
self.sc.bump();
Ok(first)
}
}
}