use std::{
cell::Cell,
collections::{BTreeMap, HashSet},
ffi::OsString,
mem,
path::{Path, PathBuf},
sync::Arc,
};
use codemap::{CodeMap, Span, Spanned};
use crate::{
ast::*,
common::{unvendor, Identifier, QuoteKind},
error::SassResult,
lexer::Lexer,
utils::{is_name, is_name_start, is_plain_css_import, opposite_bracket},
ContextFlags, Options, Token,
};
use super::{
value::{Predicate, ValueParser},
BaseParser, DeclarationOrBuffer, ScssParser, VariableDeclOrInterpolation, RESERVED_IDENTIFIERS,
};
pub(crate) trait StylesheetParser<'a>: BaseParser<'a> + Sized {
fn is_plain_css(&mut self) -> bool;
fn is_indented(&mut self) -> bool;
fn options(&self) -> &Options;
fn path(&mut self) -> &'a Path;
fn map(&mut self) -> &mut CodeMap;
fn span_before(&self) -> Span;
fn current_indentation(&self) -> usize;
fn flags(&mut self) -> &ContextFlags;
fn flags_mut(&mut self) -> &mut ContextFlags;
#[allow(clippy::type_complexity)]
const IDENTIFIER_LIKE: Option<fn(&mut Self) -> SassResult<Spanned<AstExpr>>> = None;
fn parse_style_rule_selector(&mut self) -> SassResult<Interpolation> {
self.almost_any_value(false)
}
fn expect_statement_separator(&mut self, _name: Option<&str>) -> SassResult<()> {
self.whitespace_without_comments();
match self.toks().peek() {
Some(Token {
kind: ';' | '}', ..
})
| None => Ok(()),
_ => {
self.expect_char(';')?;
unreachable!();
}
}
}
fn at_end_of_statement(&self) -> bool {
matches!(
self.toks().peek(),
Some(Token {
kind: ';' | '}' | '{',
..
}) | None
)
}
fn looking_at_children(&mut self) -> SassResult<bool> {
Ok(matches!(self.toks().peek(), Some(Token { kind: '{', .. })))
}
fn scan_else(&mut self, _if_indentation: usize) -> SassResult<bool> {
let start = self.toks().cursor();
self.whitespace()?;
if self.scan_char('@') {
if self.scan_identifier("else", true)? {
return Ok(true);
}
if self.scan_identifier("elseif", true)? {
let new_cursor = self.toks().cursor() - 2;
self.toks_mut().set_cursor(new_cursor);
return Ok(true);
}
}
self.toks_mut().set_cursor(start);
Ok(false)
}
fn parse_children(
&mut self,
child: fn(&mut Self) -> SassResult<AstStmt>,
) -> SassResult<Vec<AstStmt>> {
self.expect_char('{')?;
self.whitespace_without_comments();
let mut children = Vec::new();
let mut found_matching_brace = false;
while let Some(tok) = self.toks().peek() {
match tok.kind {
'$' => children.push(AstStmt::VariableDecl(
self.parse_variable_declaration_without_namespace(None, None)?,
)),
'/' => match self.toks().peek_n(1) {
Some(Token { kind: '/', .. }) => {
children.push(self.parse_silent_comment()?);
self.whitespace_without_comments();
}
Some(Token { kind: '*', .. }) => {
children.push(AstStmt::LoudComment(self.parse_loud_comment()?));
self.whitespace_without_comments();
}
_ => children.push(child(self)?),
},
';' => {
self.toks_mut().next();
self.whitespace_without_comments();
}
'}' => {
self.expect_char('}')?;
found_matching_brace = true;
break;
}
_ => children.push(child(self)?),
}
}
if !found_matching_brace {
return Err(("expected \"}\".", self.toks().current_span()).into());
}
Ok(children)
}
fn parse_statements(
&mut self,
statement: fn(&mut Self) -> SassResult<Option<AstStmt>>,
) -> SassResult<Vec<AstStmt>> {
let mut stmts = Vec::new();
self.whitespace_without_comments();
while let Some(tok) = self.toks().peek() {
match tok.kind {
'$' => stmts.push(AstStmt::VariableDecl(
self.parse_variable_declaration_without_namespace(None, None)?,
)),
'/' => match self.toks().peek_n(1) {
Some(Token { kind: '/', .. }) => {
stmts.push(self.parse_silent_comment()?);
self.whitespace_without_comments();
}
Some(Token { kind: '*', .. }) => {
stmts.push(AstStmt::LoudComment(self.parse_loud_comment()?));
self.whitespace_without_comments();
}
_ => {
if let Some(stmt) = statement(self)? {
stmts.push(stmt);
}
}
},
';' => {
self.toks_mut().next();
self.whitespace_without_comments();
}
_ => {
if let Some(stmt) = statement(self)? {
stmts.push(stmt);
}
}
}
}
Ok(stmts)
}
fn __parse(&mut self) -> SassResult<StyleSheet> {
let mut style_sheet = StyleSheet::new(self.is_plain_css(), self.path().to_path_buf());
self.scan_char('\u{feff}');
style_sheet.body = self.parse_statements(|parser| {
if parser.next_matches("@charset") {
parser.expect_char('@')?;
parser.expect_identifier("charset", false)?;
parser.whitespace()?;
parser.parse_string()?;
return Ok(None);
}
Ok(Some(parser.parse_statement()?))
})?;
for (idx, child) in style_sheet.body.iter().enumerate() {
match child {
AstStmt::VariableDecl(_) | AstStmt::LoudComment(_) | AstStmt::SilentComment(_) => {
continue
}
AstStmt::Use(..) => style_sheet.uses.push(idx),
AstStmt::Forward(..) => style_sheet.forwards.push(idx),
_ => break,
}
}
Ok(style_sheet)
}
fn looking_at_expression(&mut self) -> bool {
let character = if let Some(c) = self.toks().peek() {
c
} else {
return false;
};
match character.kind {
'.' => !matches!(self.toks().peek_n(1), Some(Token { kind: '.', .. })),
'!' => match self.toks().peek_n(1) {
Some(Token {
kind: 'i' | 'I', ..
})
| None => true,
Some(Token { kind, .. }) => kind.is_ascii_whitespace(),
},
'(' | '/' | '[' | '\'' | '"' | '#' | '+' | '-' | '\\' | '$' | '&' => true,
c => is_name_start(c) || c.is_ascii_digit(),
}
}
fn parse_argument_declaration(&mut self) -> SassResult<ArgumentDeclaration> {
self.expect_char('(')?;
self.whitespace()?;
let mut arguments = Vec::new();
let mut named = HashSet::new();
let mut rest_argument: Option<Identifier> = None;
while self.toks_mut().next_char_is('$') {
let name_start = self.toks().cursor();
let name = Identifier::from(self.parse_variable_name()?);
let name_span = self.toks_mut().span_from(name_start);
self.whitespace()?;
let mut default_value: Option<AstExpr> = None;
if self.scan_char(':') {
self.whitespace()?;
default_value = Some(self.parse_expression_until_comma(false)?.node);
} else if self.scan_char('.') {
self.expect_char('.')?;
self.expect_char('.')?;
self.whitespace()?;
rest_argument = Some(name);
break;
}
arguments.push(Argument {
name,
default: default_value,
});
if !named.insert(name) {
return Err(("Duplicate argument.", name_span).into());
}
if !self.scan_char(',') {
break;
}
self.whitespace()?;
}
self.expect_char(')')?;
Ok(ArgumentDeclaration {
args: arguments,
rest: rest_argument,
})
}
fn plain_at_rule_name(&mut self) -> SassResult<String> {
self.expect_char('@')?;
let name = self.parse_identifier(false, false)?;
self.whitespace()?;
Ok(name)
}
fn with_children(
&mut self,
child: fn(&mut Self) -> SassResult<AstStmt>,
) -> SassResult<Spanned<Vec<AstStmt>>> {
let start = self.toks().cursor();
let children = self.parse_children(child)?;
let span = self.toks_mut().span_from(start);
self.whitespace_without_comments();
Ok(Spanned {
node: children,
span,
})
}
fn parse_at_root_query(&mut self) -> SassResult<Interpolation> {
let mut buffer = Interpolation::new();
self.expect_char('(')?;
buffer.add_char('(');
self.whitespace()?;
buffer.add_expr(self.parse_expression(None, None, None)?);
if self.scan_char(':') {
self.whitespace()?;
buffer.add_char(':');
buffer.add_char(' ');
buffer.add_expr(self.parse_expression(None, None, None)?);
}
self.expect_char(')')?;
self.whitespace()?;
buffer.add_char(')');
Ok(buffer)
}
fn parse_at_root_rule(&mut self, start: usize) -> SassResult<AstStmt> {
Ok(AstStmt::AtRootRule(if self.toks_mut().next_char_is('(') {
let query_start = self.toks().cursor();
let query = self.parse_at_root_query()?;
let query_span = self.toks_mut().span_from(query_start);
self.whitespace()?;
let children = self.with_children(Self::parse_statement)?.node;
AstAtRootRule {
query: Some(Spanned {
node: query,
span: query_span,
}),
children,
span: self.toks_mut().span_from(start),
}
} else if self.looking_at_children()? {
let children = self.with_children(Self::parse_statement)?.node;
AstAtRootRule {
query: None,
children,
span: self.toks_mut().span_from(start),
}
} else {
let child = self.parse_style_rule(None, None)?;
AstAtRootRule {
query: None,
children: vec![child],
span: self.toks_mut().span_from(start),
}
}))
}
fn parse_content_rule(&mut self, start: usize) -> SassResult<AstStmt> {
if !self.flags().in_mixin() {
return Err((
"@content is only allowed within mixin declarations.",
self.toks_mut().span_from(start),
)
.into());
}
self.whitespace()?;
let args = if self.toks_mut().next_char_is('(') {
self.parse_argument_invocation(true, false)?
} else {
ArgumentInvocation::empty(self.toks().current_span())
};
self.expect_statement_separator(Some("@content rule"))?;
self.flags_mut().set(ContextFlags::FOUND_CONTENT_RULE, true);
Ok(AstStmt::ContentRule(AstContentRule { args }))
}
fn parse_debug_rule(&mut self) -> SassResult<AstStmt> {
let value = self.parse_expression(None, None, None)?;
self.expect_statement_separator(Some("@debug rule"))?;
Ok(AstStmt::Debug(AstDebugRule {
value: value.node,
span: value.span,
}))
}
fn parse_each_rule(
&mut self,
child: fn(&mut Self) -> SassResult<AstStmt>,
) -> SassResult<AstStmt> {
let was_in_control_directive = self.flags().in_control_flow();
self.flags_mut().set(ContextFlags::IN_CONTROL_FLOW, true);
let mut variables = vec![Identifier::from(self.parse_variable_name()?)];
self.whitespace()?;
while self.scan_char(',') {
self.whitespace()?;
variables.push(Identifier::from(self.parse_variable_name()?));
self.whitespace()?;
}
self.expect_identifier("in", false)?;
self.whitespace()?;
let list = self.parse_expression(None, None, None)?.node;
let body = self.with_children(child)?.node;
self.flags_mut()
.set(ContextFlags::IN_CONTROL_FLOW, was_in_control_directive);
Ok(AstStmt::Each(AstEach {
variables,
list,
body,
}))
}
fn parse_disallowed_at_rule(&mut self, start: usize) -> SassResult<AstStmt> {
self.almost_any_value(false)?;
Err((
"This at-rule is not allowed here.",
self.toks_mut().span_from(start),
)
.into())
}
fn parse_error_rule(&mut self) -> SassResult<AstStmt> {
let value = self.parse_expression(None, None, None)?;
self.expect_statement_separator(Some("@error rule"))?;
Ok(AstStmt::ErrorRule(AstErrorRule {
value: value.node,
span: value.span,
}))
}
fn parse_extend_rule(&mut self, start: usize) -> SassResult<AstStmt> {
if !self.flags().in_style_rule()
&& !self.flags().in_mixin()
&& !self.flags().in_content_block()
{
return Err((
"@extend may only be used within style rules.",
self.toks_mut().span_from(start),
)
.into());
}
let value = self.almost_any_value(false)?;
let is_optional = self.scan_char('!');
if is_optional {
self.expect_identifier("optional", false)?;
}
self.expect_statement_separator(Some("@extend rule"))?;
Ok(AstStmt::Extend(AstExtendRule {
value,
is_optional,
span: self.toks_mut().span_from(start),
}))
}
fn parse_for_rule(
&mut self,
child: fn(&mut Self) -> SassResult<AstStmt>,
) -> SassResult<AstStmt> {
let was_in_control_directive = self.flags().in_control_flow();
self.flags_mut().set(ContextFlags::IN_CONTROL_FLOW, true);
let var_start = self.toks().cursor();
let variable = Spanned {
node: Identifier::from(self.parse_variable_name()?),
span: self.toks_mut().span_from(var_start),
};
self.whitespace()?;
self.expect_identifier("from", false)?;
self.whitespace()?;
let exclusive: Cell<Option<bool>> = Cell::new(None);
let from = self.parse_expression(
Some(&|parser| {
if !parser.looking_at_identifier() {
return Ok(false);
}
Ok(if parser.scan_identifier("to", false)? {
exclusive.set(Some(true));
true
} else if parser.scan_identifier("through", false)? {
exclusive.set(Some(false));
true
} else {
false
})
}),
None,
None,
)?;
let is_exclusive = match exclusive.get() {
Some(b) => b,
None => {
return Err((
"Expected \"to\" or \"through\".",
self.toks().current_span(),
)
.into())
}
};
self.whitespace()?;
let to = self.parse_expression(None, None, None)?;
let body = self.with_children(child)?.node;
self.flags_mut()
.set(ContextFlags::IN_CONTROL_FLOW, was_in_control_directive);
Ok(AstStmt::For(AstFor {
variable,
from,
to,
is_exclusive,
body,
}))
}
fn parse_function_rule(&mut self, start: usize) -> SassResult<AstStmt> {
let name_start = self.toks().cursor();
let name = self.parse_identifier(true, false)?;
let name_span = self.toks_mut().span_from(name_start);
self.whitespace()?;
let arguments = self.parse_argument_declaration()?;
if self.flags().in_mixin() || self.flags().in_content_block() {
return Err((
"Mixins may not contain function declarations.",
self.toks_mut().span_from(start),
)
.into());
} else if self.flags().in_control_flow() {
return Err((
"Functions may not be declared in control directives.",
self.toks_mut().span_from(start),
)
.into());
}
if RESERVED_IDENTIFIERS.contains(&unvendor(&name)) {
return Err(("Invalid function name.", self.toks_mut().span_from(start)).into());
}
self.whitespace()?;
let children = self.with_children(Self::function_child)?.node;
Ok(AstStmt::FunctionDecl(AstFunctionDecl {
name: Spanned {
node: Identifier::from(name),
span: name_span,
},
arguments,
children,
}))
}
fn parse_variable_declaration_with_namespace(&mut self) -> SassResult<AstVariableDecl> {
let start = self.toks().cursor();
let namespace = self.parse_identifier(false, false)?;
let namespace_span = self.toks_mut().span_from(start);
self.expect_char('.')?;
self.parse_variable_declaration_without_namespace(
Some(Spanned {
node: Identifier::from(namespace),
span: namespace_span,
}),
Some(start),
)
}
fn function_child(&mut self) -> SassResult<AstStmt> {
let start = self.toks().cursor();
if !self.toks_mut().next_char_is('@') {
match self.parse_variable_declaration_with_namespace() {
Ok(decl) => return Ok(AstStmt::VariableDecl(decl)),
Err(e) => {
self.toks_mut().set_cursor(start);
let stmt = match self.parse_declaration_or_style_rule() {
Ok(stmt) => stmt,
Err(..) => return Err(e),
};
let (is_style_rule, span) = match stmt {
AstStmt::RuleSet(ruleset) => (true, ruleset.span),
AstStmt::Style(style) => (false, style.span),
_ => unreachable!(),
};
return Err((
format!(
"@function rules may not contain {}.",
if is_style_rule {
"style rules"
} else {
"declarations"
}
),
span,
)
.into());
}
}
}
return match self.plain_at_rule_name()?.as_str() {
"debug" => self.parse_debug_rule(),
"each" => self.parse_each_rule(Self::function_child),
"else" => self.parse_disallowed_at_rule(start),
"error" => self.parse_error_rule(),
"for" => self.parse_for_rule(Self::function_child),
"if" => self.parse_if_rule(Self::function_child),
"return" => self.parse_return_rule(),
"warn" => self.parse_warn_rule(),
"while" => self.parse_while_rule(Self::function_child),
_ => self.parse_disallowed_at_rule(start),
};
}
fn parse_if_rule(
&mut self,
child: fn(&mut Self) -> SassResult<AstStmt>,
) -> SassResult<AstStmt> {
let if_indentation = self.current_indentation();
let was_in_control_directive = self.flags().in_control_flow();
self.flags_mut().set(ContextFlags::IN_CONTROL_FLOW, true);
let condition = self.parse_expression(None, None, None)?.node;
let body = self.parse_children(child)?;
self.whitespace_without_comments();
let mut clauses = vec![AstIfClause { condition, body }];
let mut last_clause: Option<Vec<AstStmt>> = None;
while self.scan_else(if_indentation)? {
self.whitespace()?;
if self.scan_identifier("if", false)? {
self.whitespace()?;
let condition = self.parse_expression(None, None, None)?.node;
let body = self.parse_children(child)?;
clauses.push(AstIfClause { condition, body });
} else {
last_clause = Some(self.parse_children(child)?);
break;
}
}
self.flags_mut()
.set(ContextFlags::IN_CONTROL_FLOW, was_in_control_directive);
self.whitespace_without_comments();
Ok(AstStmt::If(AstIf {
if_clauses: clauses,
else_clause: last_clause,
}))
}
fn try_parse_import_supports_function(&mut self) -> SassResult<Option<AstSupportsCondition>> {
if !self.looking_at_interpolated_identifier() {
return Ok(None);
}
let start = self.toks().cursor();
let name = self.parse_interpolated_identifier()?;
debug_assert!(name.as_plain() != Some("not"));
if !self.scan_char('(') {
self.toks_mut().set_cursor(start);
return Ok(None);
}
let value = self.parse_interpolated_declaration_value(true, true, true)?;
self.expect_char(')')?;
Ok(Some(AstSupportsCondition::Function { name, args: value }))
}
fn parse_import_supports_query(&mut self) -> SassResult<AstSupportsCondition> {
Ok(if self.scan_identifier("not", false)? {
self.whitespace()?;
AstSupportsCondition::Negation(Box::new(self.supports_condition_in_parens()?))
} else if self.toks_mut().next_char_is('(') {
self.parse_supports_condition()?
} else {
match self.try_parse_import_supports_function()? {
Some(function) => function,
None => {
let start = self.toks().cursor();
let name = self.parse_expression(None, None, None)?;
self.expect_char(':')?;
self.supports_declaration_value(name.node, start)?
}
}
})
}
fn try_import_modifiers(&mut self) -> SassResult<Option<Interpolation>> {
if !self.looking_at_interpolated_identifier() && !self.toks_mut().next_char_is('(') {
return Ok(None);
}
let mut buffer = Interpolation::new();
loop {
if self.looking_at_interpolated_identifier() {
if !buffer.is_empty() {
buffer.add_char(' ');
}
let identifier = self.parse_interpolated_identifier()?;
let name = identifier.as_plain().map(str::to_ascii_lowercase);
buffer.add_interpolation(identifier);
if name.as_deref() != Some("and") && self.scan_char('(') {
if name.as_deref() == Some("supports") {
let query = self.parse_import_supports_query()?;
let is_declaration =
matches!(query, AstSupportsCondition::Declaration { .. });
if !is_declaration {
buffer.add_char('(');
}
buffer
.add_expr(AstExpr::Supports(Arc::new(query)).span(self.span_before()));
if !is_declaration {
buffer.add_char(')');
}
} else {
buffer.add_char('(');
buffer.add_interpolation(
self.parse_interpolated_declaration_value(true, true, true)?,
);
buffer.add_char(')');
}
self.expect_char(')')?;
self.whitespace()?;
} else {
self.whitespace()?;
if self.scan_char(',') {
buffer.add_char(',');
buffer.add_char(' ');
buffer.add_interpolation(self.parse_media_query_list()?);
return Ok(Some(buffer));
}
}
} else if self.toks_mut().next_char_is('(') {
if !buffer.is_empty() {
buffer.add_char(' ');
}
buffer.add_interpolation(self.parse_media_query_list()?);
return Ok(Some(buffer));
} else {
return Ok(Some(buffer));
}
}
}
fn try_url_contents(&mut self, name: Option<&str>) -> SassResult<Option<Interpolation>> {
let start = self.toks().cursor();
if !self.scan_char('(') {
return Ok(None);
}
self.whitespace_without_comments();
let mut buffer = Interpolation::new();
buffer.add_string(name.unwrap_or("url").to_owned());
buffer.add_char('(');
while let Some(next) = self.toks().peek() {
match next.kind {
'\\' => buffer.add_string(self.parse_escape(false)?),
'!' | '%' | '&' | '*'..='~' | '\u{80}'..=char::MAX => {
self.toks_mut().next();
buffer.add_char(next.kind);
}
'#' => {
if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) {
let interpolation = self.parse_single_interpolation()?;
buffer.add_interpolation(interpolation);
} else {
self.toks_mut().next();
buffer.add_char(next.kind);
}
}
')' => {
self.toks_mut().next();
buffer.add_char(next.kind);
return Ok(Some(buffer));
}
' ' | '\t' | '\n' | '\r' => {
self.whitespace_without_comments();
if !self.toks_mut().next_char_is(')') {
break;
}
}
_ => break,
}
}
self.toks_mut().set_cursor(start);
Ok(None)
}
fn parse_dynamic_url(&mut self) -> SassResult<AstExpr> {
let start = self.toks().cursor();
self.expect_identifier("url", false)?;
Ok(match self.try_url_contents(None)? {
Some(contents) => AstExpr::String(
StringExpr(contents, QuoteKind::None),
self.toks_mut().span_from(start),
),
None => AstExpr::InterpolatedFunction(Arc::new(InterpolatedFunction {
name: Interpolation::new_plain("url".to_owned()),
arguments: self.parse_argument_invocation(false, false)?,
span: self.toks_mut().span_from(start),
})),
})
}
fn parse_import_argument(&mut self, start: usize) -> SassResult<AstImport> {
if self.toks_mut().next_char_is('u') || self.toks_mut().next_char_is('U') {
let url = self.parse_dynamic_url()?;
self.whitespace()?;
let modifiers = self.try_import_modifiers()?;
return Ok(AstImport::Plain(AstPlainCssImport {
url: Interpolation::new_with_expr(url.span(self.toks_mut().span_from(start))),
modifiers,
span: self.toks_mut().span_from(start),
}));
}
let start = self.toks().cursor();
let url = self.parse_string()?;
let raw_url = self.toks().raw_text(start);
self.whitespace()?;
let modifiers = self.try_import_modifiers()?;
let span = self.toks_mut().span_from(start);
if is_plain_css_import(&url) || modifiers.is_some() {
Ok(AstImport::Plain(AstPlainCssImport {
url: Interpolation::new_plain(raw_url),
modifiers,
span,
}))
} else {
Ok(AstImport::Sass(AstSassImport { url, span }))
}
}
fn parse_import_rule(&mut self, start: usize) -> SassResult<AstStmt> {
let mut imports = Vec::new();
loop {
self.whitespace()?;
let argument = self.parse_import_argument(self.toks().cursor())?;
if (self.flags().in_control_flow() || self.flags().in_mixin()) && argument.is_dynamic()
{
self.parse_disallowed_at_rule(start)?;
}
imports.push(argument);
self.whitespace()?;
if !self.scan_char(',') {
break;
}
}
Ok(AstStmt::ImportRule(AstImportRule { imports }))
}
fn parse_public_identifier(&mut self) -> SassResult<String> {
let start = self.toks().cursor();
let ident = self.parse_identifier(true, false)?;
Self::assert_public(&ident, self.toks_mut().span_from(start))?;
Ok(ident)
}
fn parse_include_rule(&mut self) -> SassResult<AstStmt> {
let mut namespace: Option<Spanned<Identifier>> = None;
let name_start = self.toks().cursor();
let mut name = self.parse_identifier(false, false)?;
if self.scan_char('.') {
let namespace_span = self.toks_mut().span_from(name_start);
namespace = Some(Spanned {
node: Identifier::from(name),
span: namespace_span,
});
name = self.parse_public_identifier()?;
} else {
name = name.replace('_', "-");
}
let name = Identifier::from(name);
let name_span = self.toks_mut().span_from(name_start);
self.whitespace()?;
let args = if self.toks_mut().next_char_is('(') {
self.parse_argument_invocation(true, false)?
} else {
ArgumentInvocation::empty(self.toks().current_span())
};
self.whitespace()?;
let content_args = if self.scan_identifier("using", false)? {
self.whitespace()?;
let args = self.parse_argument_declaration()?;
self.whitespace()?;
Some(args)
} else {
None
};
let mut content_block: Option<AstContentBlock> = None;
if content_args.is_some() || self.looking_at_children()? {
let content_args = content_args.unwrap_or_else(ArgumentDeclaration::empty);
let was_in_content_block = self.flags().in_content_block();
self.flags_mut().set(ContextFlags::IN_CONTENT_BLOCK, true);
let body = self.with_children(Self::parse_statement)?.node;
content_block = Some(AstContentBlock {
args: content_args,
body,
});
self.flags_mut()
.set(ContextFlags::IN_CONTENT_BLOCK, was_in_content_block);
} else {
self.expect_statement_separator(None)?;
}
Ok(AstStmt::Include(AstInclude {
namespace,
name: Spanned {
node: name,
span: name_span,
},
args,
content: content_block,
span: name_span,
}))
}
fn parse_media_rule(&mut self, start: usize) -> SassResult<AstStmt> {
let query_start = self.toks().cursor();
let query = self.parse_media_query_list()?;
let query_span = self.toks_mut().span_from(query_start);
let body = self.with_children(Self::parse_statement)?.node;
Ok(AstStmt::Media(AstMedia {
query,
query_span,
body,
span: self.toks_mut().span_from(start),
}))
}
fn parse_interpolated_string(&mut self) -> SassResult<Spanned<StringExpr>> {
let start = self.toks().cursor();
let quote = match self.toks_mut().next() {
Some(Token {
kind: kind @ ('"' | '\''),
..
}) => kind,
Some(..) | None => unreachable!("Expected string."),
};
let mut buffer = Interpolation::new();
let mut found_match = false;
while let Some(next) = self.toks().peek() {
match next.kind {
c if c == quote => {
self.toks_mut().next();
found_match = true;
break;
}
'\n' => break,
'\\' => {
match self.toks().peek_n(1) {
Some(Token { kind: '\n', .. }) => {
self.toks_mut().next();
self.toks_mut().next();
}
_ => buffer.add_char(self.consume_escaped_char()?),
}
}
'#' => {
if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) {
buffer.add_interpolation(self.parse_single_interpolation()?);
} else {
self.toks_mut().next();
buffer.add_char(next.kind);
}
}
_ => {
buffer.add_char(next.kind);
self.toks_mut().next();
}
}
}
if !found_match {
return Err((
format!("Expected {quote}.", quote = quote),
self.toks().current_span(),
)
.into());
}
Ok(Spanned {
node: StringExpr(buffer, QuoteKind::Quoted),
span: self.toks_mut().span_from(start),
})
}
fn parse_return_rule(&mut self) -> SassResult<AstStmt> {
let value = self.parse_expression(None, None, None)?;
self.expect_statement_separator(None)?;
Ok(AstStmt::Return(AstReturn {
val: value.node,
span: value.span,
}))
}
fn parse_mixin_rule(&mut self, start: usize) -> SassResult<AstStmt> {
let name = Identifier::from(self.parse_identifier(true, false)?);
self.whitespace()?;
let args = if self.toks_mut().next_char_is('(') {
self.parse_argument_declaration()?
} else {
ArgumentDeclaration::empty()
};
if self.flags().in_mixin() || self.flags().in_content_block() {
return Err((
"Mixins may not contain mixin declarations.",
self.toks_mut().span_from(start),
)
.into());
} else if self.flags().in_control_flow() {
return Err((
"Mixins may not be declared in control directives.",
self.toks_mut().span_from(start),
)
.into());
}
self.whitespace()?;
let old_found_content_rule = self.flags().found_content_rule();
self.flags_mut()
.set(ContextFlags::FOUND_CONTENT_RULE, false);
self.flags_mut().set(ContextFlags::IN_MIXIN, true);
let body = self.with_children(Self::parse_statement)?.node;
let has_content = self.flags_mut().found_content_rule();
self.flags_mut()
.set(ContextFlags::FOUND_CONTENT_RULE, old_found_content_rule);
self.flags_mut().set(ContextFlags::IN_MIXIN, false);
Ok(AstStmt::Mixin(AstMixin {
name,
args,
body,
has_content,
}))
}
fn _parse_moz_document_rule(&mut self, _name: Interpolation) -> SassResult<AstStmt> {
todo!("special cased @-moz-document not yet implemented")
}
fn unknown_at_rule(&mut self, name: Interpolation, start: usize) -> SassResult<AstStmt> {
let was_in_unknown_at_rule = self.flags().in_unknown_at_rule();
self.flags_mut().set(ContextFlags::IN_UNKNOWN_AT_RULE, true);
let value: Option<Interpolation> =
if !self.toks_mut().next_char_is('!') && !self.at_end_of_statement() {
Some(self.almost_any_value(false)?)
} else {
None
};
let children = if self.looking_at_children()? {
Some(self.with_children(Self::parse_statement)?.node)
} else {
self.expect_statement_separator(None)?;
None
};
self.flags_mut()
.set(ContextFlags::IN_UNKNOWN_AT_RULE, was_in_unknown_at_rule);
Ok(AstStmt::UnknownAtRule(AstUnknownAtRule {
name,
value,
children,
span: self.toks_mut().span_from(start),
}))
}
fn try_supports_operation(
&mut self,
interpolation: &Interpolation,
_start: usize,
) -> SassResult<Option<AstSupportsCondition>> {
if interpolation.contents.len() != 1 {
return Ok(None);
}
let expression = match interpolation.contents.first() {
Some(InterpolationPart::Expr(e)) => e,
Some(InterpolationPart::String(..)) => return Ok(None),
None => unreachable!(),
};
let before_whitespace = self.toks().cursor();
self.whitespace()?;
let mut operation: Option<AstSupportsCondition> = None;
let mut operator: Option<String> = None;
while self.looking_at_identifier() {
if let Some(operator) = &operator {
self.expect_identifier(operator, false)?;
} else if self.scan_identifier("and", false)? {
operator = Some("and".to_owned());
} else if self.scan_identifier("or", false)? {
operator = Some("or".to_owned());
} else {
self.toks_mut().set_cursor(before_whitespace);
return Ok(None);
}
self.whitespace()?;
let right = self.supports_condition_in_parens()?;
operation = Some(AstSupportsCondition::Operation {
left: Box::new(
operation
.unwrap_or(AstSupportsCondition::Interpolation(expression.clone().node)),
),
operator: operator.clone(),
right: Box::new(right),
});
self.whitespace()?;
}
Ok(operation)
}
fn supports_declaration_value(
&mut self,
name: AstExpr,
start: usize,
) -> SassResult<AstSupportsCondition> {
let value = match &name {
AstExpr::String(StringExpr(text, QuoteKind::None), ..)
if text.initial_plain().starts_with("--") =>
{
let text = self.parse_interpolated_declaration_value(false, false, true)?;
AstExpr::String(
StringExpr(text, QuoteKind::None),
self.toks_mut().span_from(start),
)
}
_ => {
self.whitespace()?;
self.parse_expression(None, None, None)?.node
}
};
Ok(AstSupportsCondition::Declaration { name, value })
}
fn supports_condition_in_parens(&mut self) -> SassResult<AstSupportsCondition> {
let start = self.toks().cursor();
if self.looking_at_interpolated_identifier() {
let identifier = self.parse_interpolated_identifier()?;
let ident_span = self.toks_mut().span_from(start);
if identifier.as_plain().unwrap_or("").to_ascii_lowercase() == "not" {
return Err((r#""not" is not a valid identifier here."#, ident_span).into());
}
if self.scan_char('(') {
let arguments = self.parse_interpolated_declaration_value(true, true, true)?;
self.expect_char(')')?;
return Ok(AstSupportsCondition::Function {
name: identifier,
args: arguments,
});
} else if identifier.contents.len() != 1
|| !matches!(
identifier.contents.first(),
Some(InterpolationPart::Expr(..))
)
{
return Err(("Expected @supports condition.", ident_span).into());
} else {
match identifier.contents.first() {
Some(InterpolationPart::Expr(e)) => {
return Ok(AstSupportsCondition::Interpolation(e.clone().node))
}
_ => unreachable!(),
}
}
}
self.expect_char('(')?;
self.whitespace()?;
if self.scan_identifier("not", false)? {
self.whitespace()?;
let condition = self.supports_condition_in_parens()?;
self.expect_char(')')?;
return Ok(AstSupportsCondition::Negation(Box::new(condition)));
} else if self.toks_mut().next_char_is('(') {
let condition = self.parse_supports_condition()?;
self.expect_char(')')?;
return Ok(condition);
}
let name: AstExpr;
let name_start = self.toks().cursor();
let was_in_parens = self.flags().in_parens();
let expr = self.parse_expression(None, None, None);
let found_colon = self.expect_char(':');
match (expr, found_colon) {
(Ok(val), Ok(..)) => {
name = val.node;
}
(Ok(..), Err(e)) | (Err(e), Ok(..)) | (Err(e), Err(..)) => {
self.toks_mut().set_cursor(name_start);
self.flags_mut().set(ContextFlags::IN_PARENS, was_in_parens);
let identifier = self.parse_interpolated_identifier()?;
if let Some(operation) = self.try_supports_operation(&identifier, name_start)? {
self.expect_char(')')?;
return Ok(operation);
}
let mut contents = Interpolation::new();
contents.add_interpolation(identifier);
contents.add_interpolation(
self.parse_interpolated_declaration_value(true, true, false)?,
);
if self.toks_mut().next_char_is(':') {
return Err(e);
}
self.expect_char(')')?;
return Ok(AstSupportsCondition::Anything { contents });
}
}
let declaration = self.supports_declaration_value(name, start)?;
self.expect_char(')')?;
Ok(declaration)
}
fn parse_supports_condition(&mut self) -> SassResult<AstSupportsCondition> {
if self.scan_identifier("not", false)? {
self.whitespace()?;
return Ok(AstSupportsCondition::Negation(Box::new(
self.supports_condition_in_parens()?,
)));
}
let mut condition = self.supports_condition_in_parens()?;
self.whitespace()?;
let mut operator: Option<String> = None;
while self.looking_at_identifier() {
if let Some(operator) = &operator {
self.expect_identifier(operator, false)?;
} else if self.scan_identifier("or", false)? {
operator = Some("or".to_owned());
} else {
self.expect_identifier("and", false)?;
operator = Some("and".to_owned());
}
self.whitespace()?;
let right = self.supports_condition_in_parens()?;
condition = AstSupportsCondition::Operation {
left: Box::new(condition),
operator: operator.clone(),
right: Box::new(right),
};
self.whitespace()?;
}
Ok(condition)
}
fn parse_supports_rule(&mut self) -> SassResult<AstStmt> {
let condition = self.parse_supports_condition()?;
self.whitespace()?;
let children = self.with_children(Self::parse_statement)?;
Ok(AstStmt::Supports(AstSupportsRule {
condition,
children: children.node,
span: children.span,
}))
}
fn parse_warn_rule(&mut self) -> SassResult<AstStmt> {
let value = self.parse_expression(None, None, None)?;
self.expect_statement_separator(Some("@warn rule"))?;
Ok(AstStmt::Warn(AstWarn {
value: value.node,
span: value.span,
}))
}
fn parse_while_rule(
&mut self,
child: fn(&mut Self) -> SassResult<AstStmt>,
) -> SassResult<AstStmt> {
let was_in_control_directive = self.flags().in_control_flow();
self.flags_mut().set(ContextFlags::IN_CONTROL_FLOW, true);
let condition = self.parse_expression(None, None, None)?.node;
let body = self.with_children(child)?.node;
self.flags_mut()
.set(ContextFlags::IN_CONTROL_FLOW, was_in_control_directive);
Ok(AstStmt::While(AstWhile { condition, body }))
}
fn parse_forward_rule(&mut self, start: usize) -> SassResult<AstStmt> {
let url = PathBuf::from(self.parse_url_string()?);
self.whitespace()?;
let prefix = if self.scan_identifier("as", false)? {
self.whitespace()?;
let prefix = self.parse_identifier(true, false)?;
self.expect_char('*')?;
self.whitespace()?;
Some(prefix)
} else {
None
};
let mut shown_mixins_and_functions: Option<HashSet<Identifier>> = None;
let mut shown_variables: Option<HashSet<Identifier>> = None;
let mut hidden_mixins_and_functions: Option<HashSet<Identifier>> = None;
let mut hidden_variables: Option<HashSet<Identifier>> = None;
if self.scan_identifier("show", false)? {
let members = self.parse_member_list()?;
shown_mixins_and_functions = Some(members.0);
shown_variables = Some(members.1);
} else if self.scan_identifier("hide", false)? {
let members = self.parse_member_list()?;
hidden_mixins_and_functions = Some(members.0);
hidden_variables = Some(members.1);
}
let config = self.parse_configuration(true)?;
self.expect_statement_separator(Some("@forward rule"))?;
let span = self.toks_mut().span_from(start);
if !self.flags().is_use_allowed() {
return Err((
"@forward rules must be written before any other rules.",
span,
)
.into());
}
Ok(AstStmt::Forward(
if let (Some(shown_mixins_and_functions), Some(shown_variables)) =
(shown_mixins_and_functions, shown_variables)
{
AstForwardRule::show(
url,
shown_mixins_and_functions,
shown_variables,
prefix,
config,
span,
)
} else if let (Some(hidden_mixins_and_functions), Some(hidden_variables)) =
(hidden_mixins_and_functions, hidden_variables)
{
AstForwardRule::hide(
url,
hidden_mixins_and_functions,
hidden_variables,
prefix,
config,
span,
)
} else {
AstForwardRule::new(url, prefix, config, span)
},
))
}
fn parse_member_list(&mut self) -> SassResult<(HashSet<Identifier>, HashSet<Identifier>)> {
let mut identifiers = HashSet::new();
let mut variables = HashSet::new();
loop {
self.whitespace()?;
if self.toks_mut().next_char_is('$') {
variables.insert(Identifier::from(self.parse_variable_name()?));
} else {
identifiers.insert(Identifier::from(self.parse_identifier(true, false)?));
}
self.whitespace()?;
if !self.scan_char(',') {
break;
}
}
Ok((identifiers, variables))
}
fn parse_url_string(&mut self) -> SassResult<String> {
self.parse_string()
}
fn use_namespace(
&mut self,
url: &Path,
_start: usize,
url_span: Span,
) -> SassResult<Option<String>> {
if self.scan_identifier("as", false)? {
self.whitespace()?;
return Ok(if self.scan_char('*') {
None
} else {
Some(self.parse_identifier(false, false)?)
});
}
let base_name = url
.file_name()
.map_or_else(OsString::new, ToOwned::to_owned);
let base_name = base_name.to_string_lossy();
let dot = base_name.find('.');
let start = if base_name.starts_with('_') { 1 } else { 0 };
let end = dot.unwrap_or(base_name.len());
let namespace = if url.to_string_lossy().starts_with("sass:") {
return Ok(Some(url.to_string_lossy().into_owned()));
} else {
&base_name[start..end]
};
let mut toks = Lexer::new_from_string(namespace, url_span);
let identifier = if namespace.is_empty() {
Err(("", self.span_before()).into())
} else {
mem::swap(self.toks_mut(), &mut toks);
let ident = self.parse_identifier(false, false);
mem::swap(self.toks_mut(), &mut toks);
ident
};
match (identifier, toks.peek().is_none()) {
(Ok(i), true) => Ok(Some(i)),
_ => {
Err((
format!(
"The default namespace \"{namespace}\" is not a valid Sass identifier.\n\nRecommendation: add an \"as\" clause to define an explicit namespace.",
namespace = namespace
),
self.toks_mut().span_from(start)
).into())
}
}
}
fn parse_configuration(
&mut self,
allow_guarded: bool,
) -> SassResult<Option<Vec<ConfiguredVariable>>> {
if !self.scan_identifier("with", false)? {
return Ok(None);
}
let mut variable_names = HashSet::new();
let mut configuration = Vec::new();
self.whitespace()?;
self.expect_char('(')?;
loop {
self.whitespace()?;
let var_start = self.toks().cursor();
let name = Identifier::from(self.parse_variable_name()?);
let name_span = self.toks_mut().span_from(var_start);
self.whitespace()?;
self.expect_char(':')?;
self.whitespace()?;
let expr = self.parse_expression_until_comma(false)?;
let mut is_guarded = false;
let flag_start = self.toks().cursor();
if allow_guarded && self.scan_char('!') {
let flag = self.parse_identifier(false, false)?;
if flag == "default" {
is_guarded = true;
self.whitespace()?;
} else {
return Err(
("Invalid flag name.", self.toks_mut().span_from(flag_start)).into(),
);
}
}
let span = self.toks_mut().span_from(var_start);
if variable_names.contains(&name) {
return Err(("The same variable may only be configured once.", span).into());
}
variable_names.insert(name);
configuration.push(ConfiguredVariable {
name: Spanned {
node: name,
span: name_span,
},
expr,
is_guarded,
});
if !self.scan_char(',') {
break;
}
self.whitespace()?;
if !self.looking_at_expression() {
break;
}
}
self.expect_char(')')?;
Ok(Some(configuration))
}
fn parse_use_rule(&mut self, start: usize) -> SassResult<AstStmt> {
let url_start = self.toks().cursor();
let url = self.parse_url_string()?;
let url_span = self.toks().span_from(url_start);
self.whitespace()?;
let path = PathBuf::from(url);
let namespace = self.use_namespace(path.as_ref(), start, url_span)?;
self.whitespace()?;
let configuration = self.parse_configuration(false)?;
self.expect_statement_separator(Some("@use rule"))?;
let span = self.toks_mut().span_from(start);
if !self.flags().is_use_allowed() {
return Err((
"@use rules must be written before any other rules.",
self.toks_mut().span_from(start),
)
.into());
}
self.expect_statement_separator(Some("@use rule"))?;
Ok(AstStmt::Use(AstUseRule {
url: path,
namespace,
configuration: configuration.unwrap_or_default(),
span,
}))
}
fn parse_at_rule(
&mut self,
child: fn(&mut Self) -> SassResult<AstStmt>,
) -> SassResult<AstStmt> {
let start = self.toks().cursor();
self.expect_char('@')?;
let name = self.parse_interpolated_identifier()?;
self.whitespace()?;
let was_use_allowed = self.flags().is_use_allowed();
self.flags_mut().set(ContextFlags::IS_USE_ALLOWED, false);
match name.as_plain() {
Some("at-root") => self.parse_at_root_rule(start),
Some("content") => self.parse_content_rule(start),
Some("debug") => self.parse_debug_rule(),
Some("each") => self.parse_each_rule(child),
Some("else") | Some("return") => self.parse_disallowed_at_rule(start),
Some("error") => self.parse_error_rule(),
Some("extend") => self.parse_extend_rule(start),
Some("for") => self.parse_for_rule(child),
Some("forward") => {
self.flags_mut()
.set(ContextFlags::IS_USE_ALLOWED, was_use_allowed);
self.parse_forward_rule(start)
}
Some("function") => self.parse_function_rule(start),
Some("if") => self.parse_if_rule(child),
Some("import") => self.parse_import_rule(start),
Some("include") => self.parse_include_rule(),
Some("media") => self.parse_media_rule(start),
Some("mixin") => self.parse_mixin_rule(start),
Some("supports") => self.parse_supports_rule(),
Some("use") => {
self.flags_mut()
.set(ContextFlags::IS_USE_ALLOWED, was_use_allowed);
self.parse_use_rule(start)
}
Some("warn") => self.parse_warn_rule(),
Some("while") => self.parse_while_rule(child),
Some(..) | None => self.unknown_at_rule(name, start),
}
}
fn parse_statement(&mut self) -> SassResult<AstStmt> {
match self.toks().peek() {
Some(Token { kind: '@', .. }) => self.parse_at_rule(Self::parse_statement),
Some(Token { kind: '+', .. }) => {
if !self.is_indented() {
return self.parse_style_rule(None, None);
}
let start = self.toks().cursor();
self.toks_mut().next();
if !self.looking_at_identifier() {
self.toks_mut().set_cursor(start);
return self.parse_style_rule(None, None);
}
self.flags_mut().set(ContextFlags::IS_USE_ALLOWED, false);
self.parse_include_rule()
}
Some(Token { kind: '=', .. }) => {
if !self.is_indented() {
return self.parse_style_rule(None, None);
}
self.flags_mut().set(ContextFlags::IS_USE_ALLOWED, false);
let start = self.toks().cursor();
self.toks_mut().next();
self.whitespace()?;
self.parse_mixin_rule(start)
}
Some(Token { kind: '}', .. }) => {
Err(("unmatched \"}\".", self.toks().current_span()).into())
}
_ => {
if self.flags().in_style_rule()
|| self.flags().in_unknown_at_rule()
|| self.flags().in_mixin()
|| self.flags().in_content_block()
{
self.parse_declaration_or_style_rule()
} else {
self.parse_variable_declaration_or_style_rule()
}
}
}
}
fn parse_declaration_or_style_rule(&mut self) -> SassResult<AstStmt> {
let start = self.toks().cursor();
if self.is_plain_css() && self.flags().in_style_rule() && !self.flags().in_unknown_at_rule()
{
return self.parse_property_or_variable_declaration(true);
}
if self.is_indented() && self.scan_char('\\') {
return self.parse_style_rule(None, None);
};
match self.parse_declaration_or_buffer()? {
DeclarationOrBuffer::Stmt(s) => Ok(s),
DeclarationOrBuffer::Buffer(existing_buffer) => {
self.parse_style_rule(Some(existing_buffer), Some(start))
}
}
}
fn parse_property_or_variable_declaration(
&mut self,
parse_custom_properties: bool,
) -> SassResult<AstStmt> {
let start = self.toks().cursor();
let name = if matches!(
self.toks().peek(),
Some(Token {
kind: ':' | '*' | '.',
..
})
) || (matches!(self.toks().peek(), Some(Token { kind: '#', .. }))
&& !matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })))
{
let mut name_buffer = Interpolation::new();
name_buffer.add_char(self.toks_mut().next().unwrap().kind);
name_buffer.add_string(self.raw_text(Self::whitespace));
name_buffer.add_interpolation(self.parse_interpolated_identifier()?);
name_buffer
} else if !self.is_plain_css() {
match self.parse_variable_declaration_or_interpolation()? {
VariableDeclOrInterpolation::Interpolation(interpolation) => interpolation,
VariableDeclOrInterpolation::VariableDecl(decl) => {
return Ok(AstStmt::VariableDecl(decl))
}
}
} else {
self.parse_interpolated_identifier()?
};
self.whitespace()?;
self.expect_char(':')?;
if parse_custom_properties && name.initial_plain().starts_with("--") {
let interpolation = self.parse_interpolated_declaration_value(false, false, true)?;
let value_span = self.toks_mut().span_from(start);
let value = AstExpr::String(StringExpr(interpolation, QuoteKind::None), value_span)
.span(value_span);
self.expect_statement_separator(Some("custom property"))?;
return Ok(AstStmt::Style(AstStyle {
name,
value: Some(value),
body: Vec::new(),
span: value_span,
}));
}
self.whitespace()?;
if self.looking_at_children()? {
if self.is_plain_css() {
return Err((
"Nested declarations aren't allowed in plain CSS.",
self.toks().current_span(),
)
.into());
}
if name.initial_plain().starts_with("--") {
return Err((
"Declarations whose names begin with \"--\" may not be nested",
self.toks_mut().span_from(start),
)
.into());
}
let children = self.with_children(Self::parse_declaration_child)?.node;
return Ok(AstStmt::Style(AstStyle {
name,
value: None,
body: children,
span: self.toks_mut().span_from(start),
}));
}
let value = self.parse_expression(None, None, None)?;
if self.looking_at_children()? {
if self.is_plain_css() {
return Err((
"Nested declarations aren't allowed in plain CSS.",
self.toks().current_span(),
)
.into());
}
if name.initial_plain().starts_with("--") && !matches!(value.node, AstExpr::String(..))
{
return Err((
"Declarations whose names begin with \"--\" may not be nested",
self.toks_mut().span_from(start),
)
.into());
}
let children = self.with_children(Self::parse_declaration_child)?.node;
Ok(AstStmt::Style(AstStyle {
name,
value: Some(value),
body: children,
span: self.toks_mut().span_from(start),
}))
} else {
self.expect_statement_separator(None)?;
Ok(AstStmt::Style(AstStyle {
name,
value: Some(value),
body: Vec::new(),
span: self.toks_mut().span_from(start),
}))
}
}
fn parse_single_interpolation(&mut self) -> SassResult<Interpolation> {
self.expect_char('#')?;
self.expect_char('{')?;
self.whitespace()?;
let contents = self.parse_expression(None, None, None)?;
self.expect_char('}')?;
if self.is_plain_css() {
return Err(("Interpolation isn't allowed in plain CSS.", contents.span).into());
}
let mut interpolation = Interpolation::new();
interpolation
.contents
.push(InterpolationPart::Expr(contents));
Ok(interpolation)
}
fn parse_interpolated_identifier_body(&mut self, buffer: &mut Interpolation) -> SassResult<()> {
while let Some(next) = self.toks().peek() {
match next.kind {
'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '\u{80}'..=std::char::MAX => {
buffer.add_char(next.kind);
self.toks_mut().next();
}
'\\' => {
buffer.add_string(self.parse_escape(false)?);
}
'#' if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) => {
buffer.add_interpolation(self.parse_single_interpolation()?);
}
_ => break,
}
}
Ok(())
}
fn parse_interpolated_identifier(&mut self) -> SassResult<Interpolation> {
let mut buffer = Interpolation::new();
if self.scan_char('-') {
buffer.add_char('-');
if self.scan_char('-') {
buffer.add_char('-');
self.parse_interpolated_identifier_body(&mut buffer)?;
return Ok(buffer);
}
}
match self.toks().peek() {
Some(tok) if is_name_start(tok.kind) => {
buffer.add_char(tok.kind);
self.toks_mut().next();
}
Some(Token { kind: '\\', .. }) => {
buffer.add_string(self.parse_escape(true)?);
}
Some(Token { kind: '#', .. })
if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) =>
{
buffer.add_interpolation(self.parse_single_interpolation()?);
}
Some(..) | None => {
return Err(("Expected identifier.", self.toks().current_span()).into())
}
}
self.parse_interpolated_identifier_body(&mut buffer)?;
Ok(buffer)
}
fn looking_at_interpolated_identifier(&mut self) -> bool {
let first = match self.toks().peek() {
Some(Token { kind: '\\', .. }) => return true,
Some(Token { kind: '#', .. }) => {
return matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. }))
}
Some(Token { kind, .. }) if is_name_start(kind) => return true,
Some(tok) => tok,
None => return false,
};
if first.kind != '-' {
return false;
}
match self.toks().peek_n(1) {
Some(Token { kind: '#', .. }) => {
matches!(self.toks().peek_n(2), Some(Token { kind: '{', .. }))
}
Some(Token {
kind: '\\' | '-', ..
}) => true,
Some(Token { kind, .. }) => is_name_start(kind),
None => false,
}
}
fn parse_loud_comment(&mut self) -> SassResult<AstLoudComment> {
let start = self.toks().cursor();
self.expect_char('/')?;
self.expect_char('*')?;
let mut buffer = Interpolation::new_plain("/*".to_owned());
while let Some(tok) = self.toks().peek() {
match tok.kind {
'#' => {
if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) {
buffer.add_interpolation(self.parse_single_interpolation()?);
} else {
self.toks_mut().next();
buffer.add_char(tok.kind);
}
}
'*' => {
self.toks_mut().next();
buffer.add_char(tok.kind);
if self.scan_char('/') {
buffer.add_char('/');
return Ok(AstLoudComment {
text: buffer,
span: self.toks_mut().span_from(start),
});
}
}
'\r' => {
self.toks_mut().next();
if !self.toks_mut().next_char_is('\n') {
buffer.add_char('\n');
}
}
_ => {
buffer.add_char(tok.kind);
self.toks_mut().next();
}
}
}
Err(("expected more input.", self.toks().current_span()).into())
}
fn parse_interpolated_declaration_value(
&mut self,
allow_semicolon: bool,
allow_empty: bool,
allow_colon: bool,
) -> SassResult<Interpolation> {
let mut buffer = Interpolation::new();
let mut brackets = Vec::new();
let mut wrote_newline = false;
while let Some(tok) = self.toks().peek() {
match tok.kind {
'\\' => {
buffer.add_string(self.parse_escape(true)?);
wrote_newline = false;
}
'"' | '\'' => {
buffer.add_interpolation(
self.parse_interpolated_string()?
.node
.as_interpolation(false),
);
wrote_newline = false;
}
'/' => {
if matches!(self.toks().peek_n(1), Some(Token { kind: '*', .. })) {
let comment = self.fallible_raw_text(Self::skip_loud_comment)?;
buffer.add_string(comment);
} else {
self.toks_mut().next();
buffer.add_char(tok.kind);
}
wrote_newline = false;
}
'#' => {
if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) {
buffer.add_interpolation(self.parse_interpolated_identifier()?);
} else {
self.toks_mut().next();
buffer.add_char(tok.kind);
}
wrote_newline = false;
}
' ' | '\t' => {
if wrote_newline
|| !matches!(
self.toks().peek_n(1),
Some(Token {
kind: ' ' | '\r' | '\t' | '\n',
..
})
)
{
self.toks_mut().next();
buffer.add_char(tok.kind);
} else {
self.toks_mut().next();
}
}
'\n' | '\r' => {
if self.is_indented() {
break;
}
if !matches!(
self.toks().peek_n_backwards(1),
Some(Token {
kind: '\r' | '\n',
..
})
) {
buffer.add_char('\n');
}
self.toks_mut().next();
wrote_newline = true;
}
'(' | '{' | '[' => {
self.toks_mut().next();
buffer.add_char(tok.kind);
brackets.push(opposite_bracket(tok.kind));
wrote_newline = false;
}
')' | '}' | ']' => {
if brackets.is_empty() {
break;
}
buffer.add_char(tok.kind);
self.expect_char(brackets.pop().unwrap())?;
wrote_newline = false;
}
';' => {
if !allow_semicolon && brackets.is_empty() {
break;
}
buffer.add_char(tok.kind);
self.toks_mut().next();
wrote_newline = false;
}
':' => {
if !allow_colon && brackets.is_empty() {
break;
}
buffer.add_char(tok.kind);
self.toks_mut().next();
wrote_newline = false;
}
'u' | 'U' => {
let before_url = self.toks().cursor();
if !self.scan_identifier("url", false)? {
buffer.add_char(tok.kind);
self.toks_mut().next();
wrote_newline = false;
continue;
}
match self.try_url_contents(None)? {
Some(contents) => {
buffer.add_interpolation(contents);
}
None => {
self.toks_mut().set_cursor(before_url);
buffer.add_char(tok.kind);
self.toks_mut().next();
}
}
wrote_newline = false;
}
_ => {
if self.looking_at_identifier() {
buffer.add_string(self.parse_identifier(false, false)?);
} else {
buffer.add_char(tok.kind);
self.toks_mut().next();
}
wrote_newline = false;
}
}
}
if let Some(&last) = brackets.last() {
self.expect_char(last)?;
}
if !allow_empty && buffer.contents.is_empty() {
return Err(("Expected token.", self.toks().current_span()).into());
}
Ok(buffer)
}
fn parse_expression_until_comma(
&mut self,
single_equals: bool,
) -> SassResult<Spanned<AstExpr>> {
ValueParser::parse_expression(
self,
Some(&|parser| {
Ok(matches!(
parser.toks().peek(),
Some(Token { kind: ',', .. })
))
}),
false,
single_equals,
)
}
fn parse_argument_invocation(
&mut self,
for_mixin: bool,
allow_empty_second_arg: bool,
) -> SassResult<ArgumentInvocation> {
let start = self.toks().cursor();
self.expect_char('(')?;
self.whitespace()?;
let mut positional = Vec::new();
let mut named = BTreeMap::new();
let mut rest: Option<AstExpr> = None;
let mut keyword_rest: Option<AstExpr> = None;
while self.looking_at_expression() {
let expression = self.parse_expression_until_comma(!for_mixin)?;
self.whitespace()?;
if expression.node.is_variable() && self.scan_char(':') {
let name = match expression.node {
AstExpr::Variable { name, .. } => name,
_ => unreachable!(),
};
self.whitespace()?;
if named.contains_key(&name.node) {
return Err(("Duplicate argument.", name.span).into());
}
named.insert(
name.node,
self.parse_expression_until_comma(!for_mixin)?.node,
);
} else if self.scan_char('.') {
self.expect_char('.')?;
self.expect_char('.')?;
if rest.is_none() {
rest = Some(expression.node);
} else {
keyword_rest = Some(expression.node);
self.whitespace()?;
break;
}
} else if !named.is_empty() {
return Err((
"Positional arguments must come before keyword arguments.",
expression.span,
)
.into());
} else {
positional.push(expression.node);
}
self.whitespace()?;
if !self.scan_char(',') {
break;
}
self.whitespace()?;
if allow_empty_second_arg
&& positional.len() == 1
&& named.is_empty()
&& rest.is_none()
&& matches!(self.toks().peek(), Some(Token { kind: ')', .. }))
{
positional.push(AstExpr::String(
StringExpr(Interpolation::new(), QuoteKind::None),
self.toks().current_span(),
));
break;
}
}
self.expect_char(')')?;
Ok(ArgumentInvocation {
positional,
named,
rest,
keyword_rest,
span: self.toks_mut().span_from(start),
})
}
fn parse_expression(
&mut self,
parse_until: Option<Predicate<'_, Self>>,
inside_bracketed_list: Option<bool>,
single_equals: Option<bool>,
) -> SassResult<Spanned<AstExpr>> {
ValueParser::parse_expression(
self,
parse_until,
inside_bracketed_list.unwrap_or(false),
single_equals.unwrap_or(false),
)
}
fn parse_declaration_or_buffer(&mut self) -> SassResult<DeclarationOrBuffer> {
let start = self.toks().cursor();
let mut name_buffer = Interpolation::new();
let first = self.toks().peek();
let mut starts_with_punctuation = false;
if matches!(
first,
Some(Token {
kind: ':' | '*' | '.',
..
})
) || (matches!(first, Some(Token { kind: '#', .. }))
&& !matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })))
{
starts_with_punctuation = true;
name_buffer.add_char(self.toks_mut().next().unwrap().kind);
name_buffer.add_string(self.raw_text(Self::whitespace));
}
if !self.looking_at_interpolated_identifier() {
return Ok(DeclarationOrBuffer::Buffer(name_buffer));
}
let variable_or_interpolation = if starts_with_punctuation {
VariableDeclOrInterpolation::Interpolation(self.parse_interpolated_identifier()?)
} else {
self.parse_variable_declaration_or_interpolation()?
};
match variable_or_interpolation {
VariableDeclOrInterpolation::Interpolation(int) => name_buffer.add_interpolation(int),
VariableDeclOrInterpolation::VariableDecl(v) => {
return Ok(DeclarationOrBuffer::Stmt(AstStmt::VariableDecl(v)))
}
}
self.flags_mut().set(ContextFlags::IS_USE_ALLOWED, false);
if self.next_matches("/*") {
name_buffer.add_string(self.fallible_raw_text(Self::skip_loud_comment)?);
}
let mut mid_buffer = String::new();
mid_buffer.push_str(&self.raw_text(Self::whitespace));
if !self.scan_char(':') {
if !mid_buffer.is_empty() {
name_buffer.add_char(' ');
}
return Ok(DeclarationOrBuffer::Buffer(name_buffer));
}
mid_buffer.push(':');
if name_buffer.initial_plain().starts_with("--") {
let value_start = self.toks().cursor();
let value = self.parse_interpolated_declaration_value(false, false, true)?;
let value_span = self.toks_mut().span_from(value_start);
self.expect_statement_separator(Some("custom property"))?;
return Ok(DeclarationOrBuffer::Stmt(AstStmt::Style(AstStyle {
name: name_buffer,
value: Some(
AstExpr::String(StringExpr(value, QuoteKind::None), value_span)
.span(value_span),
),
span: self.toks_mut().span_from(start),
body: Vec::new(),
})));
}
if self.scan_char(':') {
name_buffer.add_string(mid_buffer);
name_buffer.add_char(':');
return Ok(DeclarationOrBuffer::Buffer(name_buffer));
} else if self.is_indented() && self.looking_at_interpolated_identifier() {
name_buffer.add_string(mid_buffer);
return Ok(DeclarationOrBuffer::Buffer(name_buffer));
}
let post_colon_whitespace = self.raw_text(Self::whitespace);
if self.looking_at_children()? {
let body = self.with_children(Self::parse_declaration_child)?.node;
return Ok(DeclarationOrBuffer::Stmt(AstStmt::Style(AstStyle {
name: name_buffer,
value: None,
span: self.toks_mut().span_from(start),
body,
})));
}
mid_buffer.push_str(&post_colon_whitespace);
let could_be_selector =
post_colon_whitespace.is_empty() && self.looking_at_interpolated_identifier();
let before_decl = self.toks().cursor();
let mut calculate_value = || {
let value = self.parse_expression(None, None, None)?;
if self.looking_at_children()? {
if could_be_selector {
self.expect_statement_separator(None)?;
}
} else if !self.at_end_of_statement() {
self.expect_statement_separator(None)?;
}
Ok(value)
};
let value = match calculate_value() {
Ok(v) => v,
Err(e) => {
if !could_be_selector {
return Err(e);
}
self.toks_mut().set_cursor(before_decl);
let additional = self.almost_any_value(false)?;
if !self.is_indented() && self.toks_mut().next_char_is(';') {
return Err(e);
}
name_buffer.add_string(mid_buffer);
name_buffer.add_interpolation(additional);
return Ok(DeclarationOrBuffer::Buffer(name_buffer));
}
};
if self.looking_at_children()? {
let body = self.with_children(Self::parse_declaration_child)?.node;
Ok(DeclarationOrBuffer::Stmt(AstStmt::Style(AstStyle {
name: name_buffer,
value: Some(value),
span: self.toks_mut().span_from(start),
body,
})))
} else {
self.expect_statement_separator(None)?;
Ok(DeclarationOrBuffer::Stmt(AstStmt::Style(AstStyle {
name: name_buffer,
value: Some(value),
span: self.toks_mut().span_from(start),
body: Vec::new(),
})))
}
}
fn parse_declaration_child(&mut self) -> SassResult<AstStmt> {
let start = self.toks().cursor();
if self.toks_mut().next_char_is('@') {
self.parse_declaration_at_rule(start)
} else {
self.parse_property_or_variable_declaration(false)
}
}
fn parse_plain_at_rule_name(&mut self) -> SassResult<String> {
self.expect_char('@')?;
let name = self.parse_identifier(false, false)?;
self.whitespace()?;
Ok(name)
}
fn parse_declaration_at_rule(&mut self, start: usize) -> SassResult<AstStmt> {
let name = self.parse_plain_at_rule_name()?;
match name.as_str() {
"content" => self.parse_content_rule(start),
"debug" => self.parse_debug_rule(),
"each" => self.parse_each_rule(Self::parse_declaration_child),
"else" => self.parse_disallowed_at_rule(start),
"error" => self.parse_error_rule(),
"for" => self.parse_for_rule(Self::parse_declaration_child),
"if" => self.parse_if_rule(Self::parse_declaration_child),
"include" => self.parse_include_rule(),
"warn" => self.parse_warn_rule(),
"while" => self.parse_while_rule(Self::parse_declaration_child),
_ => self.parse_disallowed_at_rule(start),
}
}
fn parse_variable_declaration_or_style_rule(&mut self) -> SassResult<AstStmt> {
let start = self.toks().cursor();
if self.is_plain_css() {
return self.parse_style_rule(None, None);
}
if self.is_indented() && self.scan_char('\\') {
return self.parse_style_rule(None, None);
};
if !self.looking_at_identifier() {
return self.parse_style_rule(None, None);
}
match self.parse_variable_declaration_or_interpolation()? {
VariableDeclOrInterpolation::VariableDecl(var) => Ok(AstStmt::VariableDecl(var)),
VariableDeclOrInterpolation::Interpolation(int) => {
self.parse_style_rule(Some(int), Some(start))
}
}
}
fn parse_style_rule(
&mut self,
existing_buffer: Option<Interpolation>,
start: Option<usize>,
) -> SassResult<AstStmt> {
let start = start.unwrap_or_else(|| self.toks().cursor());
self.flags_mut().set(ContextFlags::IS_USE_ALLOWED, false);
let mut interpolation = self.parse_style_rule_selector()?;
if let Some(mut existing_buffer) = existing_buffer {
existing_buffer.add_interpolation(interpolation);
interpolation = existing_buffer;
}
if interpolation.contents.is_empty() {
return Err(("expected \"}\".", self.toks().current_span()).into());
}
let was_in_style_rule = self.flags().in_style_rule();
*self.flags_mut() |= ContextFlags::IN_STYLE_RULE;
let selector_span = self.toks_mut().span_from(start);
let children = self.with_children(Self::parse_statement)?;
self.flags_mut()
.set(ContextFlags::IN_STYLE_RULE, was_in_style_rule);
let span = selector_span.merge(children.span);
Ok(AstStmt::RuleSet(AstRuleSet {
selector: interpolation,
body: children.node,
selector_span,
span,
}))
}
fn parse_silent_comment(&mut self) -> SassResult<AstStmt> {
let start = self.toks().cursor();
debug_assert!(self.next_matches("//"));
self.toks_mut().next();
self.toks_mut().next();
let mut buffer = String::new();
while let Some(tok) = self.toks_mut().next() {
if tok.kind == '\n' {
self.whitespace_without_comments();
if self.next_matches("//") {
self.toks_mut().next();
self.toks_mut().next();
buffer.clear();
continue;
}
break;
}
buffer.push(tok.kind);
}
if self.is_plain_css() {
return Err((
"Silent comments aren't allowed in plain CSS.",
self.toks_mut().span_from(start),
)
.into());
}
self.whitespace_without_comments();
Ok(AstStmt::SilentComment(AstSilentComment {
text: buffer,
span: self.toks_mut().span_from(start),
}))
}
fn next_is_hex(&self) -> bool {
match self.toks().peek() {
Some(Token { kind, .. }) => kind.is_ascii_hexdigit(),
None => false,
}
}
fn assert_public(ident: &str, span: Span) -> SassResult<()> {
if !ScssParser::is_private(ident) {
return Ok(());
}
Err((
"Private members can't be accessed from outside their modules.",
span,
)
.into())
}
fn is_private(ident: &str) -> bool {
ident.starts_with('-') || ident.starts_with('_')
}
fn parse_variable_declaration_without_namespace(
&mut self,
namespace: Option<Spanned<Identifier>>,
start: Option<usize>,
) -> SassResult<AstVariableDecl> {
let start = start.unwrap_or_else(|| self.toks().cursor());
let name = self.parse_variable_name()?;
if namespace.is_some() {
Self::assert_public(&name, self.toks_mut().span_from(start))?;
}
if self.is_plain_css() {
return Err((
"Sass variables aren't allowed in plain CSS.",
self.toks_mut().span_from(start),
)
.into());
}
self.whitespace()?;
self.expect_char(':')?;
self.whitespace()?;
let value = self.parse_expression(None, None, None)?.node;
let mut is_guarded = false;
let mut is_global = false;
while self.scan_char('!') {
let flag_start = self.toks().cursor();
let flag = self.parse_identifier(false, false)?;
match flag.as_str() {
"default" => is_guarded = true,
"global" => {
if namespace.is_some() {
return Err((
"!global isn't allowed for variables in other modules.",
self.toks_mut().span_from(flag_start),
)
.into());
}
is_global = true;
}
_ => {
return Err(
("Invalid flag name.", self.toks_mut().span_from(flag_start)).into(),
)
}
}
self.whitespace()?;
}
self.expect_statement_separator(Some("variable declaration"))?;
let declaration = AstVariableDecl {
namespace,
name: Identifier::from(name),
value,
is_guarded,
is_global,
span: self.toks_mut().span_from(start),
};
if is_global {
}
Ok(declaration)
}
fn almost_any_value(
&mut self,
omit_comments: bool,
) -> SassResult<Interpolation> {
let mut buffer = Interpolation::new();
while let Some(tok) = self.toks().peek() {
match tok.kind {
'\\' => {
buffer.add_char(tok.kind);
self.toks_mut().next();
match self.toks_mut().next() {
Some(tok) => buffer.add_char(tok.kind),
None => {
return Err(("expected more input.", self.toks().current_span()).into())
}
}
}
'"' | '\'' => {
buffer.add_interpolation(
self.parse_interpolated_string()?
.node
.as_interpolation(false),
);
}
'/' => {
let comment_start = self.toks().cursor();
if self.scan_comment()? {
if !omit_comments {
buffer.add_string(self.toks().raw_text(comment_start));
}
} else {
buffer.add_char(self.toks_mut().next().unwrap().kind);
}
}
'#' => {
if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) {
buffer.add_interpolation(self.parse_interpolated_identifier()?);
} else {
self.toks_mut().next();
buffer.add_char(tok.kind);
}
}
'\r' | '\n' => {
if self.is_indented() {
break;
}
buffer.add_char(self.toks_mut().next().unwrap().kind);
}
'!' | ';' | '{' | '}' => break,
'u' | 'U' => {
let before_url = self.toks().cursor();
if !self.scan_identifier("url", false)? {
self.toks_mut().next();
buffer.add_char(tok.kind);
continue;
}
match self.try_url_contents(None)? {
Some(contents) => buffer.add_interpolation(contents),
None => {
self.toks_mut().set_cursor(before_url);
self.toks_mut().next();
buffer.add_char(tok.kind);
}
}
}
_ => {
if self.looking_at_identifier() {
buffer.add_string(self.parse_identifier(false, false)?);
} else {
buffer.add_char(self.toks_mut().next().unwrap().kind);
}
}
}
}
Ok(buffer)
}
fn parse_variable_declaration_or_interpolation(
&mut self,
) -> SassResult<VariableDeclOrInterpolation> {
if !self.looking_at_identifier() {
return Ok(VariableDeclOrInterpolation::Interpolation(
self.parse_interpolated_identifier()?,
));
}
let start = self.toks().cursor();
let ident = self.parse_identifier(false, false)?;
if self.next_matches(".$") {
let namespace_span = self.toks_mut().span_from(start);
self.expect_char('.')?;
Ok(VariableDeclOrInterpolation::VariableDecl(
self.parse_variable_declaration_without_namespace(
Some(Spanned {
node: Identifier::from(ident),
span: namespace_span,
}),
Some(start),
)?,
))
} else {
let mut buffer = Interpolation::new_plain(ident);
if self.looking_at_interpolated_identifier_body() {
buffer.add_interpolation(self.parse_interpolated_identifier()?);
}
Ok(VariableDeclOrInterpolation::Interpolation(buffer))
}
}
fn looking_at_interpolated_identifier_body(&mut self) -> bool {
match self.toks().peek() {
Some(Token { kind: '\\', .. }) => true,
Some(Token { kind: '#', .. })
if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) =>
{
true
}
Some(Token { kind, .. }) if is_name(kind) => true,
Some(..) | None => false,
}
}
fn expression_until_comparison(&mut self) -> SassResult<Spanned<AstExpr>> {
let value = self.parse_expression(
Some(&|parser| {
Ok(match parser.toks().peek() {
Some(Token { kind: '>', .. }) | Some(Token { kind: '<', .. }) => true,
Some(Token { kind: '=', .. }) => {
!matches!(parser.toks().peek_n(1), Some(Token { kind: '=', .. }))
}
_ => false,
})
}),
None,
None,
)?;
Ok(value)
}
fn parse_media_query_list(&mut self) -> SassResult<Interpolation> {
let mut buf = Interpolation::new();
loop {
self.whitespace()?;
self.parse_media_query(&mut buf)?;
self.whitespace()?;
if !self.scan_char(',') {
break;
}
buf.add_char(',');
buf.add_char(' ');
}
Ok(buf)
}
fn parse_media_in_parens(&mut self, buf: &mut Interpolation) -> SassResult<()> {
self.expect_char_with_message('(', "media condition in parentheses")?;
buf.add_char('(');
self.whitespace()?;
if matches!(self.toks().peek(), Some(Token { kind: '(', .. })) {
self.parse_media_in_parens(buf)?;
self.whitespace()?;
if self.scan_identifier("and", false)? {
buf.add_string(" and ".to_owned());
self.expect_whitespace()?;
self.parse_media_logic_sequence(buf, "and")?;
} else if self.scan_identifier("or", false)? {
buf.add_string(" or ".to_owned());
self.expect_whitespace()?;
self.parse_media_logic_sequence(buf, "or")?;
}
} else if self.scan_identifier("not", false)? {
buf.add_string("not ".to_owned());
self.expect_whitespace()?;
self.parse_media_or_interpolation(buf)?;
} else {
buf.add_expr(self.expression_until_comparison()?);
if self.scan_char(':') {
self.whitespace()?;
buf.add_char(':');
buf.add_char(' ');
buf.add_expr(self.parse_expression(None, None, None)?);
} else {
let next = self.toks().peek();
if matches!(
next,
Some(Token {
kind: '<' | '>' | '=',
..
})
) {
let next = next.unwrap().kind;
buf.add_char(' ');
buf.add_char(self.toks_mut().next().unwrap().kind);
if (next == '<' || next == '>') && self.scan_char('=') {
buf.add_char('=');
}
buf.add_char(' ');
self.whitespace()?;
buf.add_expr(self.expression_until_comparison()?);
if (next == '<' || next == '>') && self.scan_char(next) {
buf.add_char(' ');
buf.add_char(next);
if self.scan_char('=') {
buf.add_char('=');
}
buf.add_char(' ');
self.whitespace()?;
buf.add_expr(self.expression_until_comparison()?);
}
}
}
}
self.expect_char(')')?;
self.whitespace()?;
buf.add_char(')');
Ok(())
}
fn parse_media_logic_sequence(
&mut self,
buf: &mut Interpolation,
operator: &'static str,
) -> SassResult<()> {
loop {
self.parse_media_or_interpolation(buf)?;
self.whitespace()?;
if !self.scan_identifier(operator, false)? {
return Ok(());
}
self.expect_whitespace()?;
buf.add_char(' ');
buf.add_string(operator.to_owned());
buf.add_char(' ');
}
}
fn parse_media_or_interpolation(&mut self, buf: &mut Interpolation) -> SassResult<()> {
if self.toks_mut().next_char_is('#') {
buf.add_interpolation(self.parse_single_interpolation()?);
} else {
self.parse_media_in_parens(buf)?;
}
Ok(())
}
fn parse_media_query(&mut self, buf: &mut Interpolation) -> SassResult<()> {
if matches!(self.toks().peek(), Some(Token { kind: '(', .. })) {
self.parse_media_in_parens(buf)?;
self.whitespace()?;
if self.scan_identifier("and", false)? {
buf.add_string(" and ".to_owned());
self.expect_whitespace()?;
self.parse_media_logic_sequence(buf, "and")?;
} else if self.scan_identifier("or", false)? {
buf.add_string(" or ".to_owned());
self.expect_whitespace()?;
self.parse_media_logic_sequence(buf, "or")?;
}
return Ok(());
}
let ident1 = self.parse_interpolated_identifier()?;
if ident1.as_plain().unwrap_or("").to_ascii_lowercase() == "not" {
self.expect_whitespace()?;
if !self.looking_at_interpolated_identifier() {
buf.add_string("not ".to_owned());
self.parse_media_or_interpolation(buf)?;
return Ok(());
}
}
self.whitespace()?;
buf.add_interpolation(ident1);
if !self.looking_at_interpolated_identifier() {
return Ok(());
}
buf.add_char(' ');
let ident2 = self.parse_interpolated_identifier()?;
if ident2.as_plain().unwrap_or("").to_ascii_lowercase() == "and" {
self.expect_whitespace()?;
buf.add_string(" and ".to_owned());
} else {
self.whitespace()?;
buf.add_interpolation(ident2);
if self.scan_identifier("and", false)? {
self.expect_whitespace()?;
buf.add_string(" and ".to_owned());
} else {
return Ok(());
}
}
if self.scan_identifier("not", false)? {
self.expect_whitespace()?;
buf.add_string("not ".to_owned());
self.parse_media_or_interpolation(buf)?;
return Ok(());
}
self.parse_media_logic_sequence(buf, "and")?;
Ok(())
}
}