use std::cell::RefCell;
use std::rc::Rc;
use crate::fxhash::FxHashMap as HashMap;
use crate::ast::{
BinOp, CallArg, Callable, Conjunction, CssCustomItem, CssCustomValue, CustomDecl, Declaration, Expr,
IfClause, IfCond, ImportArg, ImportModifier, MediaFeature, MediaInParens, MediaQuery, MediaQueryList,
ParamList, PropertySet, Rule, SrcLines, Stmt, Stylesheet, SupportsCondition, SupportsValue, TplPiece,
UnOp, VarDecl,
};
use crate::error::Error;
use crate::scanner::Pos;
use crate::value::{CalcNode, CalcOp, List, ListSep, Map, Number, SassFunction, SassMixin, SassStr, Value};
use crate::{Importer, OutputStyle, Syntax};
type CachedImport = std::rc::Rc<(String, Syntax, crate::ast::Stylesheet)>;
fn parse_with_syntax(src: &str, syntax: Syntax) -> Result<crate::ast::Stylesheet, Error> {
match syntax {
Syntax::Scss => crate::parser::parse(src),
Syntax::Css => crate::parser::parse_plain_css(src),
Syntax::Sass => crate::sass_parser::parse(src),
}
}
type EvaledArgs = (Vec<Value>, Vec<(String, Value)>, ListSep);
pub(crate) type Scope = std::rc::Rc<std::cell::RefCell<HashMap<String, Value>>>;
fn new_scope() -> Scope {
std::rc::Rc::new(std::cell::RefCell::new(HashMap::default()))
}
pub(crate) type FnScope = std::rc::Rc<std::cell::RefCell<HashMap<String, Rc<UserCallable>>>>;
fn new_fn_scope() -> FnScope {
std::rc::Rc::new(std::cell::RefCell::new(HashMap::default()))
}
pub(crate) struct UserCallable {
pub def: Rc<Callable>,
pub env: Vec<Scope>,
pub env_semi: Vec<bool>,
pub env_fns: Vec<FnScope>,
pub env_mixins: Vec<FnScope>,
}
#[derive(Clone)]
pub(crate) enum OutNode {
Rule {
selectors: Vec<String>,
linebreaks: Vec<bool>,
items: Vec<OutItem>,
lines: SrcLines,
extend_base: usize,
},
Comment(String, SrcLines),
Raw(String),
Blank,
AtRule {
name: String,
prelude: String,
body: Vec<OutNode>,
has_block: bool,
lines: SrcLines,
},
ModuleScope {
key: String,
nodes: Vec<OutNode>,
},
AtDecl {
prop: String,
value: String,
important: bool,
custom: bool,
lines: SrcLines,
},
}
#[derive(Clone, PartialEq, Eq)]
struct ResolvedQuery {
modifier: Option<String>,
mtype: Option<String>,
conditions: Vec<String>,
conjunction_and: bool,
}
enum MergeResult {
Empty,
Unrepresentable,
Query(ResolvedQuery),
}
#[derive(Clone)]
pub(crate) enum OutItem {
Decl {
prop: String,
value: String,
important: bool,
custom: bool,
lines: SrcLines,
},
Comment(String, SrcLines),
ChildlessAtRule {
name: String,
prelude: String,
lines: SrcLines,
},
NestedRule {
selectors: Vec<String>,
items: Vec<OutItem>,
},
NestedAtRule {
name: String,
prelude: String,
items: Vec<OutItem>,
},
}
#[derive(Clone, Copy)]
enum MemberKind {
Function,
Mixin,
Variable,
}
enum Sink<'a> {
Top(&'a mut Vec<OutNode>),
Rule {
selectors: &'a [String],
linebreaks: &'a [bool],
lines: SrcLines,
items: &'a mut Vec<OutItem>,
nested: &'a mut Vec<OutNode>,
at_depth: usize,
flushed: &'a mut Option<usize>,
extend_base: usize,
},
AtRoot(&'a mut Vec<OutNode>),
}
impl Sink<'_> {
fn is_top(&self) -> bool {
matches!(self, Sink::Top(_))
}
fn is_rule(&self) -> bool {
matches!(self, Sink::Rule { .. })
}
fn push_childless_at_rule(&mut self, name: String, prelude: String, lines: SrcLines) {
match self {
Sink::Rule { items, .. } => items.push(OutItem::ChildlessAtRule { name, prelude, lines }),
_ => self.push_at_rule(OutNode::AtRule {
name,
prelude,
body: Vec::new(),
has_block: false,
lines,
}),
}
}
fn push_comment(&mut self, text: String, lines: SrcLines) {
if text.starts_with("# sourceMappingURL=") || text.starts_with("# sourceURL=") {
if let Sink::Top(out) = self {
out.push(OutNode::Raw(STYLE_GROUP_END.to_string()));
}
return;
}
match self {
Sink::Top(out) => {
let out = &mut **out;
push_group(out, vec![OutNode::Comment(text, lines)]);
}
Sink::Rule { items, .. } => items.push(OutItem::Comment(text, lines)),
Sink::AtRoot(body) => body.push(OutNode::Comment(text, lines)),
}
}
fn push_item(&mut self, item: OutItem) {
match self {
Sink::Rule { items, .. } => items.push(item),
Sink::AtRoot(body) => match item {
OutItem::Decl {
prop,
value,
important,
custom,
lines,
} => body.push(OutNode::AtDecl {
prop,
value,
important,
custom,
lines,
}),
OutItem::Comment(text, lines) => body.push(OutNode::Comment(text, lines)),
OutItem::ChildlessAtRule { name, prelude, lines } => body.push(OutNode::AtRule {
name,
prelude,
body: Vec::new(),
has_block: false,
lines,
}),
OutItem::NestedRule { selectors, items } => body.push(OutNode::Rule {
selectors,
linebreaks: Vec::new(),
items,
lines: SrcLines::default(),
extend_base: usize::MAX,
}),
OutItem::NestedAtRule { name, prelude, items } => body.push(OutNode::AtRule {
name,
prelude,
body: items
.into_iter()
.map(|it| match it {
OutItem::Decl {
prop,
value,
important,
custom,
lines,
} => OutNode::AtDecl {
prop,
value,
important,
custom,
lines,
},
OutItem::Comment(text, lines) => OutNode::Comment(text, lines),
OutItem::NestedRule { selectors, items } => OutNode::Rule {
selectors,
linebreaks: Vec::new(),
items,
lines: SrcLines::default(),
extend_base: usize::MAX,
},
OutItem::ChildlessAtRule { name, prelude, lines } => OutNode::AtRule {
name,
prelude,
body: Vec::new(),
has_block: false,
lines,
},
OutItem::NestedAtRule { name, prelude, items } => OutNode::AtRule {
name,
prelude,
body: vec![OutNode::Rule {
selectors: Vec::new(),
linebreaks: Vec::new(),
items,
lines: SrcLines::default(),
extend_base: usize::MAX,
}],
has_block: true,
lines: SrcLines::default(),
},
})
.collect(),
has_block: true,
lines: SrcLines::default(),
}),
},
Sink::Top(_) => {}
}
}
fn flush_rule_block(&mut self) {
if let Sink::Rule {
selectors,
linebreaks,
lines,
items,
nested,
at_depth,
flushed,
extend_base,
} = self
{
if !items.is_empty() {
if selectors.is_empty() {
items.clear();
} else {
let insert_at = nested
.iter()
.position(|n| is_escaping_marker(n, *at_depth))
.unwrap_or(nested.len());
if insert_at > 0 && **flushed == Some(insert_at - 1) {
if let Some(OutNode::Rule { items: prev, .. }) = nested.get_mut(insert_at - 1) {
prev.append(*items);
return;
}
}
let rule = OutNode::Rule {
selectors: selectors.to_vec(),
linebreaks: linebreaks.to_vec(),
items: std::mem::take(*items),
lines: *lines,
extend_base: *extend_base,
};
nested.insert(insert_at, rule);
**flushed = Some(insert_at);
}
}
}
}
fn emit_style_rule(&mut self, output: Vec<OutNode>) {
match self {
Sink::Top(out) => {
let out = &mut **out;
push_group(out, output);
if !out.is_empty() {
out.push(OutNode::Raw(STYLE_GROUP_END.to_string()));
}
}
Sink::Rule { .. } => {
self.flush_rule_block();
if let Sink::Rule { nested, .. } = self {
nested.extend(output);
}
}
Sink::AtRoot(body) => body.extend(output),
}
}
fn push_at_rule(&mut self, node: OutNode) {
match self {
Sink::Top(out) => {
let out = &mut **out;
push_group(out, vec![node]);
}
Sink::Rule { .. } => {
let depth = match self {
Sink::Rule { at_depth, .. } => *at_depth,
_ => 0,
};
if !is_escaping_marker(&node, depth) {
self.flush_rule_block();
}
if let Sink::Rule { nested, .. } = self {
nested.push(node);
}
}
Sink::AtRoot(body) => body.push(node),
}
}
}
pub(crate) struct EvalOptions<'a> {
pub style: OutputStyle,
pub importer: Option<&'a dyn Importer>,
pub source: &'a str,
pub url: &'a str,
pub glyphs: crate::diag::GlyphSet,
}
pub(crate) struct Evaluator<'a> {
scopes: Vec<Scope>,
scope_semi_global: Vec<bool>,
options: EvalOptions<'a>,
loading: Vec<String>,
import_cache: HashMap<(String, Option<String>), CachedImport>,
current_url_stamp: u32,
functions: Vec<FnScope>,
mixins: Vec<FnScope>,
content_stack: Vec<Option<ContentBlock>>,
in_mixin: Vec<bool>,
media_queries: Vec<ResolvedQuery>,
current_selector: Option<Vec<String>>,
current_linebreaks: Vec<bool>,
extends: Vec<PendingExtend>,
decl_prefix: Option<String>,
in_supports_declaration: bool,
in_plain_css: bool,
config_is_implicit: bool,
current_module: String,
module_deps: RefCell<HashMap<String, std::collections::HashSet<String>>>,
module_dep_order: RefCell<HashMap<String, Vec<String>>>,
load_css_copies: RefCell<Vec<(String, String)>>,
copy_counter: std::cell::Cell<usize>,
media_hoist: Vec<Vec<OutNode>>,
at_root_hoist: std::collections::VecDeque<AtRootBatch>,
at_rule_ctx: Vec<AtCtx>,
bogus_selectors: Vec<String>,
placeholder_rules: Vec<(String, String)>,
import_clone: Option<(String, std::collections::HashSet<String>)>,
current_file_dir: Option<String>,
in_keyframes: bool,
at_root_excluding_style_rule: bool,
forwarded_globals: HashMap<String, usize>,
used_modules: HashMap<String, String>,
star_modules: Vec<String>,
used_user_modules: HashMap<String, Rc<Module>>,
star_user_modules: Vec<Rc<Module>>,
module_cache: Rc<RefCell<HashMap<String, Rc<Module>>>>,
forwarded: Forwarded,
pending_config: HashMap<String, (Value, bool)>,
pending_config_id: usize,
config_id_counter: std::cell::Cell<usize>,
consumed_config: Vec<String>,
member: String,
call_stack: Vec<DiagFrame>,
current_url: String,
current_source: Rc<str>,
file_sources: Rc<RefCell<HashMap<String, Rc<str>>>>,
deprecations_shown: HashMap<&'static str, u32>,
deprecations_omitted: u32,
deprecations_seen: std::collections::HashSet<(&'static str, String, usize, usize)>,
file_ids: HashMap<String, u32>,
}
#[derive(Clone)]
struct DiagFrame {
url: String,
pos: Pos,
member: String,
length: usize,
}
struct Module {
vars: Scope,
functions: FnScope,
mixins: FnScope,
used_user_modules: HashMap<String, Rc<Module>>,
star_user_modules: Vec<Rc<Module>>,
used_builtin_modules: HashMap<String, String>,
star_builtin_modules: Vec<String>,
forwarded_builtins: Vec<ForwardedBuiltin>,
var_origins: HashMap<String, (Rc<Module>, String)>,
var_write_origins: HashMap<String, (Rc<Module>, String)>,
fn_origins: HashMap<String, Rc<Module>>,
mixin_origins: HashMap<String, Rc<Module>>,
diag_url: String,
config_origin: std::cell::Cell<usize>,
file_dir: String,
emitted_main: std::cell::Cell<bool>,
css: Vec<OutNode>,
}
impl Module {
fn var(&self, name: &str) -> Option<Value> {
if let Some((m, oname)) = self.var_origin(name) {
return m.var(&oname);
}
let vars = self.vars.borrow();
if let Some(v) = vars.get(name) {
return Some(v.clone());
}
let norm = normalize_var_name(name);
vars.iter()
.find(|(k, _)| normalize_var_name(k) == norm)
.map(|(_, v)| v.clone())
}
fn var_origin(&self, name: &str) -> Option<(Rc<Module>, String)> {
if let Some((m, o)) = self.var_origins.get(name) {
return Some((Rc::clone(m), o.clone()));
}
let norm = normalize_var_name(name);
self.var_origins
.iter()
.find(|(k, _)| normalize_var_name(k) == norm)
.map(|(_, (m, o))| (Rc::clone(m), o.clone()))
}
fn var_write_origin(&self, name: &str) -> Option<(Rc<Module>, String)> {
if let Some((m, o)) = self.var_write_origins.get(name) {
return Some((Rc::clone(m), o.clone()));
}
let norm = normalize_var_name(name);
self.var_write_origins
.iter()
.find(|(k, _)| normalize_var_name(k) == norm)
.map(|(_, (m, o))| (Rc::clone(m), o.clone()))
}
fn fn_origin(&self, name: &str) -> Option<Rc<Module>> {
if let Some(m) = self.fn_origins.get(name) {
return Some(Rc::clone(m));
}
let norm = normalize_var_name(name);
self.fn_origins
.iter()
.find(|(k, _)| normalize_var_name(k) == norm)
.map(|(_, m)| Rc::clone(m))
}
fn mixin_origin(&self, name: &str) -> Option<Rc<Module>> {
if let Some(m) = self.mixin_origins.get(name) {
return Some(Rc::clone(m));
}
let norm = normalize_var_name(name);
self.mixin_origins
.iter()
.find(|(k, _)| normalize_var_name(k) == norm)
.map(|(_, m)| Rc::clone(m))
}
fn function(&self, name: &str) -> Option<Rc<UserCallable>> {
let fns = self.functions.borrow();
if let Some(f) = fns.get(name) {
return Some(Rc::clone(f));
}
let norm = normalize_var_name(name);
fns.iter()
.find(|(k, _)| normalize_var_name(k) == norm)
.map(|(_, f)| Rc::clone(f))
}
fn mixin(&self, name: &str) -> Option<Rc<UserCallable>> {
let mixins = self.mixins.borrow();
if let Some(m) = mixins.get(name) {
return Some(Rc::clone(m));
}
let norm = normalize_var_name(name);
mixins
.iter()
.find(|(k, _)| normalize_var_name(k) == norm)
.map(|(_, m)| Rc::clone(m))
}
}
struct ContentBlock {
stmts: Rc<Vec<Stmt>>,
params: Option<Rc<ParamList>>,
caller_env: Option<Box<SavedModuleEnv>>,
}
#[derive(Clone)]
struct SavedModuleEnv {
scopes: Vec<Scope>,
scope_semi_global: Vec<bool>,
functions: Vec<FnScope>,
mixins: Vec<FnScope>,
used_modules: HashMap<String, String>,
star_modules: Vec<String>,
used_user_modules: HashMap<String, Rc<Module>>,
star_user_modules: Vec<Rc<Module>>,
write_back: Option<Rc<Module>>,
}
#[derive(Default)]
struct Forwarded {
vars: HashMap<String, Value>,
functions: HashMap<String, Rc<UserCallable>>,
mixins: HashMap<String, Rc<UserCallable>>,
var_origins: HashMap<String, (Rc<Module>, String)>,
fn_origins: HashMap<String, Rc<Module>>,
mixin_origins: HashMap<String, Rc<Module>>,
builtins: Vec<ForwardedBuiltin>,
var_src: HashMap<String, *const Module>,
fn_src: HashMap<String, *const Module>,
mixin_src: HashMap<String, *const Module>,
}
#[derive(Clone)]
struct ForwardedBuiltin {
module: String,
prefix: Option<String>,
show: Option<std::collections::HashSet<String>>,
hide: Option<std::collections::HashSet<String>>,
}
impl ForwardedBuiltin {
fn visible(&self, bare: &str) -> bool {
if let Some(show) = &self.show {
return show.contains(bare);
}
if let Some(hide) = &self.hide {
return !hide.contains(bare);
}
true
}
}
struct PendingExtend {
target: crate::selector::Simple,
target_str: String,
extenders: Vec<String>,
extender_breaks: Vec<bool>,
optional: bool,
in_media: bool,
origin: String,
pos: Pos,
}
impl<'a> Evaluator<'a> {
pub(crate) fn new(options: EvalOptions<'a>) -> Self {
let url = options.url.to_string();
let entry_dir = dirname_of(options.url).unwrap_or_default();
let source: Rc<str> = Rc::from(options.source);
let file_sources: HashMap<String, Rc<str>> =
[(url.clone(), Rc::clone(&source))].into_iter().collect();
Evaluator {
member: "root stylesheet".to_string(),
call_stack: Vec::new(),
current_url: url,
current_source: source,
file_sources: Rc::new(RefCell::new(file_sources)),
deprecations_shown: HashMap::default(),
deprecations_omitted: 0,
deprecations_seen: std::collections::HashSet::new(),
file_ids: HashMap::default(),
scopes: vec![new_scope()],
scope_semi_global: vec![true],
options,
loading: Vec::new(),
import_cache: HashMap::default(),
current_url_stamp: 0,
functions: vec![new_fn_scope()],
mixins: vec![new_fn_scope()],
content_stack: Vec::new(),
in_mixin: Vec::new(),
media_queries: Vec::new(),
current_selector: None,
current_linebreaks: Vec::new(),
extends: Vec::new(),
decl_prefix: None,
in_supports_declaration: false,
in_plain_css: false,
config_is_implicit: false,
forwarded_globals: HashMap::default(),
current_module: String::new(),
module_deps: RefCell::new(HashMap::default()),
module_dep_order: RefCell::new(HashMap::default()),
load_css_copies: RefCell::new(Vec::new()),
copy_counter: std::cell::Cell::new(0),
in_keyframes: false,
at_root_excluding_style_rule: false,
import_clone: None,
current_file_dir: Some(entry_dir),
media_hoist: Vec::new(),
at_root_hoist: std::collections::VecDeque::new(),
at_rule_ctx: Vec::new(),
bogus_selectors: Vec::new(),
placeholder_rules: Vec::new(),
used_modules: HashMap::default(),
star_modules: Vec::new(),
used_user_modules: HashMap::default(),
star_user_modules: Vec::new(),
module_cache: Rc::new(RefCell::new(HashMap::default())),
forwarded: Forwarded::default(),
pending_config: HashMap::default(),
pending_config_id: 0,
config_id_counter: std::cell::Cell::new(0),
consumed_config: Vec::new(),
}
}
fn stamp(&mut self, mut lines: SrcLines) -> SrcLines {
if lines == SrcLines::default() {
return lines;
}
if self.current_url_stamp != 0 {
lines.file = self.current_url_stamp;
return lines;
}
let next = self.file_ids.len() as u32 + 1;
let id = *self.file_ids.entry(self.current_url.clone()).or_insert(next);
self.current_url_stamp = id;
lines.file = id;
lines
}
pub(crate) fn eval_sheet(&mut self, sheet: &Stylesheet, out: &mut Vec<OutNode>) -> Result<(), Error> {
{
let mut sink = Sink::Top(out);
let r = self.exec(&sheet.stmts, &[], &mut sink);
if let Err(e) = r {
let e = self.finalize_error(e);
self.emit_deprecation_footer();
return Err(e);
}
}
self.apply_extends(out)?;
hoist_css_imports(out);
self.emit_deprecation_footer();
Ok(())
}
fn diag_enabled(&self) -> bool {
!self.options.source.is_empty()
}
fn frames_for(&self, pos: Pos) -> Vec<DiagFrame> {
let mut frames = Vec::with_capacity(self.call_stack.len() + 1);
frames.push(DiagFrame {
url: self.current_url.clone(),
pos,
member: self.member.clone(),
length: 0,
});
frames.extend(self.call_stack.iter().rev().cloned());
frames
}
fn render_frame_block(frames: &[DiagFrame], indent: usize) -> String {
let fields: Vec<String> = frames
.iter()
.map(|f| format!("{} {}:{}", f.url, f.pos.line, f.pos.col))
.collect();
let width = fields.iter().map(String::len).max().unwrap_or(0);
let pad: String = " ".repeat(indent);
let mut out = String::new();
for (i, (field, frame)) in fields.iter().zip(frames).enumerate() {
if i > 0 {
out.push('\n');
}
out.push_str(&pad);
out.push_str(field);
for _ in 0..width.saturating_sub(field.len()) {
out.push(' ');
}
out.push_str(" ");
out.push_str(&frame.member);
}
out
}
fn source_for(&self, url: &str) -> Rc<str> {
if url == self.current_url {
return Rc::clone(&self.current_source);
}
self.file_sources
.borrow()
.get(url)
.map(Rc::clone)
.unwrap_or_else(|| Rc::clone(&self.current_source))
}
fn finalize_error(&self, mut e: Error) -> Error {
if e.rendered.is_some() || !self.diag_enabled() || !e.has_position() {
return e;
}
let frames = self.frames_for(Pos {
line: e.line,
col: e.col,
});
e.rendered = Some(self.render_error_with_frames(&e, &frames));
e
}
fn interp_selector_error(
&self,
rule: &Rule,
sel_str: &str,
interp_bounds: &[(usize, usize)],
at_idx: usize,
) -> Error {
const MSG: &str = "expected selector.";
let spans = &rule.selector_interp_spans;
let single_line = !sel_str.contains('\n');
if spans.len() == interp_bounds.len() {
for (k, &(start, len)) in interp_bounds.iter().enumerate() {
if at_idx >= start && at_idx < start + len {
let (line, col_start, col_end) = spans[k];
let pos = Pos {
line: line as usize,
col: col_start as usize,
};
let mut e = Error::at(MSG, pos);
if self.diag_enabled() {
let source = self.source_for(&self.current_url.clone());
let frames = self.frames_for(pos);
let mut rendered = format!("Error: {MSG}\n");
rendered.push_str(&crate::diag::render_interp_error_snippet(
&source,
line as usize,
col_start as usize,
col_end as usize,
sel_str,
at_idx + 1,
&frames[0].url,
self.options.glyphs,
));
rendered.push('\n');
rendered.push_str(&Self::render_frame_block(&frames, 2));
e.rendered = Some(rendered);
}
return e;
}
}
}
if single_line
&& spans.len() == interp_bounds.len()
&& spans
.iter()
.all(|&(l, _, _)| l as usize == rule.selector_pos.line)
{
let mut shift: i64 = 0;
for (k, &(start, len)) in interp_bounds.iter().enumerate() {
if at_idx >= start + len {
let (_, col_start, col_end) = spans[k];
let src_total = (col_end as i64 + 1) - (col_start as i64 - 2);
shift += src_total - len as i64;
}
}
let col = (rule.selector_pos.col as i64 + at_idx as i64 + shift).max(1) as usize;
return Error::at(
MSG,
Pos {
line: rule.selector_pos.line,
col,
},
);
}
Error::at(MSG, rule.selector_pos)
}
fn render_error_with_frames(&self, e: &Error, frames: &[DiagFrame]) -> String {
let primary = &frames[0];
let source = self.source_for(&primary.url);
let length = if primary.length > 0 {
primary.length
} else {
e.length
};
let span = crate::diag::Span {
line: primary.pos.line,
col: primary.pos.col,
length,
};
let mut out = format!("Error: {}\n", e.message);
out.push_str(&crate::diag::render_snippet(
&source,
span,
&[],
self.options.glyphs,
));
out.push('\n');
out.push_str(&Self::render_frame_block(frames, 2));
out
}
fn emit_deprecation(&mut self, dep: &crate::deprecation::Deprecation, pos: Pos, len: usize) {
if !self.diag_enabled() {
return;
}
let key = (dep.id, self.current_url.clone(), pos.line, pos.col);
if !self.deprecations_seen.insert(key) {
return;
}
let count = self.deprecations_shown.entry(dep.id).or_insert(0);
if *count >= 5 {
self.deprecations_omitted += 1;
return;
}
*count += 1;
let frames = self.frames_for(pos);
let span = crate::diag::Span {
line: pos.line,
col: pos.col,
length: len,
};
let source = self.source_for(&self.current_url.clone());
let mut block = dep.render_header();
block.push_str(&crate::diag::render_snippet(
&source,
span,
&[],
self.options.glyphs,
));
block.push('\n');
block.push_str(&Self::render_frame_block(&frames, 4));
eprintln!("{block}\n");
}
fn emit_deprecation_footer(&self) {
if self.deprecations_omitted == 0 {
return;
}
eprintln!(
"WARNING: {} repetitive deprecation warnings omitted.\nRun in verbose mode to see all warnings.\n",
self.deprecations_omitted
);
}
fn enter_call(&mut self, call_pos: Pos, call_len: usize, new_member: &str) -> String {
self.call_stack.push(DiagFrame {
url: self.current_url.clone(),
pos: call_pos,
member: self.member.clone(),
length: call_len,
});
std::mem::replace(&mut self.member, new_member.to_string())
}
fn leave_call(&mut self, saved_member: String) {
self.call_stack.pop();
self.member = saved_member;
}
fn emit_warn(&mut self, value: &Expr, pos: Pos) -> Result<(), Error> {
let v = self.eval_expr(value)?;
let msg = v.to_message();
if self.diag_enabled() {
let frames = self.frames_for(pos);
eprintln!("WARNING: {}\n{}\n", msg, Self::render_frame_block(&frames, 4));
} else {
eprintln!("WARNING: {msg}");
}
Ok(())
}
fn emit_debug(&mut self, value: &Expr, pos: Pos) -> Result<(), Error> {
let v = self.eval_expr(value)?;
let msg = v.to_message();
if self.diag_enabled() {
eprintln!("{}:{} DEBUG: {}", self.current_url, pos.line, msg);
} else {
eprintln!("DEBUG: {msg}");
}
Ok(())
}
fn build_error(&mut self, value: &Expr, pos: Pos, length: usize) -> Error {
let msg = match self.eval_expr(value) {
Ok(v) => v.to_error_message(),
Err(e) => return e,
};
if !self.diag_enabled() {
return Error::unpositioned(msg);
}
let frames: Vec<DiagFrame> = if self.call_stack.is_empty() {
vec![DiagFrame {
url: self.current_url.clone(),
pos,
member: self.member.clone(),
length,
}]
} else {
self.call_stack.iter().rev().cloned().collect()
};
let mut e = Error::at(msg, frames[0].pos);
e.length = frames[0].length;
e.rendered = Some(self.render_error_with_frames(&e, &frames));
e
}
fn register_extend(
&mut self,
selector: &[TplPiece],
optional: bool,
pos: Pos,
parents: &[String],
) -> Result<(), Error> {
if parents.is_empty() || self.at_root_excluding_style_rule {
return Err(Error::at("@extend may only be used within style rules.", pos));
}
let extenders = self.current_selector.clone().unwrap_or_else(|| parents.to_vec());
let target = self.eval_template(selector)?;
if target.trim().is_empty() {
return Err(Error::at("expected selector.", pos));
}
let in_media = !self.media_queries.is_empty();
for t in split_commas(&target) {
let t = t.trim();
if t.is_empty() {
continue;
}
match crate::selector::classify_target(t) {
crate::selector::TargetClass::Simple(simple) => {
self.extends.push(PendingExtend {
origin: self.current_module.clone(),
target: simple,
target_str: t.to_string(),
extenders: extenders.clone(),
extender_breaks: self.current_linebreaks.clone(),
optional,
in_media,
pos,
});
}
crate::selector::TargetClass::Complex => {
return Err(Error::at("complex selectors may not be extended.", pos));
}
crate::selector::TargetClass::Compound => {
return Err(Error::at(
"compound selectors may no longer be extended.\n\
Consider `@extend a, :hover` instead.\n\
See https://sass-lang.com/d/extend-compound for details.",
pos,
));
}
crate::selector::TargetClass::Invalid => {
return Err(Error::at("expected selector.", pos));
}
}
}
Ok(())
}
fn apply_extends(&mut self, out: &mut Vec<OutNode>) -> Result<(), Error> {
let deps = self.module_deps.borrow();
let bfs = |start: &str| {
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
seen.insert(start.to_string());
let mut stack = vec![start.to_string()];
while let Some(k) = stack.pop() {
if let Some(nexts) = deps.get(&k) {
for n in nexts {
if seen.insert(n.clone()) {
stack.push(n.clone());
}
}
}
}
seen
};
let mut raw_cache: HashMap<String, std::collections::HashSet<String>> = HashMap::default();
for pe in &self.extends {
if raw_cache.contains_key(&pe.origin) {
continue;
}
let seen = bfs(&pe.origin);
raw_cache.insert(pe.origin.clone(), seen);
}
let copies = self.load_css_copies.borrow();
for (copy_key, base) in copies.iter() {
let base_reach = bfs(base);
for (origin, set) in raw_cache.iter_mut() {
if base_reach.contains(origin) {
set.insert(copy_key.clone());
}
}
}
if !copies.is_empty() {
let main_reach = bfs("");
for (origin, set) in raw_cache.iter_mut() {
if !origin.is_empty() && !main_reach.contains(origin) {
set.retain(|s| copies.iter().any(|(ck, _)| ck == s));
}
}
}
drop(copies);
drop(deps);
let closure_cache: HashMap<String, std::rc::Rc<std::collections::HashSet<String>>> = raw_cache
.into_iter()
.map(|(k, v)| (k, std::rc::Rc::new(v)))
.collect();
let mut extensions: Vec<crate::selector::Extension> = Vec::new();
for pe in &self.extends {
let mut extenders = Vec::new();
let mut extender_breaks = Vec::new();
for (i, ext) in pe.extenders.iter().enumerate() {
if let Some(c) = crate::selector::parse_complex_one(ext) {
if c.trailing.is_empty() {
extenders.push(c);
extender_breaks.push(pe.extender_breaks.get(i).copied().unwrap_or(false));
}
}
}
extensions.push(crate::selector::Extension {
target: Some(pe.target.clone()),
extenders,
extender_breaks,
optional: pe.optional,
matched: std::rc::Rc::new(std::cell::Cell::new(false)),
origin: pe.origin.clone(),
origin_closure: std::rc::Rc::clone(&closure_cache[&pe.origin]),
});
}
extensions.sort_by_key(|e| std::cmp::Reverse(closure_cache.get(&e.origin).map_or(0, |c| c.len())));
for pe in &self.extends {
if pe.in_media && root_rule_contains_target(out, &pe.target) {
return Err(Error::at(
"You may not @extend selectors across media queries.",
pe.pos,
));
}
}
let origins: Vec<String> = extensions.iter().map(|e| e.origin.clone()).collect();
let closures: HashMap<String, std::collections::HashSet<String>> = closure_cache
.iter()
.map(|(k, v)| (k.clone(), (**v).clone()))
.collect();
rewrite_nodes_scoped(out, "", &extensions, &origins, &closures);
for (pe, ext) in self.extends.iter().zip(extensions.iter()) {
let private = matches!(&pe.target,
crate::selector::Simple::Placeholder(n) if n.starts_with('-') || n.starts_with('_'));
if !ext.optional
&& !ext.matched.get()
&& !self
.bogus_selectors
.iter()
.any(|s| crate::selector::selector_contains_simple(s, &pe.target))
&& !self.placeholder_rules.iter().any(|(m, s)| {
let visible = if private {
*m == ext.origin
} else {
ext.origin_closure.contains(m)
};
visible && crate::selector::selector_contains_simple(s, &pe.target)
})
{
return Err(Error::at(
format!(
"The target selector was not found.\nUse \"@extend {} !optional\" to avoid this error.",
pe.target_str
),
pe.pos,
));
}
}
Ok(())
}
fn compressed(&self) -> bool {
matches!(self.options.style, OutputStyle::Compressed)
}
fn lookup(&self, name: &str) -> Option<Value> {
for scope in self.scopes.iter().rev() {
if let Some(v) = scope.borrow().get(name) {
return Some(v.clone());
}
}
None
}
fn capture_callable(&self, def: &Rc<Callable>) -> Rc<UserCallable> {
Rc::new(UserCallable {
def: Rc::clone(def),
env: self.scopes.clone(),
env_semi: self.scope_semi_global.clone(),
env_fns: self.functions.clone(),
env_mixins: self.mixins.clone(),
})
}
fn push_scope(&mut self, semi_global: bool) {
let effective = semi_global && self.scope_semi_global.last().copied().unwrap_or(false);
self.scopes.push(new_scope());
self.scope_semi_global.push(effective);
self.functions.push(new_fn_scope());
self.mixins.push(new_fn_scope());
}
fn push_scope_frame(&mut self, frame: HashMap<String, Value>) {
self.scopes.push(std::rc::Rc::new(std::cell::RefCell::new(frame)));
self.scope_semi_global.push(false);
self.functions.push(new_fn_scope());
self.mixins.push(new_fn_scope());
}
fn pop_scope(&mut self) {
self.scopes.pop();
self.scope_semi_global.pop();
self.functions.pop();
self.mixins.pop();
}
fn define_function(&mut self, name: &str, c: Rc<UserCallable>) {
if let Some(frame) = self.functions.last() {
frame.borrow_mut().insert(normalize_arg_name(name), c);
}
}
fn define_mixin(&mut self, name: &str, c: Rc<UserCallable>) {
if let Some(frame) = self.mixins.last() {
frame.borrow_mut().insert(normalize_arg_name(name), c);
}
}
fn lookup_function(&self, name: &str) -> Option<Rc<UserCallable>> {
let key = normalize_arg_name(name);
for frame in self.functions.iter().rev() {
if let Some(f) = frame.borrow().get(&key) {
return Some(Rc::clone(f));
}
}
None
}
fn lookup_mixin(&self, name: &str) -> Option<Rc<UserCallable>> {
let key = normalize_arg_name(name);
for frame in self.mixins.iter().rev() {
if let Some(m) = frame.borrow().get(&key) {
return Some(Rc::clone(m));
}
}
None
}
fn lookup_function_norm(&self, key: &str) -> Option<Rc<UserCallable>> {
for frame in self.functions.iter().rev() {
let frame = frame.borrow();
if let Some((_, f)) = frame.iter().find(|(k, _)| normalize_arg_name(k) == key) {
return Some(Rc::clone(f));
}
}
None
}
fn lookup_mixin_norm(&self, key: &str) -> Option<Rc<UserCallable>> {
for frame in self.mixins.iter().rev() {
let frame = frame.borrow();
if let Some((_, m)) = frame.iter().find(|(k, _)| normalize_arg_name(k) == key) {
return Some(Rc::clone(m));
}
}
None
}
fn assign(&mut self, name: &str, val: Value) {
if self.scopes.len() == 1 {
if let Some(g) = self.scopes.first_mut() {
g.borrow_mut().insert(name.to_string(), val);
}
return;
}
let mut index = None;
for (i, scope) in self.scopes.iter().enumerate().rev() {
if scope.borrow().contains_key(name) {
index = Some(i);
break;
}
}
let in_semi_global = self.scope_semi_global.last().copied().unwrap_or(false);
let target = match index {
Some(0) if !in_semi_global => self.scopes.len() - 1,
Some(i) => i,
None => self.scopes.len() - 1,
};
if let Some(scope) = self.scopes.get_mut(target) {
scope.borrow_mut().insert(name.to_string(), val);
}
}
fn apply_var(&mut self, v: &VarDecl) -> Result<(), Error> {
if let Some(ns) = &v.namespace {
return self.assign_module_var(ns, v);
}
if v.is_default
&& self.scopes.len() == 1
&& self.lookup(&v.name).is_none()
&& !is_private_member(&v.name)
&& self
.star_user_modules
.iter()
.filter(|m| m.var(&v.name).is_some())
.count()
> 1
{
return Err(Error::unpositioned(
"This variable is available from multiple global modules.",
));
}
if v.is_default && self.scopes.len() == 1 {
let key = normalize_var_name(&v.name);
if let Some((cfg_val, cfg_is_default)) = self.pending_config.get(&key).cloned() {
self.consumed_config.push(key);
let already_set = matches!(self.lookup(&v.name), Some(x) if !matches!(x, Value::Null));
if !(matches!(cfg_val, Value::Null) || cfg_is_default && already_set) {
if let Some(g) = self.scopes.first_mut() {
g.borrow_mut().insert(v.name.clone(), cfg_val);
}
return Ok(());
}
}
}
let val = self.eval_expr(&v.value)?;
if v.is_default {
if let Some(existing) = self.lookup(&v.name) {
if !matches!(existing, Value::Null) {
return Ok(());
}
}
}
if (self.scopes.len() == 1 || v.is_global) && !is_private_member(&v.name) {
if let Some(g) = self.scopes.first() {
if !g.borrow().contains_key(&v.name) {
let targets: Vec<Rc<Module>> = self
.star_user_modules
.iter()
.filter(|m| m.var(&v.name).is_some())
.cloned()
.collect();
if targets.len() == 1 {
if v.is_default {
if let Some(existing) = targets[0].var(&v.name) {
if !matches!(existing, Value::Null) {
return Ok(());
}
}
}
targets[0].vars.borrow_mut().insert(v.name.clone(), val);
return Ok(());
}
}
}
}
if v.is_global {
if let Some(g) = self.scopes.first_mut() {
g.borrow_mut().insert(v.name.clone(), val);
}
} else {
self.assign(&v.name, val);
}
Ok(())
}
fn assign_module_var(&mut self, ns: &str, v: &VarDecl) -> Result<(), Error> {
if is_private_member(&v.name) {
return Err(Error::unpositioned(
"Private members can't be accessed from outside their modules.",
));
}
let module = match self.used_user_modules.get(ns).cloned() {
Some(m) => m,
None => {
if self.used_modules.contains_key(ns) {
return Err(Error::unpositioned("Cannot modify built-in variable."));
}
return Err(Error::unpositioned(format!(
"There is no module with the namespace \"{ns}\"."
)));
}
};
let (target, name) = match module.var_write_origin(&v.name) {
Some((m, o)) => (m, o),
None => (Rc::clone(&module), v.name.clone()),
};
let exists = target.var(&name).is_some();
if !exists {
return Err(Error::unpositioned("Undefined variable."));
}
if v.is_default {
if let Some(existing) = target.var(&name) {
if !matches!(existing, Value::Null) {
return Ok(());
}
}
}
let val = self.eval_expr(&v.value)?.without_slash();
target.vars.borrow_mut().insert(name, val);
Ok(())
}
fn set_local(&mut self, name: &str, val: Value) {
if let Some(sc) = self.scopes.last_mut() {
sc.borrow_mut().insert(name.to_string(), val);
}
}
fn eval_for_number(&mut self, e: &Expr) -> Result<Number, Error> {
match self.eval_expr(e)? {
Value::Number(n) => Ok(n),
other => Err(Error::unpositioned(format!(
"{} is not a number.",
other.type_name()
))),
}
}
fn for_bounds(&mut self, from: &Expr, to: &Expr) -> Result<(i64, i64, String), Error> {
let start = self.eval_for_number(from)?;
let end = self.eval_for_number(to)?;
let end_value = if start.is_unitless() || end.is_unitless() {
end.value
} else {
match crate::value::convert_factor(end.unit(), start.unit()) {
Some(f) => end.value * f,
None => {
return Err(Error::unpositioned(format!(
"Expected {} to have unit {}.",
Value::Number(end.clone()).to_css(false),
start.unit(),
)))
}
}
};
let to_int = |v: f64, n: Number| -> Result<i64, Error> {
if (v - v.round()).abs() < 1e-11 {
Ok(v.round() as i64)
} else {
Err(Error::unpositioned(format!(
"{} is not an int.",
Value::Number(n).to_css(false)
)))
}
};
let start_i = to_int(start.value, start.clone())?;
let end_i = to_int(end_value, Number::with_unit(end_value, start.unit().to_string()))?;
Ok((start_i, end_i, start.unit().to_string()))
}
fn eval_each_items(&mut self, e: &Expr) -> Result<Vec<Value>, Error> {
match self.eval_expr(e)? {
Value::List(l) => Ok(l.items),
Value::Map(m) => Ok(m
.entries
.into_iter()
.map(|(k, v)| {
Value::List(List {
items: vec![k, v],
sep: ListSep::Space,
bracketed: false,
keywords: None,
})
})
.collect()),
Value::Null => Ok(Vec::new()),
other => Ok(vec![other]),
}
}
fn bind_each(&mut self, vars: &[String], item: Value) {
if vars.len() == 1 {
self.set_local(&vars[0], item);
return;
}
let elems: Vec<Value> = match item {
Value::List(l) => l.items,
other => vec![other],
};
for (i, v) in vars.iter().enumerate() {
let val = elems.get(i).cloned().unwrap_or(Value::Null);
self.set_local(v, val);
}
}
fn eval_call_args(&mut self, args: &[CallArg]) -> Result<EvaledArgs, Error> {
let mut explicit_pos = Vec::new();
let mut splat_pos = Vec::new();
let mut keyword: Vec<(String, Value)> = Vec::new();
let mut seen_named = false;
let mut rest_sep = ListSep::Comma;
let push_named = |keyword: &mut Vec<(String, Value)>, name: String, v: Value| -> Result<(), Error> {
let norm = normalize_arg_name(&name);
if keyword.iter().any(|(n, _)| normalize_arg_name(n) == norm) {
return Err(Error::unpositioned("Duplicate argument."));
}
keyword.push((name, v));
Ok(())
};
for a in args {
let v = self.eval_expr(&a.value)?;
if a.splat {
match v {
Value::Map(m) => {
for (k, val) in m.entries {
let key = match &k {
Value::Str(s) => s.text.clone(),
other => {
return Err(Error::unpositioned(format!(
"{} is not a string in $args.",
other.to_css(false)
)))
}
};
push_named(&mut keyword, key, val)?;
}
}
Value::List(l) => {
if !matches!(l.sep, ListSep::Undecided) {
rest_sep = l.sep;
}
splat_pos.extend(l.items);
if let Some(kw) = l.keywords {
for (k, val) in kw {
if let Value::Str(s) = k {
push_named(&mut keyword, s.text, val)?;
}
}
}
}
Value::Null => {}
other => splat_pos.push(other),
}
continue;
}
match &a.name {
Some(n) => {
push_named(&mut keyword, n.clone(), v)?;
seen_named = true;
}
None => {
if seen_named {
return Err(Error::unpositioned(
"Positional arguments must come before keyword arguments.",
));
}
explicit_pos.push(v);
}
}
}
explicit_pos.extend(splat_pos);
Ok((explicit_pos, keyword, rest_sep))
}
fn bind_args(
&mut self,
params: &ParamList,
args: &[CallArg],
name: &str,
) -> Result<HashMap<String, Value>, Error> {
let evaled = self.eval_call_args(args)?;
self.bind_evaled(params, evaled, name)
}
fn bind_evaled_into_scope(
&mut self,
params: &ParamList,
evaled: EvaledArgs,
name: &str,
) -> Result<(), Error> {
let (positional, keyword_vec, rest_sep) = evaled;
let mut keyword: HashMap<String, Value> = HashMap::default();
let mut keyword_order: Vec<(String, String)> = Vec::new();
for (n, v) in keyword_vec {
let norm = normalize_arg_name(&n);
if !keyword.contains_key(&norm) {
keyword_order.push((norm.clone(), n));
}
keyword.insert(norm, v);
}
let mut pos_iter = positional.into_iter();
for param in ¶ms.params {
let val = if let Some(v) = pos_iter.next() {
v
} else if let Some(v) = keyword.remove(&normalize_arg_name(¶m.name)) {
v
} else if let Some(def) = ¶m.default {
self.eval_expr(def)?
} else {
return Err(Error::unpositioned(format!("Missing argument ${}.", param.name)));
};
if let Some(sc) = self.scopes.last() {
sc.borrow_mut().insert(param.name.clone(), val);
}
}
if let Some(rest) = ¶ms.rest {
let remaining: Vec<Value> = pos_iter.collect();
let kw: Vec<(Value, Value)> = keyword_order
.iter()
.filter_map(|(norm, _)| {
keyword.remove(norm).map(|v| {
(
Value::Str(SassStr {
text: norm.clone(),
quoted: false,
}),
v,
)
})
})
.collect();
if let Some(sc) = self.scopes.last() {
sc.borrow_mut().insert(
rest.clone(),
Value::List(List {
items: remaining,
sep: rest_sep,
bracketed: false,
keywords: Some(kw),
}),
);
}
} else if pos_iter.next().is_some() {
return Err(Error::unpositioned(format!(
"{name} was passed too many arguments."
)));
}
if params.rest.is_none() && !keyword.is_empty() {
let leftover: Vec<&str> = keyword_order
.iter()
.filter(|(norm, _)| keyword.contains_key(norm))
.map(|(_, orig)| orig.as_str())
.collect();
if let Some((last, init)) = leftover.split_last() {
let msg = if init.is_empty() {
format!("No parameter named ${last}.")
} else {
let head = init
.iter()
.map(|n| format!("${n}"))
.collect::<Vec<_>>()
.join(", ");
format!("No parameters named {head} or ${last}.")
};
return Err(Error::unpositioned(msg));
}
}
Ok(())
}
fn bind_evaled(
&mut self,
params: &ParamList,
evaled: EvaledArgs,
name: &str,
) -> Result<HashMap<String, Value>, Error> {
let (positional, keyword_vec, rest_sep) = evaled;
let mut keyword: HashMap<String, Value> = HashMap::default();
let mut keyword_order: Vec<(String, String)> = Vec::new();
for (n, v) in keyword_vec {
let norm = normalize_arg_name(&n);
if !keyword.contains_key(&norm) {
keyword_order.push((norm.clone(), n));
}
keyword.insert(norm, v);
}
let mut frame = HashMap::default();
let mut pos_iter = positional.into_iter();
for param in ¶ms.params {
let val = if let Some(v) = pos_iter.next() {
v
} else if let Some(v) = keyword.remove(&normalize_arg_name(¶m.name)) {
v
} else if let Some(def) = ¶m.default {
self.eval_expr(def)?
} else {
return Err(Error::unpositioned(format!("Missing argument ${}.", param.name)));
};
frame.insert(param.name.clone(), val);
}
if let Some(rest) = ¶ms.rest {
let remaining: Vec<Value> = pos_iter.collect();
let kw: Vec<(Value, Value)> = keyword_order
.iter()
.filter_map(|(norm, _)| {
keyword.remove(norm).map(|v| {
(
Value::Str(SassStr {
text: norm.clone(),
quoted: false,
}),
v,
)
})
})
.collect();
frame.insert(
rest.clone(),
Value::List(List {
items: remaining,
sep: rest_sep,
bracketed: false,
keywords: Some(kw),
}),
);
} else if pos_iter.next().is_some() {
return Err(Error::unpositioned(format!(
"{name} was passed too many arguments."
)));
}
if params.rest.is_none() && !keyword.is_empty() {
let leftover: Vec<&str> = keyword_order
.iter()
.filter(|(norm, _)| keyword.contains_key(norm))
.map(|(_, orig)| orig.as_str())
.collect();
if let Some((last, init)) = leftover.split_last() {
let msg = if init.is_empty() {
format!("No parameter named ${last}.")
} else {
let head = init
.iter()
.map(|n| format!("${n}"))
.collect::<Vec<_>>()
.join(", ");
format!("No parameters named {head} or ${last}.")
};
return Err(Error::unpositioned(msg));
}
}
Ok(frame)
}
fn call_function(
&mut self,
func: &Rc<UserCallable>,
args: &[CallArg],
call: Option<(Pos, usize)>,
) -> Result<Value, Error> {
let evaled = self.eval_call_args(args)?;
let saved = call.map(|(pos, len)| self.enter_call(pos, len, &format!("{}()", func.def.name)));
let saved_scopes = std::mem::replace(&mut self.scopes, func.env.clone());
let saved_semi = std::mem::replace(&mut self.scope_semi_global, func.env_semi.clone());
let saved_fns = std::mem::replace(&mut self.functions, func.env_fns.clone());
let saved_mixins = std::mem::replace(&mut self.mixins, func.env_mixins.clone());
self.push_scope(false);
let result = self
.bind_evaled_into_scope(&func.def.params, evaled, &func.def.name)
.and_then(|()| {
self.in_mixin.push(false);
let r = self.run_fn_body(&func.def.body);
self.in_mixin.pop();
r
});
self.pop_scope();
self.scopes = saved_scopes;
self.scope_semi_global = saved_semi;
self.functions = saved_fns;
self.mixins = saved_mixins;
if let Some(saved) = saved {
self.leave_call(saved);
}
match result? {
Some(v) => Ok(v.without_slash()),
None => Err(Error::unpositioned(format!(
"Function {}() did not @return a value.",
func.def.name
))),
}
}
fn run_fn_body(&mut self, stmts: &[Stmt]) -> Result<Option<Value>, Error> {
for stmt in stmts {
match stmt {
Stmt::VarDecl(v) => self.apply_var(v)?,
Stmt::Comment(..) => {}
Stmt::Return(e) => return Ok(Some(self.eval_expr(e)?)),
Stmt::FunctionDef(c) => {
let captured = self.capture_callable(c);
self.define_function(&c.name, captured);
}
Stmt::If(branches) => {
for branch in branches {
let take = match &branch.cond {
None => true,
Some(c) => self.eval_expr(c)?.is_truthy(),
};
if take {
self.push_scope(true);
let result = self.run_fn_body(&branch.body);
self.pop_scope();
if let Some(v) = result? {
return Ok(Some(v));
}
break;
}
}
}
Stmt::For {
var,
from,
to,
inclusive,
body,
} => {
let (start_i, end_i, unit) = self.for_bounds(from, to)?;
self.push_scope(true);
let mut result = Ok(None);
for i in for_indices(start_i, end_i, *inclusive) {
self.set_local(var, Value::Number(Number::with_unit(i as f64, unit.clone())));
result = self.run_fn_body(body);
if matches!(result, Ok(None)) {
continue;
}
break;
}
self.pop_scope();
if let Some(v) = result? {
return Ok(Some(v));
}
}
Stmt::Each { vars, list, body } => {
let items = self.eval_each_items(list)?;
self.push_scope(true);
let mut result = Ok(None);
for item in items {
self.bind_each(vars, item);
result = self.run_fn_body(body);
if matches!(result, Ok(None)) {
continue;
}
break;
}
self.pop_scope();
if let Some(v) = result? {
return Ok(Some(v));
}
}
Stmt::While { cond, body } => {
self.push_scope(true);
let mut result: Result<Option<Value>, Error> = Ok(None);
let mut guard = 0u32;
loop {
match self.eval_expr(cond) {
Ok(v) if v.is_truthy() => {}
Ok(_) => break,
Err(e) => {
result = Err(e);
break;
}
}
result = self.run_fn_body(body);
if !matches!(result, Ok(None)) {
break;
}
guard += 1;
if guard >= 100_000 {
result = Err(Error::unpositioned("@while exceeded 100000 iterations"));
break;
}
}
self.pop_scope();
if let Some(v) = result? {
return Ok(Some(v));
}
}
Stmt::Warn { value, pos } => self.emit_warn(value, *pos)?,
Stmt::Debug { value, pos } => self.emit_debug(value, *pos)?,
Stmt::Error { value, pos, length } => {
return Err(self.build_error(value, *pos, *length));
}
_ => {
return Err(Error::unpositioned(
"only variable assignments, control flow and @return are allowed in a function.",
));
}
}
}
Ok(None)
}
#[allow(clippy::too_many_arguments)]
fn exec_include(
&mut self,
name: &str,
args: &[CallArg],
content: Option<Rc<Vec<Stmt>>>,
content_params: Option<Rc<ParamList>>,
module: Option<&str>,
pos: Pos,
parents: &[String],
sink: &mut Sink<'_>,
) -> Result<(), Error> {
if let Some(ns) = module {
if self.used_modules.get(ns).map(String::as_str) == Some("meta") {
if name == "apply" {
return self.exec_apply(args, content, content_params, parents, sink);
}
if name == "load-css" {
return self.exec_load_css(args, content, pos, parents, sink);
}
}
}
if let Some(ns) = module {
if let Some(target) = self.used_user_modules.get(ns).cloned() {
if is_private_member(name) {
return Err(Error::unpositioned(
"Private members can't be accessed from outside their modules.",
));
}
let mixin = target
.mixin(name)
.ok_or_else(|| Error::unpositioned("Undefined mixin."))?;
let exec = target.mixin_origin(name).unwrap_or(target);
return self.run_module_mixin(&exec, &mixin, args, content, content_params, parents, sink);
}
if !self.used_modules.contains_key(ns) {
return Err(Error::unpositioned(format!(
"There is no module with the namespace \"{ns}\"."
)));
}
return Err(Error::unpositioned("Undefined mixin."));
}
if self.lookup_mixin(name).is_none() && !self.star_user_modules.is_empty() && !is_private_member(name)
{
let hits: Vec<(Rc<Module>, Rc<UserCallable>)> = self
.star_user_modules
.iter()
.filter_map(|m| m.mixin(name).map(|mx| (Rc::clone(m), mx)))
.collect();
if hits.len() > 1 {
return Err(Error::unpositioned(
"This mixin is available from multiple global modules.",
));
}
if let Some((m, mx)) = hits.into_iter().next() {
return self.run_module_mixin(&m, &mx, args, content, content_params, parents, sink);
}
}
let mixin = self
.lookup_mixin(name)
.ok_or_else(|| Error::unpositioned(format!("Undefined mixin {name}.")))?;
if content.is_some() && !body_uses_content(&mixin.def.body) {
return Err(Error::unpositioned("Mixin doesn't accept a content block."));
}
let evaled = self.eval_call_args(args)?;
let content_block = content.map(|stmts| {
let snapshot = self.snapshot_env();
ContentBlock {
stmts,
params: content_params.clone(),
caller_env: Some(Box::new(snapshot)),
}
});
let saved_scopes = std::mem::replace(&mut self.scopes, mixin.env.clone());
let saved_semi = std::mem::replace(&mut self.scope_semi_global, mixin.env_semi.clone());
let saved_fns = std::mem::replace(&mut self.functions, mixin.env_fns.clone());
let saved_mixins = std::mem::replace(&mut self.mixins, mixin.env_mixins.clone());
self.push_scope(false);
let result = self
.bind_evaled_into_scope(&mixin.def.params, evaled, &mixin.def.name)
.and_then(|()| {
self.content_stack.push(content_block);
self.in_mixin.push(true);
let r = self.exec(&mixin.def.body, parents, sink);
self.in_mixin.pop();
self.content_stack.pop();
r
});
self.pop_scope();
self.scopes = saved_scopes;
self.scope_semi_global = saved_semi;
self.functions = saved_fns;
self.mixins = saved_mixins;
result
}
#[allow(clippy::too_many_arguments)]
fn run_module_mixin(
&mut self,
module: &Rc<Module>,
mixin: &Rc<UserCallable>,
args: &[CallArg],
content: Option<Rc<Vec<Stmt>>>,
content_params: Option<Rc<ParamList>>,
parents: &[String],
sink: &mut Sink<'_>,
) -> Result<(), Error> {
if content.is_some() && !body_uses_content(&mixin.def.body) {
return Err(Error::unpositioned("Mixin doesn't accept a content block."));
}
let evaled = self.eval_call_args(args)?;
let content_block = content.map(|stmts| {
let snapshot = self.snapshot_env();
ContentBlock {
stmts,
params: content_params.clone(),
caller_env: Some(Box::new(snapshot)),
}
});
let saved = self.enter_module(module);
let saved_file = self.enter_module_file(module);
let saved_scopes = std::mem::replace(&mut self.scopes, mixin.env.clone());
let saved_semi = std::mem::replace(&mut self.scope_semi_global, mixin.env_semi.clone());
let saved_fns = std::mem::replace(&mut self.functions, mixin.env_fns.clone());
let saved_mixins = std::mem::replace(&mut self.mixins, mixin.env_mixins.clone());
self.push_scope(false);
let result = self
.bind_evaled_into_scope(&mixin.def.params, evaled, &mixin.def.name)
.and_then(|()| {
self.content_stack.push(content_block);
let r = self.exec(&mixin.def.body, parents, sink);
self.content_stack.pop();
r
});
self.pop_scope();
self.scopes = saved_scopes;
self.scope_semi_global = saved_semi;
self.functions = saved_fns;
self.mixins = saved_mixins;
self.leave_module_file(saved_file);
self.leave_module(saved);
result
}
fn exec_apply(
&mut self,
args: &[CallArg],
content: Option<Rc<Vec<Stmt>>>,
content_params: Option<Rc<ParamList>>,
parents: &[String],
sink: &mut Sink<'_>,
) -> Result<(), Error> {
let (mut pos_args, mut named, _) = self.eval_call_args(args)?;
for v in &mut pos_args {
*v = std::mem::replace(v, Value::Null).without_slash();
}
for (_, v) in &mut named {
*v = std::mem::replace(v, Value::Null).without_slash();
}
let (mixin_val, rest_pos): (Value, Vec<Value>) = if !pos_args.is_empty() {
let mut iter = pos_args.into_iter();
let first = iter.next().unwrap_or(Value::Null);
(first, iter.collect())
} else if let Some(idx) = named.iter().position(|(n, _)| n == "mixin") {
(named.remove(idx).1, Vec::new())
} else {
return Err(Error::unpositioned("Missing argument $mixin."));
};
let rest_named: Vec<(String, Value)> = named.into_iter().filter(|(n, _)| n != "mixin").collect();
let mixin = match mixin_val {
Value::Mixin(m) => m,
other => {
return Err(Error::unpositioned(format!(
"$mixin: {} is not a mixin reference.",
other.to_css(false)
)))
}
};
self.invoke_mixin_ref(
&mixin,
rest_pos,
rest_named,
content,
content_params,
parents,
sink,
)
}
fn exec_load_css(
&mut self,
args: &[CallArg],
content: Option<Rc<Vec<Stmt>>>,
pos: Pos,
parents: &[String],
sink: &mut Sink<'_>,
) -> Result<(), Error> {
if content.is_some() {
return Err(Error::at(
"Mixin doesn't accept a content block.".to_string(),
pos,
));
}
let (pos_args, named, _) = self.eval_call_args(args)?;
let mut iter = pos_args.into_iter();
let mut url_val = iter.next();
let mut with_val = iter.next();
if iter.next().is_some() {
return Err(Error::at(
"Only 2 arguments allowed, but 3 were passed.".to_string(),
pos,
));
}
for (n, v) in named {
match n.as_str() {
"url" => url_val = Some(v),
"with" => with_val = Some(v),
other => return Err(Error::at(format!("No argument named ${other}."), pos)),
}
}
let url = match url_val {
Some(Value::Str(s)) => s.text,
Some(other) => {
return Err(Error::at(
format!("$url: {} is not a string.", other.to_css(false)),
pos,
))
}
None => return Err(Error::at("Missing argument $url.".to_string(), pos)),
};
let mut config: HashMap<String, (Value, bool)> = HashMap::default();
match with_val.take() {
None => {}
Some(Value::List(l)) if l.items.is_empty() => {}
Some(Value::Map(m)) => {
for (k, v) in m.entries {
let key = match k {
Value::Str(s) => normalize_var_name(&s.text),
other => {
return Err(Error::at(
format!("$with key: {} is not a string.", other.to_css(false)),
pos,
))
}
};
if config.contains_key(&key) {
return Err(Error::at(
format!("The variable ${key} was configured twice."),
pos,
));
}
config.insert(key, (v.without_slash(), false));
}
}
Some(other) => {
return Err(Error::at(
format!("$with: {} is not a map.", other.to_css(false)),
pos,
))
}
}
if let Some(m) = url.strip_prefix("sass:") {
if crate::builtins::is_module(m) {
if !config.is_empty() {
return Err(Error::at(
format!("Built-in module sass:{m} can't be configured."),
pos,
));
}
return Ok(());
}
return Err(Error::at("Can't find stylesheet to import.".to_string(), pos));
}
let conf_keys: Vec<String> = config.keys().cloned().collect();
let mut buf: Vec<OutNode> = Vec::new();
let consumed = {
let mut module_sink = Sink::Top(&mut buf);
let config_id = if config.is_empty() {
0
} else {
self.fresh_config_id()
};
let (_module, consumed) =
self.load_module(&url, config, config_id, pos, parents, true, &mut module_sink)?;
consumed
};
if conf_keys.iter().any(|k| !consumed.contains(k)) {
return Err(Error::at(
"This variable was not declared with !default in the @used module.".to_string(),
pos,
));
}
splice_nodes(sink, buf);
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn invoke_mixin_ref(
&mut self,
mixin: &SassMixin,
pos_args: Vec<Value>,
named: Vec<(String, Value)>,
content: Option<Rc<Vec<Stmt>>>,
content_params: Option<Rc<ParamList>>,
parents: &[String],
sink: &mut Sink<'_>,
) -> Result<(), Error> {
let callable = match &mixin.user {
Some(any) => match Rc::clone(any).downcast::<UserCallable>() {
Ok(c) => c,
Err(_) => return Err(Error::unpositioned("Undefined mixin.")),
},
None => {
if content.is_some() {
return Err(Error::unpositioned("Mixin doesn't accept a content block."));
}
return Err(Error::unpositioned("Undefined mixin."));
}
};
if content.is_some() && !body_uses_content(&callable.def.body) {
return Err(Error::unpositioned("Mixin doesn't accept a content block."));
}
let content_block = content.map(|stmts| {
let snapshot = self.snapshot_env();
ContentBlock {
stmts,
params: content_params.clone(),
caller_env: Some(Box::new(snapshot)),
}
});
let module = mixin
.module
.as_ref()
.and_then(|m| Rc::clone(m).downcast::<Module>().ok());
let saved = module.as_ref().map(|m| self.enter_module(m));
let saved_scopes = std::mem::replace(&mut self.scopes, callable.env.clone());
let saved_semi = std::mem::replace(&mut self.scope_semi_global, callable.env_semi.clone());
let saved_fns = std::mem::replace(&mut self.functions, callable.env_fns.clone());
let saved_mixins = std::mem::replace(&mut self.mixins, callable.env_mixins.clone());
self.push_scope(false);
let result = self
.bind_evaled_into_scope(
&callable.def.params,
(pos_args, named, ListSep::Comma),
&callable.def.name,
)
.and_then(|()| {
self.content_stack.push(content_block);
self.in_mixin.push(true);
let r = self.exec(&callable.def.body, parents, sink);
self.in_mixin.pop();
self.content_stack.pop();
r
});
self.pop_scope();
self.scopes = saved_scopes;
self.scope_semi_global = saved_semi;
self.functions = saved_fns;
self.mixins = saved_mixins;
if let Some(saved) = saved {
self.leave_module(saved);
}
result
}
fn exec_content(
&mut self,
args: &[CallArg],
parents: &[String],
sink: &mut Sink<'_>,
) -> Result<(), Error> {
let (stmts, params, caller_env) = match self.content_stack.last() {
Some(Some(block)) => (
Rc::clone(&block.stmts),
block.params.clone(),
block.caller_env.as_ref().map(|e| (**e).clone()),
),
_ => return Ok(()),
};
let frame = match ¶ms {
Some(p) => Some(self.bind_args(p, args, "@content")?),
None => {
if !args.is_empty() {
let n = args.len();
let verb = if n == 1 { "was" } else { "were" };
return Err(Error::unpositioned(format!(
"Only 0 arguments allowed, but {n} {verb} passed."
)));
}
None
}
};
let restore = caller_env.map(|env| self.install_env(env));
match frame {
Some(frame) => self.push_scope_frame(frame),
None => self.push_scope(false),
}
let running = self.content_stack.pop();
let result = self.exec(&stmts, parents, sink);
if let Some(top) = running {
self.content_stack.push(top);
}
self.pop_scope();
if let Some(restore) = restore {
self.leave_module(restore);
}
result
}
fn install_env(&mut self, env: SavedModuleEnv) -> SavedModuleEnv {
SavedModuleEnv {
scopes: std::mem::replace(&mut self.scopes, env.scopes),
scope_semi_global: std::mem::replace(&mut self.scope_semi_global, env.scope_semi_global),
functions: std::mem::replace(&mut self.functions, env.functions),
mixins: std::mem::replace(&mut self.mixins, env.mixins),
used_modules: std::mem::replace(&mut self.used_modules, env.used_modules),
star_modules: std::mem::replace(&mut self.star_modules, env.star_modules),
used_user_modules: std::mem::replace(&mut self.used_user_modules, env.used_user_modules),
star_user_modules: std::mem::replace(&mut self.star_user_modules, env.star_user_modules),
write_back: None,
}
}
fn snapshot_env(&self) -> SavedModuleEnv {
SavedModuleEnv {
scopes: self.scopes.clone(),
scope_semi_global: self.scope_semi_global.clone(),
functions: self.functions.clone(),
mixins: self.mixins.clone(),
used_modules: self.used_modules.clone(),
star_modules: self.star_modules.clone(),
used_user_modules: self.used_user_modules.clone(),
star_user_modules: self.star_user_modules.clone(),
write_back: None,
}
}
#[allow(clippy::too_many_arguments)]
fn exec_use(
&mut self,
url: &str,
namespace: Option<&str>,
star: bool,
config: &[crate::ast::ConfigEntry],
pos: Pos,
parents: &[String],
sink: &mut Sink<'_>,
) -> Result<(), Error> {
if let Some(m) = url.strip_prefix("sass:") {
if !crate::builtins::is_module(m) {
return Err(Error::at("Can't find stylesheet to import.".to_string(), pos));
}
if !config.is_empty() {
return Err(Error::at(
"Built-in modules can't be configured.".to_string(),
pos,
));
}
let module = m.to_string();
if star {
if !self.star_modules.contains(&module) {
self.star_modules.push(module);
}
return Ok(());
}
let ns = namespace.unwrap_or(&module).to_string();
self.check_namespace_free(&ns, pos)?;
self.used_modules.insert(ns, module);
return Ok(());
}
let conf = self.eval_config(config)?;
let conf_keys: Vec<String> = conf.keys().cloned().collect();
let config_id = if conf.is_empty() {
0
} else {
self.fresh_config_id()
};
let (module, consumed) = self.load_module(url, conf, config_id, pos, parents, false, sink)?;
if conf_keys.iter().any(|k| !consumed.contains(k)) {
return Err(Error::at(
"This variable was not declared with !default in the @used module.".to_string(),
pos,
));
}
if star {
if let Some(g) = self.scopes.first() {
for name in module.vars.borrow().keys() {
if !is_private_member(name) && g.borrow().contains_key(name) {
return Err(Error::at(
format!(
"This module and the new module both define a variable named \"${name}\"."
),
pos,
));
}
}
}
let ptr = Rc::as_ptr(&module);
if !self.star_user_modules.iter().any(|m| Rc::as_ptr(m) == ptr) {
self.star_user_modules.push(module);
}
return Ok(());
}
let ns = match namespace {
Some(n) => n.to_string(),
None => default_namespace(url, pos)?,
};
self.check_namespace_free(&ns, pos)?;
self.used_user_modules.insert(ns, module);
Ok(())
}
fn check_namespace_free(&self, ns: &str, pos: Pos) -> Result<(), Error> {
if self.used_modules.contains_key(ns) || self.used_user_modules.contains_key(ns) {
return Err(Error::at(
format!("There's already a module with namespace \"{ns}\"."),
pos,
));
}
Ok(())
}
fn eval_config(
&mut self,
config: &[crate::ast::ConfigEntry],
) -> Result<HashMap<String, (Value, bool)>, Error> {
let mut map = HashMap::default();
for entry in config {
let v = self.eval_expr(&entry.value)?.without_slash();
let key = normalize_var_name(&entry.name);
if map.contains_key(&key) {
return Err(Error::unpositioned(format!(
"The variable ${} was configured twice.",
entry.name
)));
}
map.insert(key, (v, entry.is_default));
}
Ok(map)
}
fn fresh_config_id(&self) -> usize {
let n = self.config_id_counter.get() + 1;
self.config_id_counter.set(n);
n
}
fn subtree_css(&self, key: &str) -> Vec<OutNode> {
let mut out = Vec::new();
let mut visited = std::collections::HashSet::new();
self.walk_subtree(key, &mut visited, &mut out);
trim_leading_blanks(&mut out);
out
}
fn walk_subtree(
&self,
key: &str,
visited: &mut std::collections::HashSet<String>,
out: &mut Vec<OutNode>,
) {
if !visited.insert(key.to_string()) {
return;
}
let deps = self
.module_dep_order
.borrow()
.get(key)
.cloned()
.unwrap_or_default();
for d in deps {
self.walk_subtree(&d, visited, out);
}
if let Some(m) = self.module_cache.borrow().get(key) {
for n in &m.css {
if let OutNode::ModuleScope { key: k, .. } = n {
if !k.contains("#copy") && !k.contains("#import") {
continue;
}
}
out.push(n.clone());
}
}
}
fn register_load_css_copy(&self, key: &str) -> String {
let n = self.copy_counter.get() + 1;
self.copy_counter.set(n);
let copy_key = format!("{key}#copy{n}");
self.module_deps
.borrow_mut()
.entry(self.current_module.clone())
.or_default()
.insert(copy_key.clone());
self.load_css_copies
.borrow_mut()
.push((copy_key.clone(), key.to_string()));
copy_key
}
fn clone_module_css(&mut self, key: &str) -> (String, Vec<OutNode>) {
let state = self.import_clone.take();
if let Some((k, mut visited)) = state {
let copy_key = k.clone();
self.module_deps
.borrow_mut()
.entry(self.current_module.clone())
.or_default()
.insert(copy_key.clone());
self.load_css_copies
.borrow_mut()
.push((copy_key.clone(), key.to_string()));
let mut out = Vec::new();
self.walk_subtree(key, &mut visited, &mut out);
trim_leading_blanks(&mut out);
self.import_clone = Some((k, visited));
(copy_key, out)
} else {
let copy_key = self.register_load_css_copy(key);
(copy_key, self.subtree_css(key))
}
}
#[allow(clippy::too_many_arguments)]
fn load_module(
&mut self,
url: &str,
config: HashMap<String, (Value, bool)>,
config_id: usize,
pos: Pos,
parents: &[String],
force_reemit: bool,
sink: &mut Sink<'_>,
) -> Result<(Rc<Module>, Vec<String>), Error> {
let force_reemit = force_reemit || self.import_clone.is_some();
let importer = self.options.importer;
let saved = crate::arena::pause();
let base = self.current_file_dir.clone();
let resolved = importer.and_then(|imp| imp.resolve_module_with_syntax_in(url, base.as_deref()));
crate::arena::resume(saved);
let (key, src, syntax) = match resolved {
Some(triple) => triple,
None => {
return Err(Error::at("Can't find stylesheet to import.".to_string(), pos));
}
};
let cached = self.module_cache.borrow().get(&key).cloned();
if let Some(existing) = cached {
let consumed: Vec<String> = config
.keys()
.filter(|k| existing.var(k).is_some())
.cloned()
.collect();
if !consumed.is_empty() {
if self.config_is_implicit {
self.module_deps
.borrow_mut()
.entry(self.current_module.clone())
.or_default()
.insert(key.clone());
{
let mut ord = self.module_dep_order.borrow_mut();
let v = ord.entry(self.current_module.clone()).or_default();
if !v.contains(&key) {
v.push(key.clone());
}
}
splice_nodes(
sink,
vec![OutNode::ModuleScope {
key: key.clone(),
nodes: reparent_nodes(existing.css.clone(), parents),
}],
);
return Ok((existing, consumed));
}
if config_id != 0 && existing.config_origin.get() == config_id {
return Ok((existing, consumed));
}
return Err(Error::at(
"This module was already loaded, so it can't be configured using \"with\".".to_string(),
pos,
));
}
if !force_reemit {
self.module_deps
.borrow_mut()
.entry(self.current_module.clone())
.or_default()
.insert(key.clone());
let mut ord = self.module_dep_order.borrow_mut();
let v = ord.entry(self.current_module.clone()).or_default();
if !v.contains(&key) {
v.push(key.clone());
}
}
if force_reemit {
let (copy_key, nodes) = self.clone_module_css(&key);
splice_nodes(
sink,
vec![OutNode::ModuleScope {
key: copy_key,
nodes: reparent_nodes(nodes, parents),
}],
);
} else if !existing.emitted_main.get() {
existing.emitted_main.set(true);
splice_nodes(
sink,
vec![OutNode::ModuleScope {
key: key.clone(),
nodes: reparent_nodes(existing.css.clone(), parents),
}],
);
}
return Ok((existing, Vec::new()));
}
if self.loading.iter().any(|p| p == &key) {
return Err(Error::at(
"Module loop: this module is already being loaded.".to_string(),
pos,
));
}
let sheet = parse_with_syntax(&src, syntax)?;
let diag_url = self.module_diag_url(url, &key);
if self.diag_enabled() {
self.file_sources
.borrow_mut()
.insert(diag_url.clone(), Rc::from(src.as_str()));
}
let is_css = matches!(syntax, Syntax::Css);
if !force_reemit {
self.module_deps
.borrow_mut()
.entry(self.current_module.clone())
.or_default()
.insert(key.clone());
let mut ord = self.module_dep_order.borrow_mut();
let v = ord.entry(self.current_module.clone()).or_default();
if !v.contains(&key) {
v.push(key.clone());
}
}
let mut css_buf: Vec<OutNode> = Vec::new();
let (mut module, consumed) = {
let mut buf_sink = Sink::Top(&mut css_buf);
self.eval_module(
&key,
&diag_url,
&sheet,
config,
config_id,
pos,
&mut buf_sink,
is_css,
)?
};
module.css = css_buf.clone();
let module = Rc::new(module);
self.module_cache
.borrow_mut()
.insert(key.clone(), Rc::clone(&module));
if force_reemit {
let (copy_key, nodes) = self.clone_module_css(&key);
splice_nodes(
sink,
vec![OutNode::ModuleScope {
key: copy_key,
nodes: reparent_nodes(nodes, parents),
}],
);
} else {
module.emitted_main.set(true);
splice_nodes(
sink,
vec![OutNode::ModuleScope {
key: key.clone(),
nodes: reparent_nodes(css_buf, parents),
}],
);
}
Ok((module, consumed))
}
fn exec_css(&mut self, stmts: &[Stmt], parents: &[String], sink: &mut Sink<'_>) -> Result<(), Error> {
let saved = std::mem::replace(&mut self.in_plain_css, true);
let result = self.exec_css_inner(stmts, parents, sink);
self.in_plain_css = saved;
result
}
fn exec_css_inner(
&mut self,
stmts: &[Stmt],
parents: &[String],
sink: &mut Sink<'_>,
) -> Result<(), Error> {
if !parents.is_empty() {
let mut preserved: Vec<OutItem> = Vec::new();
for stmt in stmts {
if let Stmt::Rule(r) = stmt {
let own = self.css_selectors(&r.selector, true)?;
if own.iter().any(|s| part_has_parent_ref(s)) {
let inner = self.css_body(&r.body)?;
if !inner.is_empty() {
preserved.push(OutItem::NestedRule {
selectors: own,
items: inner,
});
}
}
}
}
if !preserved.is_empty() {
sink.push_at_rule(OutNode::Rule {
selectors: parents.to_vec(),
linebreaks: Vec::new(),
items: preserved,
lines: SrcLines::default(),
extend_base: usize::MAX,
});
}
}
for stmt in stmts {
match stmt {
Stmt::Rule(r) => {
let own = self.css_selectors(&r.selector, true)?;
if !parents.is_empty() && own.iter().any(|s| part_has_parent_ref(s)) {
continue;
}
let selectors: Vec<String> = if parents.is_empty() {
own
} else {
parents
.iter()
.flat_map(|p| own.iter().map(move |s| format!("{p} {s}")))
.collect()
};
let (items, bubbled) = self.css_rule_children(&r.body, &selectors)?;
if !items.is_empty() {
sink.push_at_rule(OutNode::Rule {
selectors,
linebreaks: Vec::new(),
items,
lines: self.stamp(SrcLines {
file: 0,
start: r.brace_line,
end: r.end_line,
col: 0,
}),
extend_base: usize::MAX,
});
}
for node in bubbled {
sink.push_at_rule(node);
}
}
Stmt::Comment(c, lines) => {
let text = self.eval_template(c)?;
let lines = self.stamp(*lines);
sink.push_at_rule(OutNode::Comment(text, lines));
}
Stmt::Import(args) => {
for arg in args {
let text = match arg {
ImportArg::Css { url, modifiers } => self.serialize_css_import(url, modifiers)?,
ImportArg::Sass { path, .. } => format!("\"{path}\""),
};
sink.push_at_rule(OutNode::Raw(format!("@import {text};")));
}
}
Stmt::Media { query, body, lines } => {
let queries = self.resolve_media_queries(query)?;
let prelude = serialize_media_queries(&queries);
let out_body = self.css_at_body(body)?;
if !out_body.is_empty() {
let lines = self.stamp(*lines);
sink.push_at_rule(OutNode::AtRule {
name: "media".to_string(),
prelude,
body: out_body,
has_block: true,
lines,
});
}
}
Stmt::Supports { condition, body } => {
let prelude = self.serialize_supports_condition(condition)?;
let out_body = self.css_at_body(body)?;
if !out_body.is_empty() {
sink.push_at_rule(OutNode::AtRule {
name: "supports".to_string(),
prelude,
body: out_body,
has_block: true,
lines: SrcLines::default(),
});
}
}
Stmt::AtRule {
name,
prelude,
body,
lines,
} => {
let prelude_s = self.eval_template(prelude)?.trim().to_string();
let lines = self.stamp(*lines);
match body {
None => sink.push_at_rule(OutNode::AtRule {
name: name.clone(),
prelude: prelude_s,
body: Vec::new(),
has_block: false,
lines,
}),
Some(b) => {
let out_body = self.css_at_body(b)?;
sink.push_at_rule(OutNode::AtRule {
name: name.clone(),
prelude: prelude_s,
body: out_body,
has_block: true,
lines,
});
}
}
}
Stmt::Keyframes {
name,
prelude,
body,
lines,
} => {
let prelude_s = self.eval_template(prelude)?.trim().to_string();
let out_body = self.css_at_body(body)?;
let lines = self.stamp(*lines);
sink.push_at_rule(OutNode::AtRule {
name: name.clone(),
prelude: prelude_s,
body: out_body,
has_block: true,
lines,
});
}
Stmt::CssCustomAtRule { name, prelude, body } => {
self.eval_css_custom_at_rule(name, prelude, body, sink)?;
}
_ => {}
}
}
Ok(())
}
fn css_at_body(&mut self, stmts: &[Stmt]) -> Result<Vec<OutNode>, Error> {
let mut out: Vec<OutNode> = Vec::new();
for stmt in stmts {
match stmt {
Stmt::Rule(r) => {
let selectors = self.css_selectors(&r.selector, false)?;
let (items, bubbled) = self.css_rule_children(&r.body, &selectors)?;
if !items.is_empty() {
out.push(OutNode::Rule {
selectors,
linebreaks: Vec::new(),
items,
lines: self.stamp(SrcLines {
file: 0,
start: r.brace_line,
end: r.end_line,
col: 0,
}),
extend_base: usize::MAX,
});
}
out.extend(bubbled);
}
Stmt::Decl(d) => {
let prop = self.eval_template(&d.property)?.trim().to_string();
let value = self.eval_expr(&d.value)?.to_css(false);
out.push(OutNode::AtDecl {
prop,
value,
important: d.important,
custom: false,
lines: self.stamp(SrcLines {
file: 0,
start: d.pos.line as u32,
end: d.end_line,
col: 0,
}),
});
}
Stmt::CustomDecl(d) => {
let prop = self.eval_template(&d.property)?.trim().to_string();
let value = self.eval_template(&d.value)?;
out.push(OutNode::AtDecl {
prop,
value,
important: false,
custom: true,
lines: self.stamp(SrcLines {
file: 0,
start: d.pos.line as u32,
end: d.end_line,
col: 0,
}),
});
}
Stmt::Comment(c, lines) => {
let text = self.eval_template(c)?;
let lines = self.stamp(*lines);
out.push(OutNode::Comment(text, lines));
}
Stmt::Media { query, body, lines } => {
let queries = self.resolve_media_queries(query)?;
let prelude = serialize_media_queries(&queries);
let inner = self.css_at_body(body)?;
if !inner.is_empty() {
let lines = self.stamp(*lines);
out.push(OutNode::AtRule {
name: "media".to_string(),
prelude,
body: inner,
has_block: true,
lines,
});
}
}
Stmt::Supports { condition, body } => {
let prelude = self.serialize_supports_condition(condition)?;
let inner = self.css_at_body(body)?;
if !inner.is_empty() {
out.push(OutNode::AtRule {
name: "supports".to_string(),
prelude,
body: inner,
has_block: true,
lines: SrcLines::default(),
});
}
}
Stmt::AtRule {
name,
prelude,
body,
lines,
} => {
let prelude_s = self.eval_template(prelude)?.trim().to_string();
let lines = self.stamp(*lines);
match body {
None => out.push(OutNode::AtRule {
name: name.clone(),
prelude: prelude_s,
body: Vec::new(),
has_block: false,
lines,
}),
Some(b) => {
let inner = self.css_at_body(b)?;
out.push(OutNode::AtRule {
name: name.clone(),
prelude: prelude_s,
body: inner,
has_block: true,
lines,
});
}
}
}
Stmt::Import(args) => {
for arg in args {
let text = match arg {
ImportArg::Css { url, modifiers } => self.serialize_css_import(url, modifiers)?,
ImportArg::Sass { path, .. } => format!("\"{path}\""),
};
out.push(OutNode::Raw(format!("@import {text};")));
}
}
_ => {}
}
}
Ok(out)
}
#[allow(clippy::type_complexity)]
fn css_rule_children(
&mut self,
stmts: &[Stmt],
parent_selectors: &[String],
) -> Result<(Vec<OutItem>, Vec<OutNode>), Error> {
let mut items = Vec::new();
let mut bubbled: Vec<OutNode> = Vec::new();
let bubble = |name: &str, prelude: String, inner: Vec<OutItem>, bubbled: &mut Vec<OutNode>| {
if inner.is_empty() {
return;
}
bubbled.push(OutNode::AtRule {
name: name.to_string(),
prelude,
body: vec![OutNode::Rule {
selectors: parent_selectors.to_vec(),
linebreaks: Vec::new(),
items: inner,
lines: SrcLines::default(),
extend_base: usize::MAX,
}],
has_block: true,
lines: SrcLines::default(),
});
};
for stmt in stmts {
match stmt {
Stmt::Media {
query,
body,
lines: _,
} => {
let queries = self.resolve_media_queries(query)?;
let prelude = serialize_media_queries(&queries);
let inner = self.css_body(body)?;
bubble("media", prelude, inner, &mut bubbled);
}
Stmt::Supports { condition, body } => {
let prelude = self.serialize_supports_condition(condition)?;
let inner = self.css_body(body)?;
bubble("supports", prelude, inner, &mut bubbled);
}
Stmt::AtRule {
name,
prelude,
body: Some(b),
..
} => {
let prelude_s = self.eval_template(prelude)?.trim().to_string();
let inner = self.css_body(b)?;
bubble(name, prelude_s, inner, &mut bubbled);
}
other => self.css_body_stmt(other, &mut items)?,
}
}
Ok((items, bubbled))
}
fn css_selectors(&mut self, sel: &[crate::ast::TplPiece], top_level: bool) -> Result<Vec<String>, Error> {
let s = self.eval_template(sel)?;
let parts: Vec<String> = split_commas(&s)
.into_iter()
.map(|p| p.trim().to_string())
.filter(|p| !p.is_empty())
.collect();
for p in &parts {
validate_plain_css_selector(p, top_level)?;
}
Ok(parts)
}
fn css_body(&mut self, stmts: &[Stmt]) -> Result<Vec<OutItem>, Error> {
let mut items = Vec::new();
for stmt in stmts {
self.css_body_stmt(stmt, &mut items)?;
}
Ok(items)
}
fn css_body_stmt(&mut self, stmt: &Stmt, items: &mut Vec<OutItem>) -> Result<(), Error> {
match stmt {
Stmt::Decl(d) => {
let prop = self.eval_template(&d.property)?.trim().to_string();
let value = self.eval_expr(&d.value)?.to_css(false);
items.push(OutItem::Decl {
prop,
value,
important: d.important,
custom: false,
lines: self.stamp(SrcLines {
file: 0,
start: d.pos.line as u32,
end: d.end_line,
col: 0,
}),
});
}
Stmt::CustomDecl(d) => {
let prop = self.eval_template(&d.property)?.trim().to_string();
let value = self.eval_template(&d.value)?;
items.push(OutItem::Decl {
prop,
value,
important: false,
custom: true,
lines: self.stamp(SrcLines {
file: 0,
start: d.pos.line as u32,
end: d.end_line,
col: 0,
}),
});
}
Stmt::Rule(r) => {
let selectors = self.css_selectors(&r.selector, false)?;
let inner = self.css_body(&r.body)?;
if !inner.is_empty() {
items.push(OutItem::NestedRule {
selectors,
items: inner,
});
}
}
Stmt::Comment(c, lines) => {
let text = self.eval_template(c)?;
let lines = self.stamp(*lines);
items.push(OutItem::Comment(text, lines));
}
Stmt::Import(args) => {
for arg in args {
let prelude = match arg {
ImportArg::Css { url, modifiers } => self.serialize_css_import(url, modifiers)?,
ImportArg::Sass { path, .. } => format!("\"{path}\""),
};
items.push(OutItem::ChildlessAtRule {
name: "import".to_string(),
prelude,
lines: SrcLines::default(),
});
}
}
Stmt::Media {
query,
body,
lines: _,
} => {
let queries = self.resolve_media_queries(query)?;
let prelude = serialize_media_queries(&queries);
let inner = self.css_body(body)?;
if !inner.is_empty() {
items.push(OutItem::NestedAtRule {
name: "media".to_string(),
prelude,
items: inner,
});
}
}
Stmt::Supports { condition, body } => {
let prelude = self.serialize_supports_condition(condition)?;
let inner = self.css_body(body)?;
if !inner.is_empty() {
items.push(OutItem::NestedAtRule {
name: "supports".to_string(),
prelude,
items: inner,
});
}
}
Stmt::AtRule {
name,
prelude,
body,
lines,
} => {
let prelude_s = self.eval_template(prelude)?.trim().to_string();
match body {
None => {
let lines = self.stamp(*lines);
items.push(OutItem::ChildlessAtRule {
name: name.clone(),
prelude: prelude_s,
lines,
});
}
Some(b) => {
let inner = self.css_body(b)?;
if !inner.is_empty() {
items.push(OutItem::NestedAtRule {
name: name.clone(),
prelude: prelude_s,
items: inner,
});
}
}
}
}
_ => {}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn eval_module(
&mut self,
key: &str,
diag_url: &str,
sheet: &Stylesheet,
config: HashMap<String, (Value, bool)>,
config_id: usize,
pos: Pos,
sink: &mut Sink<'_>,
css: bool,
) -> Result<(Module, Vec<String>), Error> {
let module_source = self.source_for(diag_url);
let module_dir = dirname_of(key);
let saved_dir = std::mem::replace(&mut self.current_file_dir, module_dir);
let saved_url = std::mem::replace(&mut self.current_url, diag_url.to_string());
self.current_url_stamp = 0;
let saved_source = std::mem::replace(&mut self.current_source, module_source);
let saved_scopes = std::mem::replace(&mut self.scopes, vec![new_scope()]);
let saved_semi = std::mem::replace(&mut self.scope_semi_global, vec![true]);
let saved_funcs = std::mem::replace(&mut self.functions, vec![new_fn_scope()]);
let saved_mixins = std::mem::replace(&mut self.mixins, vec![new_fn_scope()]);
let saved_used = std::mem::take(&mut self.used_modules);
let saved_star = std::mem::take(&mut self.star_modules);
let saved_used_user = std::mem::take(&mut self.used_user_modules);
let saved_star_user = std::mem::take(&mut self.star_user_modules);
let saved_fwd = std::mem::take(&mut self.forwarded);
let saved_config = std::mem::replace(&mut self.pending_config, config);
let saved_config_id = std::mem::replace(&mut self.pending_config_id, config_id);
let saved_consumed = std::mem::take(&mut self.consumed_config);
let saved_selector = self.current_selector.take();
let saved_module = std::mem::replace(&mut self.current_module, key.to_string());
self.loading.push(key.to_string());
if !css {
let mut slots: Vec<String> = Vec::new();
collect_global_var_decls(&sheet.stmts, &mut slots);
if let Some(g) = self.scopes.first() {
let mut g = g.borrow_mut();
for name in slots {
g.entry(name).or_insert(Value::Null);
}
}
}
let result = if css {
self.exec_css(&sheet.stmts, &[], sink)
} else {
self.exec(&sheet.stmts, &[], sink)
};
self.loading.pop();
let vars_scope = std::mem::take(&mut self.scopes)
.into_iter()
.next()
.unwrap_or_else(new_scope);
let functions = std::mem::take(&mut self.functions)
.into_iter()
.next()
.unwrap_or_else(new_fn_scope);
let mixins = std::mem::take(&mut self.mixins)
.into_iter()
.next()
.unwrap_or_else(new_fn_scope);
let used_user_modules = std::mem::take(&mut self.used_user_modules);
let star_user_modules = std::mem::take(&mut self.star_user_modules);
let used_builtin_modules = std::mem::take(&mut self.used_modules);
let star_builtin_modules = std::mem::take(&mut self.star_modules);
let forwarded = std::mem::take(&mut self.forwarded);
let consumed = std::mem::take(&mut self.consumed_config);
self.scopes = saved_scopes;
self.scope_semi_global = saved_semi;
self.functions = saved_funcs;
self.mixins = saved_mixins;
self.used_modules = saved_used;
self.star_modules = saved_star;
self.used_user_modules = saved_used_user;
self.star_user_modules = saved_star_user;
self.forwarded = saved_fwd;
self.pending_config = saved_config;
self.pending_config_id = saved_config_id;
self.consumed_config = saved_consumed;
self.current_selector = saved_selector;
self.current_module = saved_module;
self.current_file_dir = saved_dir;
self.current_url = saved_url;
self.current_url_stamp = 0;
self.current_source = saved_source;
result?;
let _ = pos;
let mut var_origins: HashMap<String, (Rc<Module>, String)> = HashMap::default();
let mut fn_origins: HashMap<String, Rc<Module>> = HashMap::default();
let mut mixin_origins: HashMap<String, Rc<Module>> = HashMap::default();
let var_write_origins: HashMap<String, (Rc<Module>, String)> = forwarded
.var_origins
.iter()
.map(|(k, (m, o))| (k.clone(), (Rc::clone(m), o.clone())))
.collect();
{
let mut vars = vars_scope.borrow_mut();
for (k, v) in forwarded.vars {
if let std::collections::hash_map::Entry::Vacant(e) = vars.entry(k.clone()) {
e.insert(v);
if let Some(o) = forwarded.var_origins.get(&k) {
var_origins.insert(k, (Rc::clone(&o.0), o.1.clone()));
}
}
}
}
{
let mut fns = functions.borrow_mut();
for (k, v) in forwarded.functions {
if let std::collections::hash_map::Entry::Vacant(e) = fns.entry(k.clone()) {
e.insert(v);
if let Some(o) = forwarded.fn_origins.get(&k) {
fn_origins.insert(k, Rc::clone(o));
}
}
}
}
{
let mut mxs = mixins.borrow_mut();
for (k, v) in forwarded.mixins {
if let std::collections::hash_map::Entry::Vacant(e) = mxs.entry(k.clone()) {
e.insert(v);
if let Some(o) = forwarded.mixin_origins.get(&k) {
mixin_origins.insert(k, Rc::clone(o));
}
}
}
}
Ok((
Module {
vars: vars_scope,
functions,
mixins,
used_user_modules,
star_user_modules,
used_builtin_modules,
star_builtin_modules,
forwarded_builtins: forwarded.builtins,
var_origins,
var_write_origins,
fn_origins,
mixin_origins,
diag_url: diag_url.to_string(),
config_origin: std::cell::Cell::new(self.pending_config_id),
file_dir: dirname_of(key).unwrap_or_default(),
emitted_main: std::cell::Cell::new(false),
css: Vec::new(),
},
consumed,
))
}
#[allow(clippy::too_many_arguments)]
fn exec_forward(
&mut self,
url: &str,
prefix: Option<&str>,
show: &Option<Vec<crate::ast::ForwardMember>>,
hide: &Option<Vec<crate::ast::ForwardMember>>,
config: &[crate::ast::ConfigEntry],
pos: Pos,
parents: &[String],
sink: &mut Sink<'_>,
) -> Result<(), Error> {
if let Some(m) = url.strip_prefix("sass:") {
if !crate::builtins::is_module(m) {
return Err(Error::at("Can't find stylesheet to import.".to_string(), pos));
}
if !config.is_empty() {
return Err(Error::at(
"Built-in modules can't be configured.".to_string(),
pos,
));
}
self.forwarded.builtins.push(ForwardedBuiltin {
module: m.to_string(),
prefix: prefix.map(str::to_string),
show: member_set(show, false),
hide: member_set(hide, false),
});
return Ok(());
}
let forward_conf = self.eval_config(config)?;
let downstream = self.pending_config.clone();
let var_visible = forward_var_visibility(show, hide);
let pfx_opt = prefix;
let mut passthrough: HashMap<String, (Value, bool)> = HashMap::default();
let mut passthrough_origin: HashMap<String, String> = HashMap::default();
for (dk, dv) in &downstream {
let upstream_name = match pfx_opt {
Some(p) => match dk.strip_prefix(p) {
Some(rest) => rest.to_string(),
None => continue,
},
None => dk.clone(),
};
if is_private_member(&upstream_name) || !var_visible(&upstream_name) {
continue;
}
passthrough.insert(upstream_name.clone(), dv.clone());
passthrough_origin.insert(upstream_name, dk.clone());
}
let mut combined: HashMap<String, (Value, bool)> = passthrough.clone();
let mut forward_claimed: Vec<String> = Vec::new();
let mut forward_own: Vec<String> = Vec::new();
let mut forward_shadowed: Vec<String> = Vec::new();
for (name, (val, is_default)) in &forward_conf {
if *is_default {
let downstream_overrides = passthrough
.get(name)
.is_some_and(|(v, _)| !matches!(v, Value::Null));
if downstream_overrides {
forward_claimed.push(name.clone());
} else {
combined.insert(name.clone(), (val.clone(), false));
forward_own.push(name.clone());
}
} else {
if passthrough.contains_key(name) {
forward_shadowed.push(name.clone());
}
combined.insert(name.clone(), (val.clone(), false));
forward_own.push(name.clone());
}
}
let saved_implicit = self.config_is_implicit;
if !forward_conf.is_empty() {
self.config_is_implicit = false;
}
let combined_id = if forward_conf.is_empty() {
self.pending_config_id
} else {
self.fresh_config_id()
};
let load_result = self.load_module(url, combined, combined_id, pos, parents, false, sink);
self.config_is_implicit = saved_implicit;
let (module, consumed) = load_result?;
if forward_own.iter().any(|k| !consumed.contains(k)) {
return Err(Error::at(
"This variable was not declared with !default in the @used module.".to_string(),
pos,
));
}
for up in consumed.iter().chain(forward_claimed.iter()) {
if forward_shadowed.contains(up) {
continue;
}
if let Some(dk) = passthrough_origin.get(up) {
if !self.consumed_config.contains(dk) {
self.consumed_config.push(dk.clone());
}
}
}
let show_vars = member_set(show, true);
let show_names = member_set(show, false);
let hide_vars = member_set(hide, true);
let hide_names = member_set(hide, false);
let has_show = show.is_some();
let visible_var = |name: &str| -> bool {
if is_private_member(name) {
return false;
}
let n = normalize_var_name(name);
if has_show {
show_vars.as_ref().map(|s| s.contains(&n)).unwrap_or(false)
} else {
!hide_vars.as_ref().map(|s| s.contains(&n)).unwrap_or(false)
}
};
let visible_name = |name: &str| -> bool {
if is_private_member(name) {
return false;
}
let n = normalize_var_name(name);
if has_show {
show_names.as_ref().map(|s| s.contains(&n)).unwrap_or(false)
} else {
!hide_names.as_ref().map(|s| s.contains(&n)).unwrap_or(false)
}
};
let src: *const Module = Rc::as_ptr(&module);
let pfx = prefix.unwrap_or("");
let module_vars: Vec<(String, Value)> = module
.vars
.borrow()
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
for (name, val) in &module_vars {
let key = format!("{pfx}{name}");
if !is_private_member(name) && visible_var(&key) {
let origin = module
.var_origin(name)
.unwrap_or_else(|| (Rc::clone(&module), name.clone()));
let member_src: *const Module = Rc::as_ptr(&origin.0);
if let Some(prev) = self.forwarded.var_src.get(&key) {
if *prev != member_src {
return Err(Error::at(
format!("Two forwarded modules both define a variable named ${key}."),
pos,
));
}
}
self.forwarded.vars.insert(key.clone(), val.clone());
self.forwarded.var_origins.insert(key.clone(), origin);
self.forwarded.var_src.insert(key, member_src);
}
}
for (name, f) in module.functions.borrow().iter() {
let key = format!("{pfx}{name}");
if !is_private_member(name) && visible_name(&key) {
let f_src: *const Module = module.fn_origin(name).map(|m| Rc::as_ptr(&m)).unwrap_or(src);
if let Some(prev) = self.forwarded.fn_src.get(&key) {
if *prev != f_src {
return Err(Error::at(
format!("Two forwarded modules both define a function named {key}."),
pos,
));
}
}
let origin = module.fn_origin(name).unwrap_or_else(|| Rc::clone(&module));
self.forwarded.functions.insert(key.clone(), Rc::clone(f));
self.forwarded.fn_origins.insert(key.clone(), origin);
self.forwarded.fn_src.insert(key, f_src);
}
}
for (name, m) in module.mixins.borrow().iter() {
let key = format!("{pfx}{name}");
if !is_private_member(name) && visible_name(&key) {
let m_src: *const Module = module.mixin_origin(name).map(|m| Rc::as_ptr(&m)).unwrap_or(src);
if let Some(prev) = self.forwarded.mixin_src.get(&key) {
if *prev != m_src {
return Err(Error::at(
format!("Two forwarded modules both define a mixin named {key}."),
pos,
));
}
}
let origin = module.mixin_origin(name).unwrap_or_else(|| Rc::clone(&module));
self.forwarded.mixins.insert(key.clone(), Rc::clone(m));
self.forwarded.mixin_origins.insert(key.clone(), origin);
self.forwarded.mixin_src.insert(key, m_src);
}
}
Ok(())
}
fn exec(&mut self, stmts: &[Stmt], parents: &[String], sink: &mut Sink<'_>) -> Result<(), Error> {
for stmt in stmts {
match stmt {
Stmt::VarDecl(v) => self.apply_var(v)?,
Stmt::Comment(c, lines) => {
let text = self.eval_template(c)?;
let lines = self.stamp(*lines);
sink.push_comment(text, lines);
}
Stmt::Decl(d) => {
if sink.is_top() {
return Err(Error::at("top-level declarations aren't allowed", d.pos));
}
if let Some(oi) = self.eval_decl(d)? {
sink.push_item(oi);
}
}
Stmt::PropertySet(ps) => {
if sink.is_top() {
return Err(Error::at("top-level declarations aren't allowed", ps.pos));
}
self.eval_property_set(ps, parents, sink)?;
}
Stmt::CustomDecl(d) => {
if sink.is_top() {
return Err(Error::at("top-level declarations aren't allowed", d.pos));
}
if self.decl_prefix.is_some() {
return Err(Error::at(
"Declarations whose names begin with \"--\" may not be nested.",
d.pos,
));
}
if let Some(oi) = self.eval_custom_decl(d)? {
sink.push_item(oi);
}
}
Stmt::Rule(r) => self.eval_style_rule(r, parents, sink)?,
Stmt::If(branches) => {
for branch in branches {
let take = match &branch.cond {
None => true,
Some(c) => self.eval_expr(c)?.is_truthy(),
};
if take {
self.push_scope(true);
let result = self.exec(&branch.body, parents, sink);
self.pop_scope();
result?;
break;
}
}
}
Stmt::For {
var,
from,
to,
inclusive,
body,
} => {
let (start_i, end_i, unit) = self.for_bounds(from, to)?;
self.push_scope(true);
let mut result = Ok(());
for i in for_indices(start_i, end_i, *inclusive) {
self.set_local(var, Value::Number(Number::with_unit(i as f64, unit.clone())));
result = self.exec(body, parents, sink);
if result.is_err() {
break;
}
}
self.pop_scope();
result?;
}
Stmt::Each { vars, list, body } => {
let items = self.eval_each_items(list)?;
self.push_scope(true);
let mut result = Ok(());
for item in items {
self.bind_each(vars, item);
result = self.exec(body, parents, sink);
if result.is_err() {
break;
}
}
self.pop_scope();
result?;
}
Stmt::While { cond, body } => {
self.push_scope(true);
let mut result = Ok(());
let mut guard = 0u32;
loop {
match self.eval_expr(cond) {
Ok(v) if v.is_truthy() => {}
Ok(_) => break,
Err(e) => {
result = Err(e);
break;
}
}
result = self.exec(body, parents, sink);
if result.is_err() {
break;
}
guard += 1;
if guard >= 100_000 {
result = Err(Error::unpositioned("@while exceeded 100000 iterations"));
break;
}
}
self.pop_scope();
result?;
}
Stmt::FunctionDef(callable) => {
let captured = self.capture_callable(callable);
self.define_function(&callable.name, captured);
}
Stmt::MixinDef(callable) => {
let captured = self.capture_callable(callable);
self.define_mixin(&callable.name, captured);
}
Stmt::Return(_) => {
return Err(Error::unpositioned("@return is only allowed inside a function."));
}
Stmt::Include {
name,
args,
content,
content_params,
module,
pos,
length,
} => {
let saved = self.enter_call(*pos, *length, &mixin_frame_name(name, module));
let r = self.exec_include(
name,
args,
content.clone(),
content_params.clone(),
module.as_deref(),
*pos,
parents,
sink,
);
self.leave_call(saved);
r?;
}
Stmt::Use {
url,
namespace,
star,
config,
pos,
} => self.exec_use(url, namespace.as_deref(), *star, config, *pos, parents, sink)?,
Stmt::Forward {
url,
prefix,
show,
hide,
config,
pos,
} => self.exec_forward(url, prefix.as_deref(), show, hide, config, *pos, parents, sink)?,
Stmt::Content(content_args) => {
self.in_mixin.push(false);
let result = self.exec_content(content_args, parents, sink);
self.in_mixin.pop();
result?;
}
Stmt::Import(args) => self.eval_imports(args, parents, sink)?,
Stmt::AtRule {
name,
prelude,
body,
lines,
} => {
let lines = self.stamp(*lines);
self.eval_at_rule(name, prelude, body.as_deref(), lines, parents, sink)?;
}
Stmt::InterpAtRule { name, prelude, body } => {
let resolved = self.eval_template(name)?;
if is_keyframes_name(&resolved) && body.is_some() {
if let Some(b) = body {
self.eval_keyframes(&resolved, prelude, b, SrcLines::default(), sink)?;
}
} else {
self.eval_at_rule(
&resolved,
prelude,
body.as_deref(),
SrcLines::default(),
parents,
sink,
)?;
}
}
Stmt::CssCustomAtRule { name, prelude, body } => {
self.eval_css_custom_at_rule(name, prelude, body, sink)?;
}
Stmt::Media { query, body, lines } => {
let stamped = self.stamp(*lines);
self.eval_media(query, body, stamped, parents, sink)?;
}
Stmt::Supports { condition, body } => {
self.eval_supports(condition, body, parents, sink)?;
}
Stmt::AtRoot { query, body } => {
self.eval_at_root(query.as_deref(), body, parents, sink)?;
}
Stmt::Keyframes {
name,
prelude,
body,
lines,
} => {
let lines = self.stamp(*lines);
self.eval_keyframes(name, prelude, body, lines, sink)?;
}
Stmt::Extend {
selector,
optional,
pos,
} => self.register_extend(selector, *optional, *pos, parents)?,
Stmt::Warn { value, pos } => self.emit_warn(value, *pos)?,
Stmt::Debug { value, pos } => self.emit_debug(value, *pos)?,
Stmt::Error { value, pos, length } => {
return Err(self.build_error(value, *pos, *length));
}
}
}
Ok(())
}
fn eval_style_rule(&mut self, rule: &Rule, parents: &[String], sink: &mut Sink<'_>) -> Result<(), Error> {
let (sel_str, interp_bounds) = self.eval_template_bounds(&rule.selector)?;
if sel_str.trim().is_empty() {
return Err(Error::unpositioned("expected selector."));
}
validate_selector(&sel_str, !parents.is_empty())?;
if !self.in_keyframes {
if let Some(at_idx) = find_unquoted_at(&sel_str) {
return Err(self.interp_selector_error(rule, &sel_str, &interp_bounds, at_idx));
}
}
if !self.in_keyframes {
for part in split_commas(&sel_str) {
if part.trim_start().starts_with(|c: char| c.is_ascii_digit()) {
return Err(Error::unpositioned("expected selector."));
}
}
}
let current = if self.in_keyframes {
split_commas(&sel_str)
.into_iter()
.map(|p| p.trim().to_string())
.filter(|p| !p.is_empty())
.collect()
} else {
resolve_selectors_opt(&sel_str, parents, !self.at_root_excluding_style_rule)?
};
let full_lbs: Vec<bool> = if self.current_linebreaks.is_empty() && !sel_str.contains('\n') {
Vec::new()
} else {
let part_lbs = comma_linebreaks(&sel_str, !parents.is_empty());
let n = part_lbs.len().max(1);
let parent_lbs: &[bool] = if self.current_linebreaks.len() == parents.len() {
&self.current_linebreaks
} else {
&[]
};
(0..current.len())
.map(|i| {
part_lbs.get(i % n).copied().unwrap_or(false)
|| parent_lbs.get(i / n).copied().unwrap_or(false)
})
.collect()
};
let mut emit_selectors: Vec<String> = Vec::new();
let mut emit_linebreaks: Vec<bool> = Vec::new();
for (i, s) in current.iter().enumerate() {
if complex_selector_block_is_bogus(s) {
self.bogus_selectors.push(s.clone());
continue;
}
if s.contains('%') {
self.placeholder_rules
.push((self.current_module.clone(), s.clone()));
}
let s = if self.in_keyframes {
normalize_keyframe_selector(s)
} else {
s.clone()
};
emit_selectors.push(s);
if !full_lbs.is_empty() {
emit_linebreaks.push(full_lbs.get(i).copied().unwrap_or(false));
}
}
self.push_scope(false);
let prev_selector = self.current_selector.replace(current.clone());
let prev_linebreaks = std::mem::replace(&mut self.current_linebreaks, full_lbs);
let prev_at_root = std::mem::replace(&mut self.at_root_excluding_style_rule, false);
let rule_lines = self.stamp(SrcLines {
file: 0,
start: rule.brace_line,
end: rule.end_line,
col: 0,
});
let mut items: Vec<OutItem> = Vec::new();
let mut nested: Vec<OutNode> = Vec::new();
let mut flushed: Option<usize> = None;
let at_depth = self.at_rule_ctx.len();
let extend_base = self.extends.len();
let result = {
let mut child = Sink::Rule {
selectors: &emit_selectors,
linebreaks: &emit_linebreaks,
lines: rule_lines,
items: &mut items,
nested: &mut nested,
at_depth,
flushed: &mut flushed,
extend_base,
};
let r = self.exec(&rule.body, ¤t, &mut child);
if r.is_ok() {
child.flush_rule_block();
}
r
};
self.current_selector = prev_selector;
self.current_linebreaks = prev_linebreaks;
self.at_root_excluding_style_rule = prev_at_root;
self.pop_scope();
result?;
sink.emit_style_rule(nested);
Ok(())
}
fn eval_at_rule(
&mut self,
name: &str,
prelude: &[TplPiece],
body: Option<&[Stmt]>,
lines: SrcLines,
parents: &[String],
sink: &mut Sink<'_>,
) -> Result<(), Error> {
let prelude = self.eval_template(prelude)?;
let Some(stmts) = body else {
if name == "charset" && !sink.is_rule() {
return Ok(());
}
sink.push_childless_at_rule(name.to_string(), prelude, lines);
return Ok(());
};
let body_parents: &[String] = if name == "font-face" || self.at_root_excluding_style_rule {
&[]
} else {
parents
};
let out_body = self.eval_at_body(stmts, body_parents)?;
sink.push_at_rule(OutNode::AtRule {
name: name.to_string(),
prelude,
body: out_body,
has_block: true,
lines,
});
Ok(())
}
fn eval_css_custom_at_rule(
&mut self,
name: &str,
prelude: &[TplPiece],
body: &[CssCustomItem],
sink: &mut Sink<'_>,
) -> Result<(), Error> {
let prelude = self.eval_template(prelude)?;
let mut out_body: Vec<OutNode> = Vec::new();
for item in body {
let prop = self.eval_template(&item.property)?;
match &item.value {
CssCustomValue::Raw(tpl) => {
let raw = self.eval_template(tpl)?;
out_body.push(OutNode::Raw(format!("{prop}:{raw};")));
}
CssCustomValue::Script(expr) => {
let value = self.eval_expr(expr)?.to_css(self.compressed());
out_body.push(OutNode::Raw(format!("{prop}: {value};")));
}
CssCustomValue::Set(children) => {
for (suffix, expr) in children {
let sfx = self.eval_template(suffix)?;
let value = self.eval_expr(expr)?.to_css(self.compressed());
out_body.push(OutNode::Raw(format!("{prop}-{sfx}: {value};")));
}
}
}
}
sink.push_at_rule(OutNode::AtRule {
name: name.to_string(),
prelude,
body: out_body,
has_block: true,
lines: SrcLines::default(),
});
Ok(())
}
fn eval_at_body(&mut self, stmts: &[Stmt], parents: &[String]) -> Result<Vec<OutNode>, Error> {
self.push_scope(false);
let mut body: Vec<OutNode> = Vec::new();
let result = if parents.is_empty() {
let mut child = Sink::AtRoot(&mut body);
self.exec(stmts, &[], &mut child)
} else {
let mut items: Vec<OutItem> = Vec::new();
let mut nested: Vec<OutNode> = Vec::new();
let mut flushed: Option<usize> = None;
let at_depth = self.at_rule_ctx.len();
let res = {
let mut child = Sink::Rule {
selectors: parents,
linebreaks: &[],
lines: SrcLines::default(),
items: &mut items,
nested: &mut nested,
at_depth,
flushed: &mut flushed,
extend_base: usize::MAX,
};
let r = self.exec(stmts, parents, &mut child);
if r.is_ok() {
child.flush_rule_block();
}
r
};
if res.is_ok() {
body.extend(nested);
}
res
};
self.pop_scope();
result?;
Ok(body)
}
fn eval_media(
&mut self,
query: &MediaQueryList,
body: &[Stmt],
lines: SrcLines,
parents: &[String],
sink: &mut Sink<'_>,
) -> Result<(), Error> {
if parents.is_empty() {
for stmt in body {
if matches!(stmt, Stmt::Decl(_)) {
return Err(Error::unpositioned("expected \"{\"."));
}
}
}
let queries = self.resolve_media_queries(query)?;
if self.in_keyframes && sink.is_rule() {
let prelude = serialize_media_queries(&queries);
let out_body = self.eval_at_body(body, &[])?;
sink.push_item(OutItem::NestedAtRule {
name: "media".to_string(),
prelude,
items: at_body_to_items(out_body),
});
return Ok(());
}
let merged = if self.media_queries.is_empty() {
None
} else {
match merge_media_query_lists(&self.media_queries, &queries) {
Some(m) if m.is_empty() => return Ok(()),
other => other,
}
};
let child_queries = merged.clone().unwrap_or_else(|| queries.clone());
let bubble_out = merged.is_some();
let node_queries = if bubble_out { &child_queries } else { &queries };
let prelude = serialize_media_queries(node_queries);
let enclosing = !self.media_queries.is_empty();
let saved = std::mem::replace(&mut self.media_queries, child_queries);
let saved_hoist = std::mem::take(&mut self.media_hoist);
let own_depth = self.at_rule_ctx.len();
self.at_rule_ctx.push(AtCtx::Media {
prelude: prelude.clone(),
});
let out_body = self.eval_at_body(body, parents);
self.at_rule_ctx.pop();
self.media_queries = saved;
let mut hoisted = std::mem::replace(&mut self.media_hoist, saved_hoist);
let out_body = out_body?;
let mut result: Vec<OutNode> = Vec::new();
let mut segment: Vec<OutNode> = Vec::new();
let mut hoist_iter = hoisted.drain(..);
let flush = |segment: &mut Vec<OutNode>, result: &mut Vec<OutNode>, prelude: &str| {
if !segment.is_empty() {
result.push(OutNode::AtRule {
name: "media".to_string(),
prelude: prelude.to_string(),
body: std::mem::take(segment),
has_block: true,
lines,
});
}
};
for n in out_body {
let at_root_target = match &n {
OutNode::Raw(s) => at_root_marker_target(s),
_ => None,
};
if matches!(&n, OutNode::Raw(s) if s == MEDIA_HOIST_MARKER) {
flush(&mut segment, &mut result, &prelude);
if let Some(batch) = hoist_iter.next() {
result.extend(batch);
}
} else if let Some(t) = at_root_target {
if t == own_depth + 1 {
if let Some(b) = self.at_root_hoist.pop_front() {
debug_assert_eq!(b.target, t);
segment.extend(b.nodes);
}
} else if own_depth == 0 {
flush(&mut segment, &mut result, &prelude);
if let Some(b) = self.at_root_hoist.pop_front() {
debug_assert_eq!(b.target, t);
result.extend(b.nodes);
result.push(OutNode::Raw(
if b.group_end {
STYLE_GROUP_END
} else {
AT_ROOT_PACK_TIGHT
}
.to_string(),
));
}
} else {
flush(&mut segment, &mut result, &prelude);
result.push(n);
}
} else {
segment.push(n);
}
}
drop(hoist_iter);
flush(&mut segment, &mut result, &prelude);
if result.is_empty() {
return Ok(());
}
if bubble_out && enclosing {
sink.push_at_rule(OutNode::Raw(MEDIA_HOIST_MARKER.to_string()));
self.media_hoist.push(result);
} else {
for n in result {
sink.push_at_rule(n);
}
}
Ok(())
}
fn resolve_media_queries(&mut self, list: &MediaQueryList) -> Result<Vec<ResolvedQuery>, Error> {
let mut out = Vec::with_capacity(list.queries.len());
for q in &list.queries {
out.push(self.resolve_media_query(q)?);
}
if list.queries.iter().any(media_query_has_interp) {
let text = serialize_media_queries(&out);
return css_media_parse_list(&text);
}
Ok(out)
}
fn resolve_media_query(&mut self, q: &MediaQuery) -> Result<ResolvedQuery, Error> {
match q {
MediaQuery::Type {
modifier,
mtype,
conditions,
} => {
let mtype = self.eval_template(mtype)?;
let modifier = match modifier {
Some(t) => Some(self.eval_template(t)?),
None => None,
};
let conditions = self.resolve_conditions(conditions)?;
Ok(ResolvedQuery {
modifier,
mtype: Some(mtype),
conditions,
conjunction_and: true,
})
}
MediaQuery::Condition {
conditions,
conjunction,
} => Ok(ResolvedQuery {
modifier: None,
mtype: None,
conditions: self.resolve_conditions(conditions)?,
conjunction_and: matches!(conjunction, Conjunction::And),
}),
}
}
fn resolve_conditions(&mut self, conds: &[MediaInParens]) -> Result<Vec<String>, Error> {
let mut out = Vec::with_capacity(conds.len());
for c in conds {
out.push(self.serialize_media_in_parens(c)?);
}
Ok(out)
}
fn serialize_media_in_parens(&mut self, c: &MediaInParens) -> Result<String, Error> {
match c {
MediaInParens::Feature(f) => {
let inner = self.serialize_media_feature(f)?;
Ok(format!("({inner})"))
}
MediaInParens::Not(inner) => Ok(format!("not {}", self.serialize_media_in_parens(inner)?)),
MediaInParens::Group {
conditions,
conjunction,
} => {
let parts = self.resolve_conditions(conditions)?;
let sep = if matches!(conjunction, Conjunction::And) {
" and "
} else {
" or "
};
Ok(format!("({})", parts.join(sep)))
}
MediaInParens::Interp(e) => {
let v = self.eval_expr(e)?;
Ok(v.to_interp())
}
}
}
fn serialize_media_feature(&mut self, f: &MediaFeature) -> Result<String, Error> {
match f {
MediaFeature::Decl { name, value } => {
let n = self.eval_expr(name)?.to_interp();
match value {
Some(v) => {
let val = self.eval_expr(v)?.to_interp();
Ok(format!("{n}: {val}"))
}
None => Ok(n),
}
}
MediaFeature::Range {
first,
op1,
second,
rest,
} => {
let a = self.eval_expr(first)?.to_css(self.compressed());
let b = self.eval_expr(second)?.to_css(self.compressed());
let mut s = format!("{a} {op1} {b}");
if let Some((op2, third)) = rest {
let c = self.eval_expr(third)?.to_css(self.compressed());
s.push_str(&format!(" {op2} {c}"));
}
Ok(s)
}
}
}
fn eval_supports(
&mut self,
condition: &SupportsCondition,
body: &[Stmt],
parents: &[String],
sink: &mut Sink<'_>,
) -> Result<(), Error> {
let prelude = self.serialize_supports_condition(condition)?;
let own_depth = self.at_rule_ctx.len();
self.at_rule_ctx.push(AtCtx::Supports {
prelude: prelude.clone(),
});
let out_body = self.eval_at_body(body, parents);
self.at_rule_ctx.pop();
let out_body = out_body?;
if out_body.is_empty() {
return Ok(());
}
let mut result: Vec<OutNode> = Vec::new();
let mut segment: Vec<OutNode> = Vec::new();
let flush = |segment: &mut Vec<OutNode>, result: &mut Vec<OutNode>| {
if !segment.is_empty() {
result.push(OutNode::AtRule {
name: "supports".to_string(),
prelude: prelude.clone(),
body: std::mem::take(segment),
has_block: true,
lines: SrcLines::default(),
});
}
};
for n in out_body {
let at_root_target = match &n {
OutNode::Raw(s) => at_root_marker_target(s),
_ => None,
};
if let Some(t) = at_root_target {
if t == own_depth + 1 {
if let Some(b) = self.at_root_hoist.pop_front() {
segment.extend(b.nodes);
}
} else if own_depth == 0 {
flush(&mut segment, &mut result);
if let Some(b) = self.at_root_hoist.pop_front() {
result.extend(b.nodes);
result.push(OutNode::Raw(
if b.group_end {
STYLE_GROUP_END
} else {
AT_ROOT_PACK_TIGHT
}
.to_string(),
));
}
} else {
flush(&mut segment, &mut result);
result.push(n);
}
} else {
segment.push(n);
}
}
flush(&mut segment, &mut result);
for n in result {
sink.push_at_rule(n);
}
Ok(())
}
fn serialize_supports_condition(&mut self, condition: &SupportsCondition) -> Result<String, Error> {
match condition {
SupportsCondition::Operation { left, right, op } => {
let l = self.parenthesize_supports(left, Some(*op))?;
let r = self.parenthesize_supports(right, Some(*op))?;
let word = if matches!(op, Conjunction::And) {
"and"
} else {
"or"
};
Ok(format!("{l} {word} {r}"))
}
SupportsCondition::Negation(inner) => {
Ok(format!("not {}", self.parenthesize_supports(inner, None)?))
}
SupportsCondition::Interpolation(expr) => Ok(self.eval_expr(expr)?.to_interp()),
SupportsCondition::Declaration { name, value, custom } => {
let saved = self.in_supports_declaration;
self.in_supports_declaration = true;
let result = (|| {
let n = self.eval_expr(name)?.to_css(self.compressed());
let v = match value.as_ref() {
SupportsValue::Expr(e) => self.eval_expr(e)?.to_css(self.compressed()),
SupportsValue::Raw(tpl) => unquoted_string_css(&self.eval_template(tpl)?),
};
Ok::<_, Error>((n, v))
})();
self.in_supports_declaration = saved;
let (n, v) = result?;
let sep = if *custom { "" } else { " " };
Ok(format!("({n}:{sep}{v})"))
}
SupportsCondition::Function { name, arguments } => {
let n = self.eval_template(name)?;
let args = self.eval_template(arguments)?;
Ok(format!("{n}({args})"))
}
SupportsCondition::Anything(contents) => {
let inner = self.eval_template(contents)?;
Ok(format!("({inner})"))
}
}
}
fn serialize_css_import(
&mut self,
url: &[TplPiece],
modifiers: &[ImportModifier],
) -> Result<String, Error> {
let mut out = self.eval_template(url)?;
for m in modifiers {
match m {
ImportModifier::Raw(tpl) => {
out.push(' ');
out.push_str(&self.eval_template(tpl)?);
}
ImportModifier::Supports {
condition,
declaration,
} => {
out.push(' ');
let inner = unquoted_string_css(&self.serialize_supports_condition(condition)?);
if *declaration {
out.push_str(&format!("supports{inner}"));
} else {
out.push_str(&format!("supports({inner})"));
}
}
ImportModifier::Media { list, comma_before } => {
out.push_str(if *comma_before { ", " } else { " " });
let queries = self.resolve_media_queries(list)?;
out.push_str(&serialize_media_queries(&queries));
}
}
}
Ok(out)
}
fn parenthesize_supports(
&mut self,
condition: &SupportsCondition,
operator: Option<Conjunction>,
) -> Result<String, Error> {
let needs_parens = match condition {
SupportsCondition::Negation(_) => true,
SupportsCondition::Operation { op, .. } => match operator {
None => true,
Some(outer) => outer != *op,
},
_ => false,
};
let inner = self.serialize_supports_condition(condition)?;
if needs_parens {
Ok(format!("({inner})"))
} else {
Ok(inner)
}
}
fn eval_keyframes(
&mut self,
name: &str,
prelude: &[TplPiece],
body: &[Stmt],
lines: SrcLines,
sink: &mut Sink<'_>,
) -> Result<(), Error> {
for stmt in body {
if let Stmt::Rule(frame) = stmt {
for inner in &frame.body {
if matches!(inner, Stmt::Rule(_)) {
return Err(Error::unpositioned(
"Style rules may not be used within keyframe blocks.",
));
}
}
}
}
let prelude = self.eval_template(prelude)?;
let saved_kf = std::mem::replace(&mut self.in_keyframes, true);
let own_depth = self.at_rule_ctx.len();
self.at_rule_ctx.push(AtCtx::Keyframes {
name: name.to_string(),
prelude: prelude.clone(),
});
let out_body = self.eval_at_body(body, &[]);
self.at_rule_ctx.pop();
self.in_keyframes = saved_kf;
let out_body = out_body?;
let mut shell: Vec<OutNode> = Vec::new();
let mut after: Vec<OutNode> = Vec::new();
for n in out_body {
let at_root_target = match &n {
OutNode::Raw(s) => at_root_marker_target(s),
_ => None,
};
if let Some(t) = at_root_target {
if t == own_depth + 1 {
if let Some(b) = self.at_root_hoist.pop_front() {
shell.extend(b.nodes);
}
} else if own_depth == 0 {
if let Some(b) = self.at_root_hoist.pop_front() {
after.extend(b.nodes);
after.push(OutNode::Raw(
if b.group_end {
STYLE_GROUP_END
} else {
AT_ROOT_PACK_TIGHT
}
.to_string(),
));
}
} else {
after.push(n);
}
} else {
shell.push(n);
}
}
sink.push_at_rule(OutNode::AtRule {
name: name.to_string(),
prelude,
body: shell,
has_block: true,
lines,
});
for n in after {
sink.push_at_rule(n);
}
Ok(())
}
fn eval_at_root(
&mut self,
query: Option<&[TplPiece]>,
body: &[Stmt],
parents: &[String],
sink: &mut Sink<'_>,
) -> Result<(), Error> {
let query_text = match query {
Some(tpl) => Some(self.eval_template(tpl)?),
None => None,
};
let q = AtRootQuery::parse(query_text.as_deref());
let excluded: Vec<bool> = self
.at_rule_ctx
.iter()
.map(|c| q.excludes_name(c.query_name()))
.collect();
let first_excluded = excluded.iter().position(|&e| e);
let any_excluded_layer = first_excluded.is_some();
if !any_excluded_layer && !q.excludes_style_rules() {
return self.exec(body, parents, sink);
}
self.push_scope(false);
let saved = self.at_root_excluding_style_rule;
self.at_root_excluding_style_rule = q.excludes_style_rules();
let saved_media = if q.excludes_name("media") {
Some(std::mem::take(&mut self.media_queries))
} else {
None
};
let saved_kf = if q.excludes_name("keyframes") {
Some(std::mem::replace(&mut self.in_keyframes, false))
} else {
None
};
let mut out: Vec<OutNode> = Vec::new();
let res = {
let mut child = Sink::AtRoot(&mut out);
self.exec(body, parents, &mut child)
};
self.at_root_excluding_style_rule = saved;
if let Some(m) = saved_media {
self.media_queries = m;
}
if let Some(k) = saved_kf {
self.in_keyframes = k;
}
self.pop_scope();
res?;
if (q.excludes_style_rules() || parents.is_empty())
&& out.iter().any(|n| matches!(n, OutNode::AtDecl { .. }))
{
return Err(Error::unpositioned(
"Declarations may only be used within style rules.",
));
}
let out = if !q.excludes_style_rules() && !parents.is_empty() {
let mut wrapped: Vec<OutNode> = Vec::new();
let mut decls: Vec<OutItem> = Vec::new();
let flush = |decls: &mut Vec<OutItem>, wrapped: &mut Vec<OutNode>| {
if !decls.is_empty() {
wrapped.push(OutNode::Rule {
selectors: parents.to_vec(),
linebreaks: Vec::new(),
items: std::mem::take(decls),
lines: SrcLines::default(),
extend_base: usize::MAX,
});
}
};
for n in out {
match n {
OutNode::AtDecl {
prop,
value,
important,
custom,
lines,
} => decls.push(OutItem::Decl {
prop,
value,
important,
custom,
lines,
}),
other => {
flush(&mut decls, &mut wrapped);
wrapped.push(other);
}
}
}
flush(&mut decls, &mut wrapped);
wrapped
} else {
out
};
let mut spaced: Vec<OutNode> = Vec::new();
let mut prev_was_rule = false;
for node in out {
if prev_was_rule {
spaced.push(OutNode::Blank);
}
prev_was_rule = matches!(node, OutNode::Rule { .. });
spaced.push(node);
}
if spaced.is_empty() {
return Ok(());
}
if let Some(te) = first_excluded {
let mut batch = spaced;
for (i, layer) in self.at_rule_ctx.iter().enumerate().rev() {
if i > te && !excluded[i] {
batch = vec![layer.wrap(batch)];
}
}
self.at_root_hoist.push_back(AtRootBatch {
target: te,
group_end: parents.is_empty(),
nodes: batch,
});
sink.push_at_rule(OutNode::Raw(format!("{AT_ROOT_HOIST_MARKER}{te}")));
} else {
for node in spaced {
sink.push_at_rule(node);
}
}
Ok(())
}
fn eval_decl(&mut self, d: &Declaration) -> Result<Option<OutItem>, Error> {
let name = self.eval_template(&d.property)?.trim().to_string();
let prop = match &self.decl_prefix {
Some(prefix) => format!("{prefix}-{name}"),
None => name,
};
let value = self.eval_expr(&d.value)?;
if matches!(value, Value::Null) {
return Ok(None);
}
if let Some(m) = find_map(&value) {
return Err(Error::at(
format!("{} isn't a valid CSS value.", m.to_css(false)),
d.pos,
));
}
if let Value::Function(f) = &value {
return Err(Error::at(
format!("{} isn't a valid CSS value.", f.inspect()),
d.pos,
));
}
if let Value::Mixin(m) = &value {
return Err(Error::at(
format!("{} isn't a valid CSS value.", m.inspect()),
d.pos,
));
}
if let Value::List(l) = &value {
if l.items.is_empty() && !l.bracketed {
return Err(Error::at("() isn't a valid CSS value.", d.pos));
}
}
let vstr = value.to_css(self.compressed());
if vstr.is_empty() {
return Ok(None);
}
Ok(Some(OutItem::Decl {
prop,
value: vstr,
important: d.important,
custom: false,
lines: self.stamp(SrcLines {
file: 0,
start: d.pos.line as u32,
end: d.end_line,
col: 0,
}),
}))
}
fn eval_custom_decl(&mut self, d: &CustomDecl) -> Result<Option<OutItem>, Error> {
let prop = self.eval_template(&d.property)?.trim().to_string();
let value = self.eval_template(&d.value)?;
Ok(Some(OutItem::Decl {
prop,
value,
important: false,
custom: true,
lines: self.stamp(SrcLines {
file: 0,
start: d.pos.line as u32,
end: d.end_line,
col: d.pos.col.saturating_sub(1) as u32,
}),
}))
}
fn eval_property_set(
&mut self,
ps: &PropertySet,
parents: &[String],
sink: &mut Sink<'_>,
) -> Result<(), Error> {
if self.decl_prefix.is_some() && literal_name_is_custom_property(&ps.property) {
return Err(Error::at(
"Declarations whose names begin with \"--\" may not be nested.",
ps.pos,
));
}
let name = self.eval_template(&ps.property)?.trim().to_string();
let full = match &self.decl_prefix {
Some(prefix) => format!("{prefix}-{name}"),
None => name,
};
if let Some(value_expr) = &ps.value {
let value = self.eval_expr(value_expr)?;
if !matches!(value, Value::Null) {
if let Some(m) = find_map(&value) {
return Err(Error::at(
format!("{} isn't a valid CSS value.", m.to_css(false)),
ps.pos,
));
}
let vstr = value.to_css(self.compressed());
sink.push_item(OutItem::Decl {
prop: full.clone(),
value: vstr,
important: ps.important,
custom: false,
lines: SrcLines::default(),
});
}
}
let saved = self.decl_prefix.replace(full);
let result = self.exec(&ps.body, parents, sink);
self.decl_prefix = saved;
result
}
fn eval_imports(
&mut self,
args: &[ImportArg],
parents: &[String],
sink: &mut Sink<'_>,
) -> Result<(), Error> {
let importer = self.options.importer;
for arg in args {
match arg {
ImportArg::Css { url, modifiers } => {
let text = self.serialize_css_import(url, modifiers)?;
if matches!(sink, Sink::Rule { .. }) {
sink.push_item(OutItem::ChildlessAtRule {
name: "import".to_string(),
prelude: text,
lines: SrcLines::default(),
});
} else {
sink.push_at_rule(OutNode::Raw(format!("@import {text};")));
}
}
ImportArg::Sass { path, pos, length } => {
if is_css_import(path) {
sink.push_at_rule(OutNode::Raw(format!("@import \"{path}\";")));
continue;
}
self.emit_deprecation(&crate::deprecation::Deprecation::import(), *pos, *length);
let base = self.current_file_dir.clone();
let cache_key = (path.clone(), base.clone());
let entry = match self.import_cache.get(&cache_key) {
Some(e) => {
if self.loading.iter().any(|p| p == path) {
return Err(Error::unpositioned("This file is already being loaded."));
}
Some(e.clone())
}
None => {
let saved = crate::arena::pause();
let resolved =
importer.and_then(|imp| imp.resolve_import_with_path(path, base.as_deref()));
crate::arena::resume(saved);
match resolved {
Some((resolved_key, src, syntax)) => {
if self.loading.iter().any(|p| p == path) {
return Err(Error::unpositioned(
"This file is already being loaded.",
));
}
let sheet = parse_with_syntax(&src, syntax)?;
let e = std::rc::Rc::new((resolved_key, syntax, sheet));
self.import_cache.insert(cache_key, e.clone());
Some(e)
}
None => None,
}
}
};
match entry {
Some(entry) => {
let (resolved_key, syntax, sheet) = (&entry.0, entry.1, &entry.2);
if matches!(syntax, Syntax::Css) {
self.loading.push(path.clone());
let result = self.exec_css(&sheet.stmts, parents, sink);
self.loading.pop();
result?;
continue;
}
self.loading.push(path.clone());
let saved_used = std::mem::take(&mut self.used_modules);
let saved_star = std::mem::take(&mut self.star_modules);
let saved_used_user = std::mem::take(&mut self.used_user_modules);
let saved_star_user = std::mem::take(&mut self.star_user_modules);
{
let mut slots: Vec<String> = Vec::new();
collect_global_var_decls(&sheet.stmts, &mut slots);
if let Some(g) = self.scopes.first() {
let mut g = g.borrow_mut();
for name in slots {
g.entry(name).or_insert(Value::Null);
}
}
}
let saved_fwd = std::mem::take(&mut self.forwarded);
let loads_modules = sheet
.stmts
.iter()
.any(|s| matches!(s, Stmt::Use { .. } | Stmt::Forward { .. }));
let saved_pending_consumed = if loads_modules {
let mut implicit_config: HashMap<String, (Value, bool)> = HashMap::default();
for scope in &self.scopes {
for (k, v) in scope.borrow().iter() {
implicit_config.insert(normalize_var_name(k), (v.clone(), false));
}
}
Some((
std::mem::replace(&mut self.pending_config, implicit_config),
std::mem::take(&mut self.consumed_config),
std::mem::replace(&mut self.config_is_implicit, true),
std::mem::replace(&mut self.pending_config_id, 0),
))
} else {
None
};
let saved_dir = if resolved_key.is_empty() {
self.current_file_dir.clone()
} else {
std::mem::replace(&mut self.current_file_dir, dirname_of(resolved_key))
};
let saved_clone = if loads_modules {
let n = self.copy_counter.get() + 1;
self.copy_counter.set(n);
self.import_clone
.replace((format!("#import{n}"), std::collections::HashSet::new()))
} else {
self.import_clone.take()
};
fn has_top_decl(stmts: &[Stmt]) -> bool {
stmts.iter().any(|s| match s {
Stmt::Decl(_) | Stmt::PropertySet(_) | Stmt::CustomDecl(_) => true,
Stmt::If(branches) => branches.iter().any(|b| has_top_decl(&b.body)),
Stmt::For { body, .. }
| Stmt::Each { body, .. }
| Stmt::While { body, .. } => has_top_decl(body),
_ => false,
})
}
if has_top_decl(&sheet.stmts) {
return Err(Error::unpositioned("expected \"{\"."));
}
let result = self.exec(&sheet.stmts, parents, sink);
self.current_file_dir = saved_dir;
self.import_clone = saved_clone;
if let Some((p, c, i, id)) = saved_pending_consumed {
self.pending_config = p;
self.consumed_config = c;
self.config_is_implicit = i;
self.pending_config_id = id;
}
let imported_fwd = std::mem::replace(&mut self.forwarded, saved_fwd);
self.used_modules = saved_used;
self.star_modules = saved_star;
self.used_user_modules = saved_used_user;
self.star_user_modules = saved_star_user;
self.loading.pop();
result?;
if self.scopes.len() == 1 {
for (k, f) in imported_fwd.functions {
let rebound = self.capture_callable(&f.def);
self.define_function(&k, rebound);
}
for (k, m) in imported_fwd.mixins {
let rebound = self.capture_callable(&m.def);
self.define_mixin(&k, rebound);
}
if let Some(g) = self.scopes.first() {
let mut g = g.borrow_mut();
for (k, val) in imported_fwd.vars {
let src_ptr =
imported_fwd.var_src.get(&k).map(|p| *p as usize).unwrap_or(0);
match self.forwarded_globals.get(&k) {
Some(prev) if *prev == src_ptr => {
g.entry(k).or_insert(val);
}
_ => {
self.forwarded_globals.insert(k.clone(), src_ptr);
g.insert(k, val);
}
}
}
}
} else {
if let Some(s) = self.scopes.last() {
let mut s = s.borrow_mut();
for (k, val) in imported_fwd.vars {
s.insert(k, val);
}
}
for (k, f) in imported_fwd.functions {
let rebound = self.capture_callable(&f.def);
self.define_function(&k, rebound);
}
for (k, m) in imported_fwd.mixins {
let rebound = self.capture_callable(&m.def);
self.define_mixin(&k, rebound);
}
}
}
None => {
return Err(Error::unpositioned(format!(
"Can't find stylesheet to import: {path}"
)));
}
}
}
}
}
Ok(())
}
fn eval_template(&mut self, pieces: &[TplPiece]) -> Result<String, Error> {
let mut s = String::new();
for piece in pieces {
match piece {
TplPiece::Lit(t) => s.push_str(t),
TplPiece::Interp(e) => {
let v = self.eval_expr(e)?;
s.push_str(&v.to_interp());
}
}
}
Ok(s)
}
fn eval_template_bounds(&mut self, pieces: &[TplPiece]) -> Result<(String, Vec<(usize, usize)>), Error> {
let mut s = String::new();
let mut chars = 0usize;
let mut bounds = Vec::new();
for piece in pieces {
match piece {
TplPiece::Lit(t) => {
s.push_str(t);
chars += t.chars().count();
}
TplPiece::Interp(e) => {
let v = self.eval_expr(e)?;
let out = v.to_interp();
let len = out.chars().count();
bounds.push((chars, len));
s.push_str(&out);
chars += len;
}
}
}
Ok((s, bounds))
}
fn parent_selector_value(&self) -> Value {
let Some(selectors) = &self.current_selector else {
return Value::Null;
};
if selectors.is_empty() {
return Value::Null;
}
let items: Vec<Value> = selectors
.iter()
.map(|complex| {
let compounds: Vec<Value> = complex
.split_whitespace()
.map(|c| {
Value::Str(SassStr {
text: c.to_string(),
quoted: false,
})
})
.collect();
Value::List(List {
items: compounds,
sep: ListSep::Space,
bracketed: false,
keywords: None,
})
})
.collect();
Value::List(List {
items,
sep: ListSep::Comma,
bracketed: false,
keywords: None,
})
}
fn eval_expr(&mut self, expr: &Expr) -> Result<Value, Error> {
match self.eval_expr_inner(expr) {
Ok(v) => Ok(v),
Err(e) => Err(self.finalize_error(e)),
}
}
fn eval_expr_inner(&mut self, expr: &Expr) -> Result<Value, Error> {
match expr {
Expr::Number(v, unit) => Ok(Value::Number(Number::with_unit(*v, unit.clone()))),
Expr::Color(c) => Ok(Value::Color(c.clone())),
Expr::Bool(b) => Ok(Value::Bool(*b)),
Expr::Null => Ok(Value::Null),
Expr::Parent => Ok(self.parent_selector_value()),
Expr::Var { name, pos } => match self.lookup(name) {
Some(v) => Ok(v.without_slash()),
None => {
let star_hits: Vec<Value> = if is_private_member(name) {
Vec::new()
} else {
self.star_user_modules
.iter()
.filter_map(|m| m.var(name))
.collect()
};
if star_hits.len() > 1 {
return Err(Error::at(
"This variable is available from multiple global modules.",
*pos,
));
}
if let Some(v) = star_hits.into_iter().next() {
return Ok(v.without_slash());
}
for m in &self.star_modules {
if let Ok(v) = crate::builtins::module_var(m, name, *pos) {
return Ok(v);
}
}
Err(Error::at("Undefined variable.", *pos).with_length(1 + name.len()))
}
},
Expr::NsVar { module, name } => self.eval_module_var(module, name, Pos { line: 1, col: 1 }),
Expr::QuotedString(pieces) => {
let saved = std::mem::replace(&mut self.in_supports_declaration, false);
let text = self.eval_template(pieces);
self.in_supports_declaration = saved;
Ok(Value::Str(SassStr {
text: text?,
quoted: true,
}))
}
Expr::Ident(pieces) => {
let saved = std::mem::replace(&mut self.in_supports_declaration, false);
let text = self.eval_template(pieces);
self.in_supports_declaration = saved;
Ok(Value::Str(SassStr {
text: text?,
quoted: false,
}))
}
Expr::Interp(inner) => {
let saved = std::mem::replace(&mut self.in_supports_declaration, false);
let v = self.eval_expr(inner);
self.in_supports_declaration = saved;
Ok(Value::Str(SassStr {
text: v?.to_interp(),
quoted: false,
}))
}
Expr::ModernIf(clauses) => self.eval_modern_if(clauses),
Expr::Paren(inner) => Ok(self.eval_expr(inner)?.without_slash()),
Expr::List {
items,
sep,
bracketed,
} => {
let mut vals = Vec::with_capacity(items.len());
for it in items {
vals.push(self.eval_expr(it)?);
}
Ok(Value::List(List {
items: vals,
sep: *sep,
bracketed: *bracketed,
keywords: None,
}))
}
Expr::Map(entries) => {
let mut map = Map { entries: Vec::new() };
for (k, v) in entries {
let key = self.eval_expr(k)?.without_slash();
let val = self.eval_expr(v)?;
if map.get(&key).is_some() {
return Err(Error::unpositioned("Duplicate key."));
}
map.insert(key, val);
}
Ok(Value::Map(map))
}
Expr::Unary { op, operand } => {
let v = self.eval_expr(operand)?.without_slash();
match op {
UnOp::Neg => match v {
Value::Number(n) => Ok(Value::Number(n.copy_units(-n.value))),
Value::Calc(_) => Err(Error::unpositioned(format!(
"Undefined operation \"-{}\".",
v.to_css(false)
))),
other => Ok(Value::Str(SassStr {
text: format!("-{}", other.to_css(false)),
quoted: false,
})),
},
UnOp::Plus => match v {
Value::Number(_) => Ok(v),
Value::Calc(_) => Err(Error::unpositioned(format!(
"Undefined operation \"+{}\".",
v.to_css(false)
))),
other => Ok(Value::Str(SassStr {
text: format!("+{}", other.to_css(false)),
quoted: false,
})),
},
UnOp::Not => Ok(Value::Bool(!v.is_truthy())),
}
}
Expr::Binary { op, lhs, rhs, pos } => {
let l = self.eval_expr(lhs)?;
match op {
BinOp::And => {
if l.is_truthy() {
self.eval_expr(rhs)
} else {
Ok(l)
}
}
BinOp::Or => {
if l.is_truthy() {
Ok(l)
} else {
self.eval_expr(rhs)
}
}
_ => {
let r = self.eval_expr(rhs)?;
eval_binary(*op, l.without_slash(), r.without_slash(), *pos)
}
}
}
Expr::Div {
lhs, rhs, slash, pos, ..
} => {
let l = self.eval_expr(lhs)?;
let r = self.eval_expr(rhs)?;
eval_div(l, r, *slash, *pos)
}
Expr::Calc { inner, .. } => {
if !self.in_plain_css {
if let Some(callable) = self.lookup_function("calc") {
let arg = self.eval_expr(inner)?.without_slash();
let f = crate::value::SassFunction {
name: "calc".to_string(),
css: false,
user: Some(callable as Rc<dyn std::any::Any>),
};
return self
.invoke_function_ref(&f, vec![arg], Vec::new(), Pos { line: 0, col: 0 })
.map(Value::without_slash);
}
}
let node = self.eval_calc(inner)?;
if self.in_supports_declaration {
return Ok(Value::Calc(node));
}
match node {
CalcNode::Number(n) => Ok(Value::Number(n)),
CalcNode::Str(s) if is_complete_calculation(&s) => Ok(Value::Str(SassStr {
text: s,
quoted: false,
})),
other => Ok(Value::Calc(other)),
}
}
Expr::InterpFunc { name, args, pos } => {
let fname = self.eval_template(name)?;
if args.iter().any(|a| a.name.is_some()) {
return Err(Error::at(
"Plain CSS functions don't support keyword arguments.",
*pos,
));
}
let mut parts: Vec<String> = Vec::with_capacity(args.len());
for a in args {
let v = self.eval_expr(&a.value)?;
parts.push(v.to_css(self.compressed()));
}
Ok(Value::Str(SassStr {
text: format!("{fname}({})", parts.join(", ")),
quoted: false,
}))
}
Expr::Func {
name,
args,
pos,
length,
module,
} => {
if let Some(ns) = module {
return self.eval_module_call(ns, name, args, *pos, *length);
}
if self.in_plain_css
&& !is_supports_calc_function(name)
&& !name.eq_ignore_ascii_case("calc")
&& !args.iter().any(|a| a.splat || a.name.is_some())
{
let mut parts: Vec<String> = Vec::with_capacity(args.len());
for a in args {
parts.push(self.eval_expr(&a.value)?.to_css(self.compressed()));
}
return Ok(Value::Str(SassStr {
text: format!("{name}({})", parts.join(", ")),
quoted: false,
}));
}
if self.in_supports_declaration
&& is_supports_calc_function(name)
&& self.lookup_function(name).is_none()
{
return self.eval_supports_calc_func(name, args, *pos);
}
if name == "if" {
return self.eval_if_function(args, *pos);
}
if !name.starts_with("--") {
if let Some(func) = self.lookup_function(name) {
return self.call_function(&func, args, Some((*pos, *length)));
}
}
if !self.star_user_modules.is_empty() && !is_private_member(name) {
let hits: Vec<(Rc<Module>, Rc<UserCallable>)> = self
.star_user_modules
.iter()
.filter_map(|m| m.function(name).map(|f| (Rc::clone(m), f)))
.collect();
if hits.len() > 1 {
return Err(Error::at(
"This function is available from multiple global modules.".to_string(),
*pos,
));
}
if let Some((m, f)) = hits.into_iter().next() {
return self.call_user_module_function(&m, &f, args, Some((*pos, *length)));
}
}
if name.eq_ignore_ascii_case("calc") && args.is_empty() {
return Err(Error::at("Missing argument.", *pos));
}
if is_calc_function(name) && args.iter().any(|a| a.splat) {
return Err(Error::at("Rest arguments can't be used with calculations.", *pos));
}
if is_pure_calc_math_function(name)
&& self.lookup_function(name).is_none()
&& !args.iter().any(|a| a.splat || a.name.is_some())
{
if let Some(v) = self.try_eval_calc_math_call(name, args, *pos)? {
return Ok(v);
}
}
if name.eq_ignore_ascii_case("calc-size")
&& self.lookup_function(name).is_none()
&& !args.iter().any(|a| a.splat || a.name.is_some())
{
return self.eval_calc_size(args, *pos);
}
if name.eq_ignore_ascii_case("clamp")
&& self.lookup_function(name).is_none()
&& args.len() == 3
&& !args.iter().any(|a| a.splat || a.name.is_some())
{
return self.try_eval_clamp(args, *pos);
}
if name.eq_ignore_ascii_case("abs")
&& self.lookup_function(name).is_none()
&& args.len() == 1
&& args[0].name.is_none()
&& !args[0].splat
&& expr_contains_calc_substitution(&args[0].value)
{
let node = self.eval_calc(&args[0].value)?;
return Ok(Value::Str(SassStr {
text: format!("abs({})", node.to_calc_css(self.compressed())),
quoted: false,
}));
}
if name.eq_ignore_ascii_case("round")
&& self.lookup_function(name).is_none()
&& (1..=3).contains(&args.len())
&& !args.iter().any(|a| a.splat || a.name.is_some())
{
if args.iter().any(|a| expr_has_non_calc_op(&a.value)) {
if args.len() > 1 {
return Err(Error::at(
format!("Only 1 argument allowed, but {} were passed.", args.len()),
*pos,
));
}
} else if let Ok(nodes) =
args.iter()
.map(|a| self.eval_calc(&a.value))
.collect::<Result<Vec<CalcNode>, Error>>()
{
let simplified = |i: usize, n: &CalcNode| match n {
CalcNode::Number(_) => true,
CalcNode::Str(s) => {
i == 0
&& nodes.len() >= 2
&& matches!(
s.to_ascii_lowercase().as_str(),
"nearest" | "up" | "down" | "to-zero"
)
}
_ => false,
};
if !nodes.iter().enumerate().all(|(i, n)| simplified(i, n)) {
return Ok(Value::Calc(CalcNode::Func {
name: "round".to_string(),
args: nodes,
}));
}
}
}
if (name.eq_ignore_ascii_case("min") || name.eq_ignore_ascii_case("max"))
&& self.lookup_function(name).is_none()
&& !args.is_empty()
&& !args.iter().any(|a| a.splat || a.name.is_some())
&& !args.iter().any(|a| expr_has_non_calc_op(&a.value))
{
if let Ok(nodes) = args
.iter()
.map(|a| self.eval_calc(&a.value))
.collect::<Result<Vec<CalcNode>, Error>>()
{
if !nodes.iter().all(|n| matches!(n, CalcNode::Number(_))) {
return Ok(Value::Calc(CalcNode::Func {
name: name.to_ascii_lowercase(),
args: nodes,
}));
}
}
}
let (mut pos_args, mut named, _) = self.eval_call_args(args)?;
if matches!(
name.as_str(),
"variable-exists"
| "global-variable-exists"
| "mixin-exists"
| "function-exists"
| "content-exists"
| "get-function"
| "call"
| "keywords"
) {
for v in &mut pos_args {
*v = std::mem::replace(v, Value::Null).without_slash();
}
for (_, v) in &mut named {
*v = std::mem::replace(v, Value::Null).without_slash();
}
if let Some(r) = self.try_meta_eval_call(name, &pos_args, &named, *pos) {
return r;
}
}
if name == "alpha"
&& named.is_empty()
&& !pos_args.is_empty()
&& pos_args
.iter()
.all(|v| matches!(v, Value::Str(s) if !s.quoted && s.text.contains('=')))
{
let inner = pos_args
.iter()
.map(|v| v.to_css(false))
.collect::<Vec<_>>()
.join(", ");
return Ok(Value::Str(SassStr {
text: format!("alpha({inner})"),
quoted: false,
}));
}
if !crate::builtins::is_builtin(name) {
for m in self.star_modules.clone() {
if crate::builtins::module_has_member(&m, name) {
for v in &mut pos_args {
*v = std::mem::replace(v, Value::Null).without_slash();
}
for (n, v) in &mut named {
*v = std::mem::replace(v, Value::Null).without_slash();
let _ = n;
}
return crate::builtins::call_module(&m, name, &pos_args, &named, *pos)
.map(Value::without_slash);
}
}
}
if crate::builtins::is_builtin(name) {
for v in &mut pos_args {
*v = std::mem::replace(v, Value::Null).without_slash();
}
for (_, v) in &mut named {
*v = std::mem::replace(v, Value::Null).without_slash();
}
}
crate::builtins::call(name, &pos_args, &named, *pos).map(Value::without_slash)
}
}
}
fn eval_module_call(
&mut self,
ns: &str,
member: &str,
args: &[CallArg],
pos: Pos,
length: usize,
) -> Result<Value, Error> {
if let Some(module) = self.used_user_modules.get(ns).cloned() {
if is_private_member(member) {
return Err(Error::at(
"Private members can't be accessed from outside their modules.".to_string(),
pos,
));
}
if let Some(func) = module.function(member) {
let exec = module.fn_origin(member).unwrap_or(module);
return self.call_user_module_function(&exec, &func, args, Some((pos, length)));
}
if let Some(v) = self.try_forwarded_builtin_call(&module, member, args, pos)? {
return Ok(v);
}
return Err(Error::at("Undefined function.".to_string(), pos));
}
let module = match self.used_modules.get(ns) {
Some(m) => m.clone(),
None => {
return Err(Error::at(
format!("There is no module with the namespace \"{ns}\"."),
pos,
));
}
};
let (mut pos_args, mut named, _) = self.eval_call_args(args)?;
for v in &mut pos_args {
*v = std::mem::replace(v, Value::Null).without_slash();
}
for (_, v) in &mut named {
*v = std::mem::replace(v, Value::Null).without_slash();
}
if module == "meta" {
if let Some(r) = self.try_meta_eval_call(member, &pos_args, &named, pos) {
return r;
}
}
crate::builtins::call_module(&module, member, &pos_args, &named, pos).map(Value::without_slash)
}
fn try_meta_eval_call(
&mut self,
member: &str,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
) -> Option<Result<Value, Error>> {
match member {
"variable-exists" => Some(self.meta_variable_exists(pos_args, named, pos, false)),
"global-variable-exists" => Some(self.meta_variable_exists(pos_args, named, pos, true)),
"mixin-exists" => Some(self.meta_mixin_exists(pos_args, named, pos)),
"function-exists" => Some(self.meta_function_exists(pos_args, named, pos)),
"content-exists" => Some(self.meta_content_exists(pos_args, pos)),
"get-function" => Some(self.meta_get_function(pos_args, named, pos)),
"get-mixin" => Some(self.meta_get_mixin(pos_args, named, pos)),
"call" => Some(self.meta_call(pos_args, named, pos)),
"module-variables" => Some(self.meta_module_members(pos_args, named, pos, MemberKind::Variable)),
"module-functions" => Some(self.meta_module_members(pos_args, named, pos, MemberKind::Function)),
"module-mixins" => Some(self.meta_module_members(pos_args, named, pos, MemberKind::Mixin)),
"accepts-content" => Some(self.meta_accepts_content(pos_args, named, pos)),
"keywords" => Some(Self::meta_keywords(pos_args, named, pos)),
_ => None,
}
}
fn meta_keywords(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let v = pos_args
.first()
.or_else(|| named.iter().find(|(n, _)| n == "args").map(|(_, v)| v))
.ok_or_else(|| Error::at("Missing argument $args.".to_string(), pos))?;
match v {
Value::List(l) if l.keywords.is_some() => Ok(Value::Map(Map {
entries: l.keywords.clone().unwrap_or_default(),
})),
other => Err(Error::at(
format!("$args: {} is not an argument list.", other.to_css(false)),
pos,
)),
}
}
fn meta_accepts_content(
&self,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
) -> Result<Value, Error> {
let v = pos_args
.first()
.or_else(|| named.iter().find(|(n, _)| n == "mixin").map(|(_, v)| v))
.ok_or_else(|| Error::at("Missing argument $mixin.".to_string(), pos))?;
let mixin = match v {
Value::Mixin(m) => m,
other => {
return Err(Error::at(
format!("$mixin: {} is not a mixin reference.", other.to_css(false)),
pos,
))
}
};
let accepts = match &mixin.user {
Some(any) => Rc::clone(any)
.downcast::<UserCallable>()
.map(|c| body_uses_content(&c.def.body))
.unwrap_or(false),
None => mixin.name == "apply",
};
Ok(Value::Bool(accepts))
}
fn meta_get_function(
&self,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
) -> Result<Value, Error> {
let params = ["name", "css", "module"];
if pos_args.len() > params.len() {
return Err(Error::at(
format!(
"Only {} arguments allowed, but {} were passed.",
params.len(),
pos_args.len()
),
pos,
));
}
let arg = |i: usize| -> Option<&Value> {
pos_args
.get(i)
.or_else(|| named.iter().find(|(n, _)| n == params[i]).map(|(_, v)| v))
};
let name = match arg(0) {
Some(Value::Str(s)) => s.text.clone(),
Some(other) => {
return Err(Error::at(
format!("$name: {} is not a string.", other.to_css(false)),
pos,
))
}
None => return Err(Error::at("Missing argument $name.", pos)),
};
let css = matches!(arg(1), Some(v) if v.is_truthy());
if let Some(module_v) = arg(2) {
match module_v {
Value::Null => {}
Value::Str(s) => return self.get_function_from_module(&name, &s.text, pos),
other => {
return Err(Error::at(
format!("$module: {} is not a string.", other.to_css(false)),
pos,
))
}
}
}
if css {
return Ok(Value::Function(SassFunction {
name,
css: true,
user: None,
}));
}
let key = normalize_arg_name(&name);
if let Some(f) = self.lookup_function_norm(&key) {
return Ok(Value::Function(SassFunction {
name,
css: false,
user: Some(f as Rc<dyn std::any::Any>),
}));
}
if !is_private_member(&name) {
for m in &self.star_user_modules {
if let Some(f) = m.function(&name) {
return Ok(Value::Function(SassFunction {
name,
css: false,
user: Some(Rc::clone(&f) as Rc<dyn std::any::Any>),
}));
}
}
}
if crate::builtins::is_builtin(&name) {
return Ok(Value::Function(SassFunction {
name,
css: false,
user: None,
}));
}
Err(Error::at(format!("Function not found: {name}"), pos))
}
fn meta_get_mixin(
&self,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
) -> Result<Value, Error> {
let params = ["name", "module"];
if pos_args.len() > params.len() {
return Err(Error::at(
format!(
"Only {} arguments allowed, but {} were passed.",
params.len(),
pos_args.len()
),
pos,
));
}
let arg = |i: usize| -> Option<&Value> {
pos_args
.get(i)
.or_else(|| named.iter().find(|(n, _)| n == params[i]).map(|(_, v)| v))
};
let name = match arg(0) {
Some(Value::Str(s)) => s.text.clone(),
Some(other) => {
return Err(Error::at(
format!("$name: {} is not a string.", other.to_css(false)),
pos,
))
}
None => return Err(Error::at("Missing argument $name.", pos)),
};
if let Some(module_val) = arg(1) {
if !matches!(module_val, Value::Null) {
let module_name = match module_val {
Value::Str(s) => s.text.clone(),
other => {
return Err(Error::at(
format!("$module: {} is not a string.", other.to_css(false)),
pos,
))
}
};
return self.get_mixin_from_module(&name, &module_name, pos);
}
}
let key = normalize_arg_name(&name);
if let Some(m) = self.lookup_mixin_norm(&key) {
return Ok(Value::Mixin(SassMixin {
name,
user: Some(m as Rc<dyn std::any::Any>),
module: None,
}));
}
if !self.star_user_modules.is_empty() && !is_private_member(&name) {
let hits: Vec<&Rc<Module>> = self
.star_user_modules
.iter()
.filter(|m| m.mixin(&name).is_some())
.collect();
if hits.len() > 1 {
return Err(Error::at(
"This mixin is available from multiple global modules.",
pos,
));
}
if let Some(module) = hits.into_iter().next() {
let m = module
.mixin(&name)
.ok_or_else(|| Error::at(format!("Mixin not found: {name}"), pos))?;
return Ok(Value::Mixin(SassMixin {
name,
user: Some(Rc::clone(&m) as Rc<dyn std::any::Any>),
module: Some(Rc::clone(module) as Rc<dyn std::any::Any>),
}));
}
}
Err(Error::at(format!("Mixin not found: {name}"), pos))
}
fn get_function_from_module(&self, name: &str, module_name: &str, pos: Pos) -> Result<Value, Error> {
if let Some(module) = self.used_user_modules.get(module_name) {
if is_private_member(name) {
return Err(Error::at(
"Private members can't be accessed from outside their modules.".to_string(),
pos,
));
}
if let Some(f) = module.function(name) {
return Ok(Value::Function(SassFunction {
name: name.to_string(),
css: false,
user: Some(Rc::clone(&f) as Rc<dyn std::any::Any>),
}));
}
return Err(Error::at(format!("Function not found: {name}"), pos));
}
if let Some(builtin) = self.used_modules.get(module_name) {
if crate::builtins::module_has_member(builtin, name) {
let global = crate::builtins::module_member_to_global(builtin, name)
.unwrap_or(name)
.to_string();
return Ok(Value::Function(SassFunction {
name: global,
css: false,
user: None,
}));
}
return Err(Error::at(format!("Function not found: {name}"), pos));
}
Err(Error::at(
format!("There is no module with the namespace \"{module_name}\"."),
pos,
))
}
fn get_mixin_from_module(&self, name: &str, module_name: &str, pos: Pos) -> Result<Value, Error> {
if let Some(module) = self.used_user_modules.get(module_name) {
if is_private_member(name) {
return Err(Error::at(
"Private members can't be accessed from outside their modules.".to_string(),
pos,
));
}
if let Some(m) = module.mixin(name) {
return Ok(Value::Mixin(SassMixin {
name: name.to_string(),
user: Some(Rc::clone(&m) as Rc<dyn std::any::Any>),
module: Some(Rc::clone(module) as Rc<dyn std::any::Any>),
}));
}
return Err(Error::at(format!("Mixin not found: {name}"), pos));
}
if self.used_modules.contains_key(module_name) {
if is_builtin_mixin(module_name, name) {
return Ok(Value::Mixin(SassMixin {
name: name.to_string(),
user: None,
module: None,
}));
}
return Err(Error::at(format!("Mixin not found: {name}"), pos));
}
Err(Error::at(
format!("There is no module with the namespace \"{module_name}\"."),
pos,
))
}
fn meta_call(&mut self, pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let (func_val, rest_pos): (Value, Vec<Value>) = if let Some(first) = pos_args.first() {
(first.clone(), pos_args[1..].to_vec())
} else if let Some((_, v)) = named.iter().find(|(n, _)| n == "function") {
(v.clone(), Vec::new())
} else {
return Err(Error::at("Missing argument $function.", pos));
};
let rest_named: Vec<(String, Value)> =
named.iter().filter(|(n, _)| n != "function").cloned().collect();
match func_val {
Value::Function(f) => self.invoke_function_ref(&f, rest_pos, rest_named, pos),
Value::Str(s) => {
let f = SassFunction {
name: s.text.clone(),
css: false,
user: self
.lookup_function_norm(&normalize_arg_name(&s.text))
.map(|c| c as Rc<dyn std::any::Any>),
};
self.invoke_function_ref(&f, rest_pos, rest_named, pos)
}
other => Err(Error::at(
format!("$function: {} is not a function reference.", other.to_css(false)),
pos,
)),
}
}
fn invoke_function_ref(
&mut self,
f: &SassFunction,
pos_args: Vec<Value>,
named: Vec<(String, Value)>,
pos: Pos,
) -> Result<Value, Error> {
if let Some(any) = &f.user {
if let Ok(callable) = Rc::clone(any).downcast::<UserCallable>() {
let saved_scopes = std::mem::replace(&mut self.scopes, callable.env.clone());
let saved_semi = std::mem::replace(&mut self.scope_semi_global, callable.env_semi.clone());
let saved_fns = std::mem::replace(&mut self.functions, callable.env_fns.clone());
let saved_mixins = std::mem::replace(&mut self.mixins, callable.env_mixins.clone());
self.push_scope(false);
let result = self
.bind_evaled_into_scope(
&callable.def.params,
(pos_args, named, ListSep::Comma),
&callable.def.name,
)
.and_then(|()| {
self.in_mixin.push(false);
let r = self.run_fn_body(&callable.def.body);
self.in_mixin.pop();
r
});
self.pop_scope();
self.scopes = saved_scopes;
self.scope_semi_global = saved_semi;
self.functions = saved_fns;
self.mixins = saved_mixins;
return match result? {
Some(v) => Ok(v.without_slash()),
None => Err(Error::unpositioned(format!(
"Function {}() did not @return a value.",
callable.def.name
))),
};
}
}
if f.css {
let mut parts: Vec<String> = pos_args.iter().map(|v| v.to_css(false)).collect();
for (n, v) in &named {
parts.push(format!("${n}: {}", v.to_css(false)));
}
return Ok(Value::Str(SassStr {
text: format!("{}({})", f.name, parts.join(", ")),
quoted: false,
}));
}
if let Some(r) = self.try_meta_eval_call(&f.name, &pos_args, &named, pos) {
return r;
}
crate::builtins::call(&f.name, &pos_args, &named, pos).map(Value::without_slash)
}
fn exists_name_module_args(
&self,
pos_args: &[Value],
named: &[(String, Value)],
fname: &str,
pos: Pos,
allow_module: bool,
) -> Result<(String, Option<String>), Error> {
let max = if allow_module { 2 } else { 1 };
if pos_args.len() > max {
return Err(Error::at(
format!(
"Only {max} argument{} allowed, but {} were passed.",
if max == 1 { "" } else { "s" },
pos_args.len()
),
pos,
));
}
let name_v = pos_args
.first()
.or_else(|| named.iter().find(|(n, _)| n == "name").map(|(_, v)| v))
.ok_or_else(|| Error::at(format!("Missing argument $name for {fname}()."), pos))?;
let name = match name_v {
Value::Str(s) => s.text.clone(),
other => {
return Err(Error::at(
format!("$name: {} is not a string.", other.to_css(false)),
pos,
))
}
};
let module = if allow_module {
let m = pos_args
.get(1)
.or_else(|| named.iter().find(|(n, _)| n == "module").map(|(_, v)| v));
match m {
None | Some(Value::Null) => None,
Some(Value::Str(s)) => Some(s.text.clone()),
Some(other) => {
return Err(Error::at(
format!("$module: {} is not a string.", other.to_css(false)),
pos,
))
}
}
} else {
None
};
Ok((name, module))
}
fn module_member_exists(&self, ns: &str, name: &str, kind: MemberKind, pos: Pos) -> Result<bool, Error> {
if let Some(m) = self.used_user_modules.get(ns) {
return Ok(match kind {
MemberKind::Function => m.function(name).is_some(),
MemberKind::Mixin => m.mixin(name).is_some(),
MemberKind::Variable => m.var(name).is_some(),
});
}
if let Some(builtin) = self.used_modules.get(ns).cloned() {
return Ok(match kind {
MemberKind::Function => crate::builtins::module_has_member(&builtin, name),
MemberKind::Mixin => builtin == "meta" && matches!(name, "load-css" | "apply"),
MemberKind::Variable => crate::builtins::module_var(&builtin, name, pos).is_ok(),
});
}
Err(Error::at(
format!("There is no module with the namespace \"{ns}\"."),
pos,
))
}
fn meta_module_members(
&self,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
kind: MemberKind,
) -> Result<Value, Error> {
let fname = match kind {
MemberKind::Function => "module-functions",
MemberKind::Mixin => "module-mixins",
MemberKind::Variable => "module-variables",
};
if pos_args.len() > 1 {
return Err(Error::at(
format!("Only 1 argument allowed, but {} were passed.", pos_args.len()),
pos,
));
}
let v = pos_args
.first()
.or_else(|| named.iter().find(|(n, _)| n == "module").map(|(_, v)| v))
.ok_or_else(|| Error::at(format!("Missing argument $module for {fname}()."), pos))?;
let ns = match v {
Value::Str(s) => s.text.clone(),
other => {
return Err(Error::at(
format!("$module: {} is not a string.", other.to_css(false)),
pos,
))
}
};
let Some(module) = self.used_user_modules.get(&ns).cloned() else {
if let Some(builtin) = self.used_modules.get(&ns) {
let names: Vec<&str> = match (builtin.as_str(), kind) {
("meta", MemberKind::Function) => crate::builtins::META_FUNCTION_NAMES.to_vec(),
("meta", MemberKind::Mixin) => crate::builtins::META_MIXIN_NAMES.to_vec(),
_ => Vec::new(),
};
let entries: Vec<(Value, Value)> = names
.into_iter()
.map(|name| {
let key = Value::Str(SassStr {
text: name.to_string(),
quoted: true,
});
let val = match kind {
MemberKind::Function => Value::Function(SassFunction {
name: name.to_string(),
css: false,
user: None,
}),
MemberKind::Mixin => Value::Mixin(SassMixin {
name: name.to_string(),
user: None,
module: None,
}),
MemberKind::Variable => Value::Null,
};
(key, val)
})
.collect();
return Ok(Value::Map(Map { entries }));
}
return Err(Error::at(
format!("There is no module with the namespace \"{ns}\"."),
pos,
));
};
let mut names: Vec<String> = match kind {
MemberKind::Variable => module.vars.borrow().keys().cloned().collect(),
MemberKind::Function => module.functions.borrow().keys().cloned().collect(),
MemberKind::Mixin => module.mixins.borrow().keys().cloned().collect(),
};
names.retain(|n| !is_private_member(n));
names.sort();
let entries: Vec<(Value, Value)> = names
.into_iter()
.map(|name| {
let key = Value::Str(SassStr {
text: name.replace('_', "-"),
quoted: true,
});
let val = match kind {
MemberKind::Variable => module.var(&name).unwrap_or(Value::Null),
MemberKind::Function => Value::Function(SassFunction {
name: name.clone(),
css: false,
user: module
.function(&name)
.map(|f| Rc::clone(&f) as Rc<dyn std::any::Any>),
}),
MemberKind::Mixin => Value::Mixin(SassMixin {
name: name.clone(),
user: module
.mixin(&name)
.map(|m| Rc::clone(&m) as Rc<dyn std::any::Any>),
module: Some(Rc::clone(&module) as Rc<dyn std::any::Any>),
}),
};
(key, val)
})
.collect();
Ok(Value::Map(Map { entries }))
}
fn meta_variable_exists(
&self,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
global: bool,
) -> Result<Value, Error> {
let fname = if global {
"global-variable-exists"
} else {
"variable-exists"
};
let (name, module) = self.exists_name_module_args(pos_args, named, fname, pos, global)?;
if let Some(ns) = module {
return Ok(Value::Bool(self.module_member_exists(
&ns,
&name,
MemberKind::Variable,
pos,
)?));
}
let key = normalize_arg_name(&name);
let scopes: &[Scope] = if global { &self.scopes[..1] } else { &self.scopes };
let found = scopes
.iter()
.any(|s| s.borrow().keys().any(|k| normalize_arg_name(k) == key));
if found {
return Ok(Value::Bool(true));
}
let count = self.star_member_count(&name, MemberKind::Variable);
if count > 1 {
return Err(Error::at(
"This variable is available from multiple global modules.",
pos,
));
}
Ok(Value::Bool(count >= 1))
}
fn meta_mixin_exists(
&self,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
) -> Result<Value, Error> {
let (name, module) = self.exists_name_module_args(pos_args, named, "mixin-exists", pos, true)?;
if let Some(ns) = module {
return Ok(Value::Bool(self.module_member_exists(
&ns,
&name,
MemberKind::Mixin,
pos,
)?));
}
let key = normalize_arg_name(&name);
let local = self.lookup_mixin_norm(&key).is_some();
if local {
return Ok(Value::Bool(true));
}
let count = self.star_member_count(&name, MemberKind::Mixin);
if count > 1 {
return Err(Error::at(
"This mixin is available from multiple global modules.",
pos,
));
}
Ok(Value::Bool(count >= 1))
}
fn meta_function_exists(
&self,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
) -> Result<Value, Error> {
let (name, module) = self.exists_name_module_args(pos_args, named, "function-exists", pos, true)?;
if let Some(ns) = module {
return Ok(Value::Bool(self.module_member_exists(
&ns,
&name,
MemberKind::Function,
pos,
)?));
}
let key = normalize_arg_name(&name);
let user = self.lookup_function_norm(&key).is_some();
if user {
return Ok(Value::Bool(true));
}
let count = self.star_member_count(&name, MemberKind::Function);
if count > 1 {
return Err(Error::at(
"This function is available from multiple global modules.",
pos,
));
}
Ok(Value::Bool(count >= 1 || crate::builtins::is_builtin(&name)))
}
fn star_member_count(&self, name: &str, kind: MemberKind) -> usize {
if is_private_member(name) {
return 0;
}
self.star_user_modules
.iter()
.filter(|m| match kind {
MemberKind::Variable => m.var(name).is_some(),
MemberKind::Mixin => m.mixin(name).is_some(),
MemberKind::Function => m.function(name).is_some(),
})
.count()
}
fn meta_content_exists(&self, pos_args: &[Value], pos: Pos) -> Result<Value, Error> {
if !pos_args.is_empty() {
return Err(Error::at(
format!("Only 0 arguments allowed, but {} were passed.", pos_args.len()),
pos,
));
}
if self.in_mixin.last().copied() != Some(true) {
return Err(Error::at(
"content-exists() may only be called within a mixin.",
pos,
));
}
let has = matches!(self.content_stack.last(), Some(Some(_)));
Ok(Value::Bool(has))
}
fn try_forwarded_builtin_call(
&mut self,
module: &Rc<Module>,
member: &str,
args: &[CallArg],
pos: Pos,
) -> Result<Option<Value>, Error> {
for fb in &module.forwarded_builtins {
let bare = match &fb.prefix {
Some(p) => match member.strip_prefix(p.as_str()) {
Some(rest) => rest,
None => continue,
},
None => member,
};
if fb.visible(bare) && crate::builtins::module_has_member(&fb.module, bare) {
let (mut pos_args, mut named, _) = self.eval_call_args(args)?;
for v in &mut pos_args {
*v = std::mem::replace(v, Value::Null).without_slash();
}
for (_, v) in &mut named {
*v = std::mem::replace(v, Value::Null).without_slash();
}
return Ok(Some(
crate::builtins::call_module(&fb.module, bare, &pos_args, &named, pos)?.without_slash(),
));
}
}
Ok(None)
}
fn call_user_module_function(
&mut self,
module: &Rc<Module>,
func: &Rc<UserCallable>,
args: &[CallArg],
call: Option<(Pos, usize)>,
) -> Result<Value, Error> {
let evaled = self.eval_call_args(args)?;
let saved_member = call.map(|(pos, len)| self.enter_call(pos, len, &format!("{}()", func.def.name)));
let saved = self.enter_module(module);
let saved_file = self.enter_module_file(module);
let saved_scopes = std::mem::replace(&mut self.scopes, func.env.clone());
let saved_semi = std::mem::replace(&mut self.scope_semi_global, func.env_semi.clone());
let saved_fns = std::mem::replace(&mut self.functions, func.env_fns.clone());
let saved_mixins = std::mem::replace(&mut self.mixins, func.env_mixins.clone());
self.push_scope(false);
let result = self
.bind_evaled_into_scope(&func.def.params, evaled, &func.def.name)
.and_then(|()| self.run_fn_body(&func.def.body));
self.pop_scope();
self.scopes = saved_scopes;
self.scope_semi_global = saved_semi;
self.functions = saved_fns;
self.mixins = saved_mixins;
self.leave_module_file(saved_file);
self.leave_module(saved);
if let Some(saved_member) = saved_member {
self.leave_call(saved_member);
}
match result? {
Some(v) => Ok(v.without_slash()),
None => Err(Error::unpositioned(format!(
"Function {}() did not @return a value.",
func.def.name
))),
}
}
fn enter_module_file(&mut self, module: &Rc<Module>) -> Option<(String, Rc<str>, Option<String>)> {
if module.diag_url.is_empty() {
return None;
}
let source = self.source_for(&module.diag_url);
let dir = if module.file_dir.is_empty() {
None
} else {
Some(module.file_dir.clone())
};
self.current_url_stamp = 0;
Some((
std::mem::replace(&mut self.current_url, module.diag_url.clone()),
std::mem::replace(&mut self.current_source, source),
std::mem::replace(&mut self.current_file_dir, dir),
))
}
fn leave_module_file(&mut self, saved: Option<(String, Rc<str>, Option<String>)>) {
if let Some((url, source, dir)) = saved {
self.current_url = url;
self.current_url_stamp = 0;
self.current_source = source;
self.current_file_dir = dir;
}
}
fn module_diag_url(&self, url: &str, key: &str) -> String {
let base = key.rsplit(['/', '\\']).next().unwrap_or(key);
if base.is_empty() {
url.to_string()
} else {
base.to_string()
}
}
fn enter_module(&mut self, module: &Rc<Module>) -> SavedModuleEnv {
let module_scope = std::rc::Rc::clone(&module.vars);
SavedModuleEnv {
scopes: std::mem::replace(&mut self.scopes, vec![module_scope]),
scope_semi_global: std::mem::replace(&mut self.scope_semi_global, vec![true]),
functions: std::mem::replace(&mut self.functions, vec![std::rc::Rc::clone(&module.functions)]),
mixins: std::mem::replace(&mut self.mixins, vec![std::rc::Rc::clone(&module.mixins)]),
used_modules: std::mem::replace(&mut self.used_modules, module.used_builtin_modules.clone()),
star_modules: std::mem::replace(&mut self.star_modules, module.star_builtin_modules.clone()),
used_user_modules: std::mem::replace(
&mut self.used_user_modules,
module.used_user_modules.clone(),
),
star_user_modules: std::mem::replace(
&mut self.star_user_modules,
module.star_user_modules.clone(),
),
write_back: Some(Rc::clone(module)),
}
}
fn leave_module(&mut self, saved: SavedModuleEnv) {
let _ = &saved.write_back;
self.scopes = saved.scopes;
self.scope_semi_global = saved.scope_semi_global;
self.functions = saved.functions;
self.mixins = saved.mixins;
self.used_modules = saved.used_modules;
self.star_modules = saved.star_modules;
self.used_user_modules = saved.used_user_modules;
self.star_user_modules = saved.star_user_modules;
}
fn eval_module_var(&self, ns: &str, name: &str, pos: Pos) -> Result<Value, Error> {
if let Some(module) = self.used_user_modules.get(ns) {
if is_private_member(name) {
return Err(Error::at(
"Private members can't be accessed from outside their modules.".to_string(),
pos,
));
}
return match module.var(name) {
Some(v) => Ok(v.without_slash()),
None => Err(Error::at("Undefined variable.".to_string(), pos)),
};
}
match self.used_modules.get(ns) {
Some(module) => crate::builtins::module_var(module, name, pos),
None => Err(Error::at(
format!("There is no module with the namespace \"{ns}\"."),
pos,
)),
}
}
fn eval_if_function(&mut self, args: &[CallArg], pos: Pos) -> Result<Value, Error> {
enum IfArg<'a> {
Lazy(&'a Expr),
Eager(Value),
}
fn slot_index(name: &str) -> Option<usize> {
match name {
"condition" => Some(0),
"if-true" => Some(1),
"if-false" => Some(2),
_ => None,
}
}
let mut by_pos: Vec<IfArg<'_>> = Vec::new();
let mut named: [Option<IfArg<'_>>; 3] = [None, None, None];
for a in args {
if a.splat {
match self.eval_expr(&a.value)? {
Value::List(l) => {
if let Some(kw) = &l.keywords {
for (k, v) in kw {
if let Value::Str(s) = k {
match slot_index(&s.text) {
Some(i) => named[i] = Some(IfArg::Eager(v.clone())),
None => {
return Err(Error::at(
format!("if() has no argument named ${}.", s.text),
pos,
))
}
}
}
}
}
for item in l.items {
by_pos.push(IfArg::Eager(item));
}
}
Value::Map(m) => {
for (k, v) in m.entries {
let name = match k {
Value::Str(s) => s.text,
other => {
return Err(Error::at(
format!(
"Variable keyword argument map must have string keys.\n{} is not a string.",
other.to_css(false)
),
pos,
))
}
};
match slot_index(&name) {
Some(i) => named[i] = Some(IfArg::Eager(v)),
None => {
return Err(Error::at(
format!("if() has no argument named ${name}."),
pos,
))
}
}
}
}
other => by_pos.push(IfArg::Eager(other)),
}
continue;
}
match a.name.as_deref() {
Some(name) => match slot_index(name) {
Some(i) => named[i] = Some(IfArg::Lazy(&a.value)),
None => {
return Err(Error::at(format!("if() has no argument named ${name}."), pos));
}
},
None => by_pos.push(IfArg::Lazy(&a.value)),
}
}
let [cond, t_val, f_val] = named;
let mut pos_iter = by_pos.into_iter();
let cond = cond.or_else(|| pos_iter.next());
let t_val = t_val.or_else(|| pos_iter.next());
let f_val = f_val.or_else(|| pos_iter.next());
match (cond, t_val, f_val) {
(Some(c), Some(t), Some(f)) => {
let truthy = match c {
IfArg::Lazy(e) => self.eval_expr(e)?.is_truthy(),
IfArg::Eager(v) => v.is_truthy(),
};
let branch = if truthy { t } else { f };
match branch {
IfArg::Lazy(e) => Ok(self.eval_expr(e)?.without_slash()),
IfArg::Eager(v) => Ok(v.without_slash()),
}
}
_ => Err(Error::at(
"if() requires arguments $condition, $if-true, $if-false.",
pos,
)),
}
}
fn eval_modern_if(&mut self, clauses: &[IfClause]) -> Result<Value, Error> {
let mut verbatim: Option<Vec<String>> = None;
for clause in clauses {
let result = match &clause.condition {
None => CondEval::Bool(true),
Some(cond) => self.eval_if_cond(cond)?,
};
match (&mut verbatim, result) {
(None, CondEval::Bool(true)) => {
return Ok(self.eval_expr(&clause.value)?.without_slash());
}
(None, CondEval::Bool(false)) => {}
(None, CondEval::Css(rc)) => {
let value = self.eval_if_value(&clause.value)?;
verbatim = Some(vec![format!("{}: {}", rc.to_css(), value)]);
}
(Some(out), CondEval::Bool(true)) => {
let value = self.eval_if_value(&clause.value)?;
out.push(format!("else: {value}"));
}
(Some(_), CondEval::Bool(false)) => {}
(Some(out), CondEval::Css(rc)) => {
let value = self.eval_if_value(&clause.value)?;
out.push(format!("{}: {}", rc.to_css(), value));
}
}
}
match verbatim {
Some(parts) => Ok(Value::Str(SassStr {
text: format!("if({})", parts.join("; ")),
quoted: false,
})),
None => Ok(Value::Null),
}
}
fn eval_if_value(&mut self, expr: &Expr) -> Result<String, Error> {
let v = self.eval_expr(expr)?.without_slash();
Ok(serialize_if_value(&v))
}
fn eval_if_cond(&mut self, cond: &IfCond) -> Result<CondEval, Error> {
match cond {
IfCond::Sass(expr) => Ok(CondEval::Bool(self.eval_expr(expr)?.is_truthy())),
IfCond::Raw { pieces, .. } => {
let text = self.eval_template(pieces)?;
let mut collapsed = String::with_capacity(text.len());
let mut prev_ws = false;
for c in text.chars() {
if c.is_whitespace() {
prev_ws = true;
continue;
}
if prev_ws && !collapsed.is_empty() && c != ')' {
collapsed.push(' ');
}
prev_ws = false;
collapsed.push(c);
}
let collapsed = collapsed.replace("( ", "(");
Ok(CondEval::Css(RCond::Css(collapsed)))
}
IfCond::Not(inner) => match self.eval_if_cond(inner)? {
CondEval::Bool(b) => Ok(CondEval::Bool(!b)),
CondEval::Css(rc) => Ok(CondEval::Css(RCond::Not(Box::new(rc)))),
},
IfCond::Paren(inner) => match self.eval_if_cond(inner)? {
CondEval::Bool(b) => Ok(CondEval::Bool(b)),
CondEval::Css(rc) => Ok(CondEval::Css(RCond::Paren(Box::new(rc)))),
},
IfCond::And(items) => {
let mut residuals: Vec<RCond> = Vec::new();
for item in items {
match self.eval_if_cond(item)? {
CondEval::Bool(false) => return Ok(CondEval::Bool(false)),
CondEval::Bool(true) => {}
CondEval::Css(rc) => residuals.push(rc),
}
}
Ok(combine_residuals(residuals, true))
}
IfCond::Or(items) => {
let mut residuals: Vec<RCond> = Vec::new();
for item in items {
match self.eval_if_cond(item)? {
CondEval::Bool(true) => return Ok(CondEval::Bool(true)),
CondEval::Bool(false) => {}
CondEval::Css(rc) => residuals.push(rc),
}
}
Ok(combine_residuals(residuals, false))
}
}
}
fn eval_supports_calc_func(&mut self, name: &str, args: &[CallArg], pos: Pos) -> Result<Value, Error> {
if args.iter().any(|a| a.splat) {
return Err(Error::at("Rest arguments can't be used with calculations.", pos));
}
let mut parts = Vec::with_capacity(args.len());
for a in args {
let inner = self.eval_calc(&a.value)?.to_calc_css(self.compressed());
match &a.name {
Some(n) => parts.push(format!("${n}: {inner}")),
None => parts.push(inner),
}
}
Ok(Value::Str(SassStr {
text: format!("{name}({})", parts.join(", ")),
quoted: false,
}))
}
fn try_eval_calc_math_call(
&mut self,
name: &str,
args: &[CallArg],
_pos: Pos,
) -> Result<Option<Value>, Error> {
let mut nodes = Vec::with_capacity(args.len());
for a in args {
nodes.push(self.eval_calc(&a.value)?);
}
if nodes.iter().all(|n| matches!(n, CalcNode::Number(_))) {
return Ok(None);
}
if !nodes.iter().any(calc_node_has_opaque) {
return Ok(None);
}
let lname = name.to_ascii_lowercase();
let parts: Vec<String> = nodes.iter().map(|n| n.to_calc_css(self.compressed())).collect();
Ok(Some(Value::Str(SassStr {
text: format!("{lname}({})", parts.join(", ")),
quoted: false,
})))
}
fn try_eval_clamp(&mut self, args: &[CallArg], pos: Pos) -> Result<Value, Error> {
let mut nodes = Vec::with_capacity(args.len());
for a in args {
let node = self.eval_calc(&a.value)?;
if let Some(complex) = calc_complex_unit_operand(&node) {
return Err(Error::at(
format!(
"Number calc({}) isn't compatible with CSS calculations.",
complex.to_calc_css(false)
),
pos,
));
}
nodes.push(node);
}
if nodes.iter().all(|n| matches!(n, CalcNode::Number(_))) {
let values: Vec<Value> = nodes
.into_iter()
.map(|n| match n {
CalcNode::Number(num) => Value::Number(num),
other => Value::Calc(other),
})
.collect();
return crate::builtins::call("clamp", &values, &[], pos);
}
let parts: Vec<String> = nodes.iter().map(|n| n.to_calc_css(self.compressed())).collect();
Ok(Value::Str(SassStr {
text: format!("clamp({})", parts.join(", ")),
quoted: false,
}))
}
fn eval_calc_size(&mut self, args: &[CallArg], pos: Pos) -> Result<Value, Error> {
if args.is_empty() {
return Err(Error::at("Missing argument.", pos));
}
if args.len() > 2 {
return Err(Error::at(
format!("Only 2 arguments allowed, but {} were passed.", args.len()),
pos,
));
}
let mut parts = Vec::with_capacity(args.len());
for a in args {
parts.push(self.eval_calc(&a.value)?.to_calc_css(self.compressed()));
}
Ok(Value::Str(SassStr {
text: format!("calc-size({})", parts.join(", ")),
quoted: false,
}))
}
fn eval_calc(&mut self, expr: &Expr) -> Result<CalcNode, Error> {
match expr {
Expr::Binary { op, lhs, rhs, pos } => {
let calc_op = match op {
BinOp::Add => CalcOp::Add,
BinOp::Sub => CalcOp::Sub,
BinOp::Mul => CalcOp::Mul,
_ => return Err(Error::at("This operation can't be used in a calculation.", *pos)),
};
let l = self.eval_calc(lhs)?;
let r = self.eval_calc(rhs)?;
if self.in_supports_declaration {
return Ok(CalcNode::Op {
op: calc_op,
left: Box::new(l),
right: Box::new(r),
});
}
fold_calc(calc_op, l, r, *pos)
}
Expr::Div { lhs, rhs, pos, .. } => {
let l = self.eval_calc(lhs)?;
let r = self.eval_calc(rhs)?;
if self.in_supports_declaration {
return Ok(CalcNode::Op {
op: CalcOp::Div,
left: Box::new(l),
right: Box::new(r),
});
}
fold_calc(CalcOp::Div, l, r, *pos)
}
Expr::Unary {
op: UnOp::Neg,
operand,
} => {
let node = self.eval_calc(operand)?;
match node {
CalcNode::Number(n) => Ok(CalcNode::Number(n.copy_units(-n.value))),
other => Ok(CalcNode::Op {
op: CalcOp::Mul,
left: Box::new(CalcNode::Number(Number::unitless(-1.0))),
right: Box::new(other),
}),
}
}
Expr::Paren(inner) => {
let node = self.eval_calc(inner)?;
match node {
CalcNode::Str(s) => Ok(CalcNode::Str(format!("({s})"))),
other => Ok(other),
}
}
Expr::Calc { inner, .. } => {
if self.in_supports_declaration {
let s = self.eval_calc(inner)?.to_calc_css(self.compressed());
return Ok(CalcNode::Str(format!("calc({s})")));
}
let node = self.eval_calc(inner)?;
match node {
CalcNode::Str(s) if nested_calc_needs_parens(&s) => Ok(CalcNode::Str(format!("({s})"))),
other => Ok(other),
}
}
Expr::List {
items,
sep: ListSep::Space,
bracketed: false,
} => {
let has_subst = items.iter().any(expr_has_substitution);
if !has_subst
&& !items
.iter()
.any(|e| matches!(e, Expr::Var { .. } | Expr::NsVar { .. }))
{
return Err(Error::unpositioned("Missing math operator."));
}
let mut parts = Vec::with_capacity(items.len());
let mut any_str = false;
for it in items {
let node = self.eval_calc(it)?;
if matches!(node, CalcNode::Str(_)) {
any_str = true;
}
parts.push(node.to_calc_css(false));
}
if !has_subst && !any_str {
return Err(Error::unpositioned("Missing math operator."));
}
Ok(CalcNode::Str(parts.join(" ")))
}
other => {
let v = self.eval_expr(other)?;
if let Value::Str(s) = &v {
if !s.quoted {
if let Some(value) = calc_constant(&s.text) {
return Ok(CalcNode::Number(Number::unitless(value)));
}
}
}
match &v {
Value::Number(_) | Value::Calc(_) | Value::Slash(_, _) => {}
Value::Str(s) if !s.quoted => {}
other => {
return Err(Error::unpositioned(format!(
"Value {} can't be used in a calculation.",
calc_value_repr(other)
)));
}
}
Ok(value_to_calc_node(v))
}
}
}
}
fn literal_name_is_custom_property(property: &[TplPiece]) -> bool {
match property.first() {
Some(TplPiece::Lit(s)) => s.trim_start().starts_with("--"),
_ => false,
}
}
fn expr_has_substitution(e: &Expr) -> bool {
match e {
Expr::Interp(_) => true,
Expr::Func { name, .. } => name.eq_ignore_ascii_case("var") || name.eq_ignore_ascii_case("env"),
Expr::Ident(pieces) => pieces.iter().any(|p| match p {
TplPiece::Interp(_) => true,
TplPiece::Lit(s) => {
let lower = s.trim_start().to_ascii_lowercase();
lower.starts_with("var(") || lower.starts_with("env(")
}
}),
_ => false,
}
}
fn expr_contains_calc_substitution(e: &Expr) -> bool {
if expr_has_substitution(e) {
return true;
}
match e {
Expr::Binary { lhs, rhs, .. } => {
expr_contains_calc_substitution(lhs) || expr_contains_calc_substitution(rhs)
}
Expr::Div { lhs, rhs, .. } => {
expr_contains_calc_substitution(lhs) || expr_contains_calc_substitution(rhs)
}
Expr::Unary { operand, .. } => expr_contains_calc_substitution(operand),
Expr::Paren(inner) => expr_contains_calc_substitution(inner),
Expr::Calc { inner } => expr_contains_calc_substitution(inner),
Expr::List { items, .. } => items.iter().any(expr_contains_calc_substitution),
_ => false,
}
}
fn collect_global_var_decls(stmts: &[Stmt], out: &mut Vec<String>) {
for stmt in stmts {
match stmt {
Stmt::VarDecl(v) if v.is_global && v.namespace.is_none() => out.push(v.name.clone()),
Stmt::Rule(r) => collect_global_var_decls(&r.body, out),
Stmt::If(branches) => {
for b in branches {
collect_global_var_decls(&b.body, out);
}
}
Stmt::For { body, .. }
| Stmt::Each { body, .. }
| Stmt::While { body, .. }
| Stmt::Media { body, .. }
| Stmt::Supports { body, .. }
| Stmt::AtRoot { body, .. }
| Stmt::Keyframes { body, .. } => collect_global_var_decls(body, out),
Stmt::AtRule { body: Some(b), .. } => collect_global_var_decls(b, out),
_ => {}
}
}
}
fn reparent_nodes(nodes: Vec<OutNode>, parents: &[String]) -> Vec<OutNode> {
if parents.is_empty() {
return nodes;
}
let mut preserved: Vec<OutItem> = Vec::new();
let mut rest: Vec<OutNode> = Vec::new();
for n in nodes {
match n {
OutNode::Rule {
selectors,
linebreaks: _,
items,
lines,
extend_base,
} => {
if selectors.iter().any(|s| part_has_parent_ref(s)) {
preserved.push(OutItem::NestedRule { selectors, items });
} else {
rest.push(OutNode::Rule {
selectors: parents
.iter()
.flat_map(|p| selectors.iter().map(move |s| format!("{p} {s}")))
.collect(),
linebreaks: Vec::new(),
items,
lines,
extend_base,
});
}
}
OutNode::AtRule {
name,
prelude,
body,
has_block,
lines,
} => rest.push(OutNode::AtRule {
name,
prelude,
body: reparent_nodes(body, parents),
has_block,
lines,
}),
other => rest.push(other),
}
}
let mut out = Vec::new();
if !preserved.is_empty() {
out.push(OutNode::Rule {
selectors: parents.to_vec(),
linebreaks: Vec::new(),
items: preserved,
lines: SrcLines::default(),
extend_base: usize::MAX,
});
}
out.extend(rest);
out
}
fn expr_has_non_calc_op(e: &Expr) -> bool {
match e {
Expr::Binary { op, lhs, rhs, .. } => {
matches!(
op,
BinOp::Mod
| BinOp::Eq
| BinOp::Neq
| BinOp::Lt
| BinOp::Gt
| BinOp::Le
| BinOp::Ge
| BinOp::And
| BinOp::Or
| BinOp::SingleEq
) || expr_has_non_calc_op(lhs)
|| expr_has_non_calc_op(rhs)
}
Expr::Div { lhs, rhs, .. } => expr_has_non_calc_op(lhs) || expr_has_non_calc_op(rhs),
Expr::Unary { operand, .. } => expr_has_non_calc_op(operand),
Expr::Paren(inner) => expr_has_non_calc_op(inner),
Expr::List { items, .. } => items.iter().any(expr_has_non_calc_op),
_ => false,
}
}
fn calc_value_repr(v: &Value) -> String {
match v {
Value::Null => "null".to_string(),
Value::List(_) => format!("({})", v.to_css(false)),
other => other.to_css(false),
}
}
fn calc_constant(text: &str) -> Option<f64> {
match text.to_ascii_lowercase().as_str() {
"pi" => Some(std::f64::consts::PI),
"e" => Some(std::f64::consts::E),
"infinity" => Some(f64::INFINITY),
"-infinity" => Some(f64::NEG_INFINITY),
"nan" => Some(f64::NAN),
_ => None,
}
}
fn nested_calc_needs_parens(s: &str) -> bool {
if is_complete_calculation(s) {
return false;
}
let trimmed = s.trim_start();
let is_var = trimmed.len() >= 4 && trimmed[..4].eq_ignore_ascii_case("var(");
is_var
|| s.chars()
.any(|c| c.is_whitespace() || matches!(c, '*' | '/' | '\\'))
}
fn is_complete_calculation(s: &str) -> bool {
let s = s.trim();
let Some(open) = s.find('(') else { return false };
if !s.ends_with(')') {
return false;
}
let name = s[..open].trim().to_ascii_lowercase();
let is_calc_name = matches!(
name.as_str(),
"calc"
| "min"
| "max"
| "clamp"
| "round"
| "mod"
| "rem"
| "sin"
| "cos"
| "tan"
| "asin"
| "acos"
| "atan"
| "atan2"
| "pow"
| "sqrt"
| "exp"
| "log"
| "hypot"
| "abs"
| "sign"
| "calc-size"
);
if !is_calc_name {
return false;
}
let mut depth = 0u32;
for (i, c) in s.char_indices() {
match c {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
return i == s.len() - 1;
}
}
_ => {}
}
}
false
}
fn value_to_calc_node(v: Value) -> CalcNode {
match v.without_slash() {
Value::Number(n) => CalcNode::Number(n),
Value::Calc(node) => node,
other => CalcNode::Str(other.to_css(false)),
}
}
fn fold_calc(op: CalcOp, left: CalcNode, right: CalcNode, pos: Pos) -> Result<CalcNode, Error> {
if let (CalcNode::Number(a), CalcNode::Number(b)) = (&left, &right) {
if let Some(n) = fold_numbers(op, a, b, pos)? {
return Ok(CalcNode::Number(n));
}
}
if matches!(op, CalcOp::Add | CalcOp::Sub) {
for operand in [&left, &right] {
if let Some(node) = calc_complex_unit_operand(operand) {
return Err(Error::at(
format!(
"Number calc({}) isn't compatible with CSS calculations.",
node.to_calc_css(false)
),
pos,
));
}
}
}
Ok(CalcNode::Op {
op,
left: Box::new(left),
right: Box::new(right),
})
}
fn calc_complex_unit_operand(node: &CalcNode) -> Option<&CalcNode> {
match node {
CalcNode::Op {
op: CalcOp::Mul,
left,
right,
} if calc_node_carries_unit(left) && calc_node_carries_unit(right) => Some(node),
CalcNode::Op {
op: CalcOp::Div,
right,
..
} if calc_node_carries_unit(right) => Some(node),
_ => None,
}
}
fn calc_node_carries_unit(node: &CalcNode) -> bool {
match node {
CalcNode::Number(n) => !n.is_unitless(),
CalcNode::Str(_) => false,
CalcNode::Op {
op: CalcOp::Mul | CalcOp::Div,
left,
right,
} => calc_node_carries_unit(left) || calc_node_carries_unit(right),
CalcNode::Op { .. } => false,
CalcNode::Func { .. } => false,
}
}
fn fold_numbers(op: CalcOp, a: &Number, b: &Number, pos: Pos) -> Result<Option<Number>, Error> {
match op {
CalcOp::Add | CalcOp::Sub => {
let apply = |x: f64, y: f64| if op == CalcOp::Add { x + y } else { x - y };
if a.has_complex_units() || b.has_complex_units() {
if let Some(factor) = crate::value::unit_lists_factor(
(b.numer_units(), b.denom_units()),
(a.numer_units(), a.denom_units()),
) {
return Ok(Some(a.copy_units(apply(a.value, b.value * factor))));
}
let complex = if a.has_complex_units() { a } else { b };
return Err(Error::at(
format!(
"Number {} isn't compatible with CSS calculations.",
complex.to_css(false)
),
pos,
));
}
if a.unit() == b.unit() {
return Ok(Some(a.copy_units(apply(a.value, b.value))));
}
if a.is_unitless() || b.is_unitless() {
return Err(calc_incompatible(a, b, pos));
}
if let Some(factor) = crate::value::convert_factor(b.unit(), a.unit()) {
Ok(Some(a.copy_units(apply(a.value, b.value * factor))))
} else if crate::value::calc_units_incompatible(a.unit(), b.unit()) {
Err(calc_incompatible(a, b, pos))
} else {
Ok(None)
}
}
CalcOp::Mul => Ok(Some(a.mul(b))),
CalcOp::Div => Ok(Some(a.div(b))),
}
}
fn calc_incompatible(a: &Number, b: &Number, pos: Pos) -> Error {
Error::at(
format!("{} and {} are incompatible.", a.to_css(false), b.to_css(false)),
pos,
)
}
pub(crate) fn eval_div(l: Value, r: Value, slash: bool, pos: Pos) -> Result<Value, Error> {
if let Some(e) = callable_value_error(&l, &r, pos) {
return Err(e);
}
if !slash {
if let (Value::Number(a), Value::Number(b)) = (&l, &r) {
return divide_numbers(a, b, pos);
}
}
if slash {
if let (Value::Number(a), Value::Number(b)) = (l.clone().without_slash(), r.clone().without_slash()) {
let repr = format!("{}/{}", slash_repr(&l), slash_repr(&r));
return Ok(Value::Slash(a.div(&b), repr));
}
}
match (l.clone().without_slash(), r.clone().without_slash()) {
(Value::Number(a), Value::Number(b)) => divide_numbers(&a, &b, pos),
(lv @ Value::Color(_), rv @ (Value::Number(_) | Value::Color(_))) => {
Err(undefined_op(&lv, "/", &rv, pos))
}
_ => Ok(Value::Str(SassStr {
text: format!("{}/{}", slash_repr(&l), slash_repr(&r)),
quoted: false,
})),
}
}
fn divide_numbers(a: &Number, b: &Number, _pos: Pos) -> Result<Value, Error> {
Ok(Value::Number(a.div(b)))
}
fn slash_repr(v: &Value) -> String {
match v {
Value::Slash(_, repr) => repr.clone(),
other => other.to_css(false),
}
}
fn eval_binary(op: BinOp, l: Value, r: Value, pos: Pos) -> Result<Value, Error> {
match op {
BinOp::Add => binary_add(l, r, pos),
BinOp::Sub => binary_sub(l, r, pos),
BinOp::Mod => num_binop(l, r, pos, "%", sass_modulo),
BinOp::Mul => binary_mul(l, r, pos),
BinOp::Eq => Ok(Value::Bool(l.sass_eq(&r))),
BinOp::Neq => Ok(Value::Bool(!l.sass_eq(&r))),
BinOp::Lt => num_compare(l, r, pos, "<", |a, b| a < b),
BinOp::Gt => num_compare(l, r, pos, ">", |a, b| a > b),
BinOp::Le => num_compare(l, r, pos, "<=", |a, b| a <= b),
BinOp::Ge => num_compare(l, r, pos, ">=", |a, b| a >= b),
BinOp::And | BinOp::Or => Err(Error::unpositioned(
"internal: and/or are short-circuited in eval_expr",
)),
BinOp::SingleEq => Ok(Value::Str(SassStr {
text: format!("{}={}", l.to_css(false), r.to_css(false)),
quoted: false,
})),
}
}
fn num_compare(
l: Value,
r: Value,
pos: Pos,
sym: &str,
f: impl Fn(f64, f64) -> bool,
) -> Result<Value, Error> {
match (l, r) {
(Value::Number(a), Value::Number(b)) => {
let (av, bv, _) = coerce_pair(&a, &b, pos)?;
Ok(Value::Bool(f(av, bv)))
}
(l, r) => Err(undefined_op(&l, sym, &r, pos)),
}
}
fn binary_add(l: Value, r: Value, pos: Pos) -> Result<Value, Error> {
if let (Value::Number(a), Value::Number(b)) = (&l, &r) {
let (av, bv, proto) = coerce_pair(a, b, pos)?;
return Ok(Value::Number(proto.copy_units(av + bv)));
}
if color_arith_undefined(&l, &r) {
return Err(undefined_op(&l, "+", &r, pos));
}
if let Some(e) = callable_value_error(&l, &r, pos) {
return Err(e);
}
let calc_with_nonstring = (matches!(&l, Value::Calc(_)) && !matches!(&r, Value::Str(_)))
|| (matches!(&r, Value::Calc(_)) && !matches!(&l, Value::Str(_)));
if calc_with_nonstring {
return Err(undefined_op(&l, "+", &r, pos));
}
if let Some(m) = find_map(&l).or_else(|| find_map(&r)) {
return Err(Error::at(
format!("{} isn't a valid CSS value.", m.to_css(false)),
pos,
));
}
let quoted = match &l {
Value::Str(s) => s.quoted,
_ => matches!(&r, Value::Str(s) if s.quoted),
};
let text = format!("{}{}", concat_str(&l), concat_str(&r));
Ok(Value::Str(SassStr { text, quoted }))
}
fn binary_sub(l: Value, r: Value, pos: Pos) -> Result<Value, Error> {
if let (Value::Number(a), Value::Number(b)) = (&l, &r) {
let (av, bv, proto) = coerce_pair(a, b, pos)?;
return Ok(Value::Number(proto.copy_units(av - bv)));
}
if color_arith_undefined(&l, &r) {
return Err(undefined_op(&l, "-", &r, pos));
}
if let Some(e) = callable_value_error(&l, &r, pos) {
return Err(e);
}
if matches!(&l, Value::Calc(_)) || matches!(&r, Value::Calc(_)) {
return Err(undefined_op(&l, "-", &r, pos));
}
if let Some(m) = find_map(&l).or_else(|| find_map(&r)) {
return Err(Error::at(
format!("{} isn't a valid CSS value.", m.to_css(false)),
pos,
));
}
let text = format!("{}-{}", l.to_css(false), r.to_css(false));
Ok(Value::Str(SassStr { text, quoted: false }))
}
fn binary_mul(l: Value, r: Value, pos: Pos) -> Result<Value, Error> {
match (l, r) {
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a.mul(&b))),
(l, r) => Err(undefined_op(&l, "*", &r, pos)),
}
}
fn sass_modulo(a: f64, b: f64) -> f64 {
if a.is_infinite() {
return f64::NAN;
}
if b.is_infinite() {
return if a.is_sign_negative() == b.is_sign_negative() {
a
} else {
f64::NAN
};
}
if b == 0.0 {
return f64::NAN;
}
a - b * (a / b).floor()
}
fn num_binop(l: Value, r: Value, pos: Pos, sym: &str, f: impl Fn(f64, f64) -> f64) -> Result<Value, Error> {
match (l, r) {
(Value::Number(a), Value::Number(b)) => {
let (av, bv, proto) = coerce_pair(&a, &b, pos)?;
Ok(Value::Number(proto.copy_units(f(av, bv))))
}
(l, r) => Err(undefined_op(&l, sym, &r, pos)),
}
}
fn coerce_pair(a: &Number, b: &Number, pos: Pos) -> Result<(f64, f64, Number), Error> {
let incompatible = || {
Err(Error::at(
format!(
"{} and {} have incompatible units.",
a.to_css(false),
b.to_css(false)
),
pos,
))
};
if a.has_complex_units() || b.has_complex_units() {
if b.is_unitless() {
return Ok((a.value, b.value, a.clone()));
}
if a.is_unitless() {
return Ok((a.value, b.value, b.clone()));
}
return match crate::value::unit_lists_factor(
(b.numer_units(), b.denom_units()),
(a.numer_units(), a.denom_units()),
) {
Some(factor) => Ok((a.value, b.value * factor, a.clone())),
None => incompatible(),
};
}
if a.unit() == b.unit() || b.is_unitless() {
return Ok((a.value, b.value, a.clone()));
}
if a.is_unitless() {
return Ok((a.value, b.value, b.clone()));
}
match crate::value::convert_factor(b.unit(), a.unit()) {
Some(factor) => Ok((a.value, b.value * factor, a.clone())),
None => incompatible(),
}
}
fn concat_str(v: &Value) -> String {
match v {
Value::Str(s) => s.text.clone(),
other => other.to_css(false),
}
}
#[derive(Clone, Copy, PartialEq)]
enum DeclScope {
Allowed,
Control,
Function,
Mixin,
}
pub(crate) fn validate_declarations(sheet: &Stylesheet) -> Result<(), Error> {
validate_decl_scope(&sheet.stmts, DeclScope::Allowed)
}
fn validate_decl_scope(stmts: &[Stmt], scope: DeclScope) -> Result<(), Error> {
for stmt in stmts {
match stmt {
Stmt::FunctionDef(c) => {
if let Some(msg) = decl_error(scope, "function") {
return Err(Error::unpositioned(msg));
}
validate_decl_scope(&c.body, DeclScope::Function)?;
}
Stmt::MixinDef(c) => {
if let Some(msg) = decl_error(scope, "mixin") {
return Err(Error::unpositioned(msg));
}
validate_decl_scope(&c.body, DeclScope::Mixin)?;
}
Stmt::If(branches) => {
let inner = enter_control(scope);
for b in branches {
validate_decl_scope(&b.body, inner)?;
}
}
Stmt::For { body, .. } | Stmt::Each { body, .. } | Stmt::While { body, .. } => {
validate_decl_scope(body, enter_control(scope))?;
}
Stmt::Rule(r) => validate_decl_scope(&r.body, scope)?,
Stmt::AtRule { body: Some(body), .. }
| Stmt::Media { body, .. }
| Stmt::Supports { body, .. }
| Stmt::AtRoot { body, .. }
| Stmt::Keyframes { body, .. } => validate_decl_scope(body, scope)?,
Stmt::Include {
content: Some(content),
..
} => validate_decl_scope(content, scope)?,
Stmt::Import(args)
if scope != DeclScope::Allowed
&& args.iter().any(|a| matches!(a, ImportArg::Sass { .. })) =>
{
return Err(Error::unpositioned(
"This at-rule is not allowed here.".to_string(),
));
}
_ => {}
}
}
Ok(())
}
fn enter_control(scope: DeclScope) -> DeclScope {
match scope {
DeclScope::Function | DeclScope::Mixin => scope,
_ => DeclScope::Control,
}
}
fn decl_error(scope: DeclScope, kind: &str) -> Option<String> {
match scope {
DeclScope::Allowed => None,
DeclScope::Control => Some(format!(
"{} may not be declared in control directives.",
if kind == "function" { "Functions" } else { "Mixins" }
)),
DeclScope::Function => Some("This at-rule is not allowed here.".to_string()),
DeclScope::Mixin => Some(format!("Mixins may not contain {kind} declarations.")),
}
}
fn color_arith_undefined(l: &Value, r: &Value) -> bool {
let numeric = |v: &Value| matches!(v, Value::Color(_) | Value::Number(_));
(matches!(l, Value::Color(_)) && numeric(r)) || (matches!(r, Value::Color(_)) && numeric(l))
}
fn callable_value_error(l: &Value, r: &Value, pos: Pos) -> Option<Error> {
for v in [l, r] {
let inspect = match v {
Value::Function(f) => Some(f.inspect()),
Value::Mixin(m) => Some(m.inspect()),
_ => None,
};
if let Some(s) = inspect {
return Some(Error::at(format!("{s} isn't a valid CSS value."), pos));
}
}
None
}
fn undefined_op(l: &Value, sym: &str, r: &Value, pos: Pos) -> Error {
Error::at(
format!(
"Undefined operation \"{} {} {}\".",
l.to_css(false),
sym,
r.to_css(false)
),
pos,
)
}
fn is_css_import(arg: &str) -> bool {
arg.ends_with(".css")
|| arg.starts_with("http://")
|| arg.starts_with("https://")
|| arg.starts_with("//")
}
fn hoist_css_imports(out: &mut Vec<OutNode>) {
fn is_import(n: &OutNode) -> bool {
matches!(n, OutNode::Raw(s) if s.starts_with("@import"))
}
fn scan(nodes: &[OutNode], seen_css: &mut bool) -> bool {
for n in nodes {
match n {
OutNode::ModuleScope { nodes, .. } => {
if scan(nodes, seen_css) {
return true;
}
}
n if is_import(n) => {
if *seen_css {
return true;
}
}
OutNode::Blank | OutNode::Comment(..) => {}
_ => *seen_css = true,
}
}
false
}
let mut seen_css = false;
if !scan(out, &mut seen_css) {
return;
}
fn is_clone_scope(key: &str) -> bool {
key.contains("#copy") || key.contains("#import")
}
fn normalize_out_of_order(nodes: Vec<OutNode>) -> Vec<OutNode> {
let mut queue: std::collections::VecDeque<OutNode> = nodes.into();
let mut result: Vec<OutNode> = Vec::new();
let mut out_of_order: Vec<OutNode> = Vec::new();
let mut frozen = false;
let mut insert_at = 0usize;
while let Some(n) = queue.pop_front() {
match n {
OutNode::ModuleScope { key, nodes: inner } if is_clone_scope(&key) => {
for x in inner.into_iter().rev() {
queue.push_front(x);
}
}
OutNode::ModuleScope { key, nodes: inner } => {
result.push(OutNode::ModuleScope {
key,
nodes: normalize_out_of_order(inner),
});
if !frozen {
insert_at = result.len();
}
}
n if is_import(&n) => {
if frozen {
out_of_order.push(n);
} else {
result.push(n);
insert_at = result.len();
}
}
OutNode::Comment(..) | OutNode::Blank => {
result.push(n);
if !frozen {
insert_at = result.len();
}
}
other => {
frozen = true;
result.push(other);
}
}
}
if !out_of_order.is_empty() {
result.splice(insert_at..insert_at, out_of_order);
}
result
}
fn visit(nodes: Vec<OutNode>, imports: &mut Vec<OutNode>, css_seen: &mut bool) -> Vec<OutNode> {
let mut rest: Vec<OutNode> = Vec::new();
let mut iter = nodes.into_iter().peekable();
let mut pending: Vec<OutNode> = Vec::new();
loop {
match iter.peek() {
Some(OutNode::Comment(..)) | Some(OutNode::Blank) => {
pending.push(iter.next().unwrap());
}
Some(OutNode::ModuleScope { .. }) => {
let Some(OutNode::ModuleScope { key, nodes: inner }) = iter.next() else {
unreachable!()
};
if !*css_seen {
imports.extend(pending.drain(..).filter(|n| !matches!(n, OutNode::Blank)));
} else {
rest.append(&mut pending);
}
let inner_rest = visit(inner, imports, css_seen);
if !inner_rest.is_empty() {
*css_seen = true;
let mut regrouped: Vec<OutNode> = Vec::new();
for n in inner_rest {
match n {
OutNode::Blank => {}
other => push_group(&mut regrouped, vec![other]),
}
}
rest.push(OutNode::ModuleScope {
key,
nodes: regrouped,
});
}
}
_ => break,
}
}
let mut own: Vec<OutNode> = pending;
own.extend(iter);
let mut last_import: Option<usize> = None;
for (i, n) in own.iter().enumerate() {
if is_import(n) {
last_import = Some(i);
} else if !matches!(n, OutNode::Comment(..) | OutNode::Blank) {
break;
}
}
if let Some(li) = last_import {
let tail = own.split_off(li + 1);
imports.extend(own.into_iter().filter(|n| !matches!(n, OutNode::Blank)));
own = tail;
}
if own.iter().any(|n| !matches!(n, OutNode::Blank)) {
*css_seen = true;
}
rest.extend(own);
rest
}
let original = normalize_out_of_order(std::mem::take(out));
let mut imports = Vec::new();
let mut css_seen = false;
let rest = visit(original, &mut imports, &mut css_seen);
out.extend(imports);
for node in rest {
match node {
OutNode::Blank => {}
other => push_group(out, vec![other]),
}
}
}
fn splice_nodes(sink: &mut Sink<'_>, nodes: Vec<OutNode>) {
match sink {
Sink::Top(out) => push_group(out, nodes),
_ => {
for n in nodes {
if !matches!(n, OutNode::Blank) {
sink.push_at_rule(n);
}
}
}
}
}
fn trim_leading_blanks(nodes: &mut Vec<OutNode>) {
while matches!(nodes.first(), Some(OutNode::Blank)) {
nodes.remove(0);
}
}
const STYLE_GROUP_END: &str = "\u{0}STYLE_GROUP_END\u{0}";
fn push_group(out: &mut Vec<OutNode>, mut group: Vec<OutNode>) {
if group.is_empty() {
return;
}
if group.len() == 1
&& matches!(&group[0], OutNode::Raw(s) if s == AT_ROOT_PACK_TIGHT || s == STYLE_GROUP_END)
{
out.append(&mut group);
return;
}
fn last_effective(n: &OutNode) -> &OutNode {
if let OutNode::ModuleScope { nodes, .. } = n {
if let Some(l) = nodes.iter().rev().find(|x| !matches!(x, OutNode::Blank)) {
return last_effective(l);
}
}
n
}
let top_marker = matches!(out.last(), Some(OutNode::Raw(s)) if s == STYLE_GROUP_END);
if top_marker {
out.pop();
}
let last_eff = out.last().map(last_effective);
let prev_group_end = top_marker || matches!(last_eff, Some(OutNode::Raw(s)) if s == STYLE_GROUP_END);
let prev_packs_tight = match last_eff {
Some(OutNode::AtRule { .. } | OutNode::Comment(..)) => true,
Some(OutNode::Raw(s)) => s != STYLE_GROUP_END,
_ => false,
};
if (top_marker || !out.is_empty()) && (prev_group_end || !prev_packs_tight) {
out.push(OutNode::Blank);
}
out.append(&mut group);
}
fn normalize_arg_name(name: &str) -> String {
if name.contains('\\') {
return decode_ident_escapes(name).replace('_', "-");
}
name.replace('_', "-")
}
fn decode_ident_escapes(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut it = s.chars().peekable();
while let Some(c) = it.next() {
if c != '\\' {
out.push(c);
continue;
}
let mut hex = String::new();
while hex.len() < 6 && it.peek().is_some_and(|h| h.is_ascii_hexdigit()) {
hex.push(it.next().unwrap());
}
if hex.is_empty() {
match it.next() {
Some(l) => out.push(l),
None => out.push('\\'),
}
} else {
let cp = u32::from_str_radix(&hex, 16).unwrap_or(0xFFFD);
out.push(match char::from_u32(cp) {
Some('\0') | None => '\u{FFFD}',
Some(ch) => ch,
});
if matches!(it.peek(), Some(' ' | '\t' | '\n')) {
it.next();
}
}
}
out
}
fn is_calc_function(name: &str) -> bool {
matches!(name, "clamp" | "hypot" | "atan2" | "log" | "pow")
}
fn is_pure_calc_math_function(name: &str) -> bool {
let lower = name.to_ascii_lowercase();
matches!(
lower.as_str(),
"sin"
| "cos"
| "tan"
| "asin"
| "acos"
| "atan"
| "atan2"
| "exp"
| "log"
| "pow"
| "hypot"
| "sqrt"
| "sign"
| "mod"
| "rem"
)
}
fn calc_node_has_opaque(node: &CalcNode) -> bool {
match node {
CalcNode::Number(_) => false,
CalcNode::Str(_) => true,
CalcNode::Op { left, right, .. } => calc_node_has_opaque(left) || calc_node_has_opaque(right),
CalcNode::Func { .. } => true,
}
}
fn unquoted_string_css(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut after_newline = false;
for ch in s.chars() {
match ch {
'\n' => {
out.push(' ');
after_newline = true;
}
' ' => {
if !after_newline {
out.push(' ');
}
}
other => {
after_newline = false;
out.push(other);
}
}
}
out
}
fn is_supports_calc_function(name: &str) -> bool {
let lower = name.to_ascii_lowercase();
matches!(
lower.as_str(),
"min"
| "max"
| "clamp"
| "round"
| "mod"
| "rem"
| "abs"
| "sign"
| "sin"
| "cos"
| "tan"
| "asin"
| "acos"
| "atan"
| "atan2"
| "exp"
| "sqrt"
| "pow"
| "log"
| "hypot"
)
}
fn find_map(v: &Value) -> Option<&Map> {
match v {
Value::Map(m) => Some(m),
Value::List(l) => l.items.iter().find_map(find_map),
_ => None,
}
}
fn for_indices(start: i64, end: i64, inclusive: bool) -> Vec<i64> {
let mut out = Vec::new();
if start <= end {
let last = if inclusive { end } else { end - 1 };
let mut i = start;
while i <= last {
out.push(i);
i += 1;
}
} else {
let last = if inclusive { end } else { end + 1 };
let mut i = start;
while i >= last {
out.push(i);
i -= 1;
}
}
out
}
fn mixin_frame_name(name: &str, _module: &Option<String>) -> String {
format!("{name}()")
}
fn body_uses_content(body: &[Stmt]) -> bool {
body.iter().any(stmt_uses_content)
}
fn stmt_uses_content(stmt: &Stmt) -> bool {
match stmt {
Stmt::Content(_) => true,
Stmt::Rule(r) => body_uses_content(&r.body),
Stmt::If(branches) => branches.iter().any(|b| body_uses_content(&b.body)),
Stmt::For { body, .. }
| Stmt::Each { body, .. }
| Stmt::While { body, .. }
| Stmt::Media { body, .. }
| Stmt::AtRoot { body, .. }
| Stmt::Keyframes { body, .. } => body_uses_content(body),
Stmt::AtRule { body: Some(body), .. } => body_uses_content(body),
Stmt::Include {
content: Some(content),
..
} => body_uses_content(content),
_ => false,
}
}
fn dirname_of(key: &str) -> Option<String> {
let p = std::path::Path::new(key);
p.parent()
.filter(|d| !d.as_os_str().is_empty())
.map(|d| d.to_string_lossy().into_owned())
}
const MEDIA_HOIST_MARKER: &str = "\u{0}MEDIA_HOIST\u{0}";
const AT_ROOT_HOIST_MARKER: &str = "\u{0}AT_ROOT_HOIST\u{0}";
const AT_ROOT_PACK_TIGHT: &str = "\u{0}ATR_TIGHT\u{0}";
struct AtRootBatch {
target: usize,
group_end: bool,
nodes: Vec<OutNode>,
}
fn at_root_marker_target(s: &str) -> Option<usize> {
s.strip_prefix(AT_ROOT_HOIST_MARKER).and_then(|t| t.parse().ok())
}
fn is_escaping_marker(n: &OutNode, depth: usize) -> bool {
match n {
OutNode::Raw(s) => s == MEDIA_HOIST_MARKER || at_root_marker_target(s).is_some_and(|t| t < depth),
_ => false,
}
}
#[derive(Clone)]
enum AtCtx {
Media { prelude: String },
Supports { prelude: String },
Keyframes { name: String, prelude: String },
}
impl AtCtx {
fn query_name(&self) -> &'static str {
match self {
AtCtx::Media { .. } => "media",
AtCtx::Supports { .. } => "supports",
AtCtx::Keyframes { .. } => "keyframes",
}
}
fn wrap(&self, body: Vec<OutNode>) -> OutNode {
match self {
AtCtx::Media { prelude } => OutNode::AtRule {
name: "media".to_string(),
prelude: prelude.clone(),
body,
has_block: true,
lines: SrcLines::default(),
},
AtCtx::Supports { prelude } => OutNode::AtRule {
name: "supports".to_string(),
prelude: prelude.clone(),
body,
has_block: true,
lines: SrcLines::default(),
},
AtCtx::Keyframes { name, prelude } => OutNode::AtRule {
name: name.clone(),
prelude: prelude.clone(),
body,
has_block: true,
lines: SrcLines::default(),
},
}
}
}
struct AtRootQuery {
include: bool,
names: Vec<String>,
all: bool,
rule: bool,
}
impl AtRootQuery {
fn parse(text: Option<&str>) -> AtRootQuery {
let Some(text) = text else {
return AtRootQuery {
include: false,
names: vec!["rule".to_string()],
all: false,
rule: true,
};
};
let inner = text.trim().trim_start_matches('(').trim_end_matches(')');
let (include, list) = match inner.split_once(':') {
Some((k, v)) if k.trim().eq_ignore_ascii_case("with") => (true, v),
Some((_, v)) => (false, v),
None => (false, inner),
};
let names: Vec<String> = list
.split_whitespace()
.map(|s| s.trim_matches('"').trim_matches('\'').to_ascii_lowercase())
.collect();
let all = names.iter().any(|n| n == "all");
let rule = names.iter().any(|n| n == "rule");
AtRootQuery {
include,
names,
all,
rule,
}
}
fn excludes_style_rules(&self) -> bool {
(self.all || self.rule) != self.include
}
fn excludes_name(&self, name: &str) -> bool {
if self.all {
return !self.include;
}
self.names.iter().any(|n| n == name) != self.include
}
}
fn normalize_keyframe_selector(s: &str) -> String {
if !s.contains('E') {
return s.to_string();
}
let t = s.trim();
let is_pct = t.ends_with('%')
&& t[..t.len() - 1]
.chars()
.all(|c| c.is_ascii_digit() || matches!(c, '.' | '+' | '-' | 'e' | 'E'));
if is_pct {
s.replace('E', "e")
} else {
s.to_string()
}
}
fn at_body_to_items(nodes: Vec<OutNode>) -> Vec<OutItem> {
let mut items = Vec::new();
for n in nodes {
match n {
OutNode::AtDecl {
prop,
value,
important,
custom,
lines,
} => items.push(OutItem::Decl {
prop,
value,
important,
custom,
lines,
}),
OutNode::Comment(t, lines) => items.push(OutItem::Comment(t, lines)),
OutNode::Rule {
selectors, items: ri, ..
} => items.push(OutItem::NestedRule { selectors, items: ri }),
OutNode::AtRule {
name,
prelude,
body,
has_block,
lines,
} => {
if has_block {
items.push(OutItem::NestedAtRule {
name,
prelude,
items: at_body_to_items(body),
});
} else {
items.push(OutItem::ChildlessAtRule { name, prelude, lines });
}
}
OutNode::ModuleScope { nodes, .. } => items.extend(at_body_to_items(nodes)),
OutNode::Raw(_) | OutNode::Blank => {}
}
}
items
}
fn validate_plain_css_selector(part: &str, top_level: bool) -> Result<(), Error> {
let trimmed = part.trim();
let chars: Vec<char> = trimmed.chars().collect();
if top_level && matches!(chars.first(), Some('>' | '+' | '~')) {
return Err(Error::unpositioned(
"Top-level leading combinators aren't allowed in plain CSS.",
));
}
if matches!(chars.last(), Some('>' | '+' | '~')) {
return Err(Error::unpositioned("expected selector."));
}
let mut i = 0;
let mut at_compound_start = true;
let mut depth = 0i32; while i < chars.len() {
let c = chars[i];
match c {
'\\' => {
i += 2;
at_compound_start = false;
continue;
}
'[' | '(' => depth += 1,
']' | ')' => depth -= 1,
_ if depth > 0 => {}
' ' | '\t' | '\n' | '\r' | '>' | '+' | '~' => at_compound_start = true,
'%' if at_compound_start => {
return Err(Error::unpositioned(
"Placeholder selectors aren't allowed in plain CSS.",
));
}
'&' => {
let next = chars.get(i + 1).copied();
if matches!(next, Some(n) if n.is_ascii_alphanumeric() || n == '-' || n == '_' || n == '\\') {
return Err(Error::unpositioned(
"Parent selectors can't have suffixes in plain CSS.",
));
}
at_compound_start = false;
}
_ => at_compound_start = false,
}
i += 1;
}
Ok(())
}
fn paren_follows_pseudo(chars: &[char], open: usize) -> bool {
let mut j = open;
while j > 0 {
let p = chars[j - 1];
if p.is_ascii_alphanumeric() || p == '-' || p == '_' || (p as u32) >= 0x80 {
j -= 1;
} else {
break;
}
}
j < open && j > 0 && chars[j - 1] == ':'
}
fn validate_selector(sel: &str, has_parent: bool) -> Result<(), Error> {
if sel.trim_start().starts_with(',') {
return Err(Error::unpositioned("expected selector."));
}
if sel
.bytes()
.any(|c| matches!(c, b'(' | b'[' | b')' | b']' | b'"' | b'\'' | b'\\'))
{
let mut stack: Vec<char> = Vec::new();
let mut quote: Option<char> = None;
let mut prev_escape = false;
for c in sel.chars() {
if prev_escape {
prev_escape = false;
continue;
}
match c {
'\\' => prev_escape = true,
q @ ('"' | '\'') => match quote {
Some(open) if open == q => quote = None,
Some(_) => {}
None => quote = Some(q),
},
_ if quote.is_some() => {}
'(' | '[' => stack.push(c),
')' | ']' => {
let open = stack.pop();
if c == ')' && open == Some('[') {
return Err(Error::unpositioned("expected \"]\"."));
}
if c == ']' && open == Some('(') {
return Err(Error::unpositioned("expected \")\"."));
}
}
_ => {}
}
}
}
if sel.as_bytes().windows(5).any(|w| {
w[0] == b':'
&& (w[1] | 0x20) == b'n'
&& (w[2] | 0x20) == b't'
&& (w[3] | 0x20) == b'h'
&& w[4] == b'-'
}) {
let lower = sel.to_ascii_lowercase();
for pat in [
":nth-child(",
":nth-last-child(",
":nth-of-type(",
":nth-last-of-type(",
] {
let mut from = 0;
while let Some(p) = lower[from..].find(pat) {
let start = from + p + pat.len();
if lower[start..].trim_start().starts_with(')') {
return Err(Error::unpositioned("Expected \"n\"."));
}
from = start;
}
}
}
let mut needs_slow = false;
let mut has_sigil = false;
for &b in sel.as_bytes() {
match b {
b'\\' | b'"' | b'\'' | b'[' | b'(' | b')' | b']' | b'&' | b'%' | b'/' => needs_slow = true,
b'#' | b'.' => has_sigil = true,
_ => {}
}
}
if needs_slow {
validate_selector_tail(sel, has_parent)
} else if has_sigil {
validate_plain_sigils(sel)
} else {
Ok(())
}
}
fn validate_plain_sigils(sel: &str) -> Result<(), Error> {
let b = sel.as_bytes();
for i in 0..b.len() {
let c = b[i];
if c != b'#' && c != b'.' {
continue;
}
if c == b'.' && i > 0 && b[i - 1].is_ascii_digit() {
continue;
}
let starts_ident = match b.get(i + 1) {
Some(&n) => n.is_ascii_alphabetic() || n == b'-' || n == b'_' || n >= 0x80,
None => false,
};
if !starts_ident {
return Err(Error::unpositioned("Expected identifier."));
}
}
Ok(())
}
fn validate_selector_tail(sel: &str, has_parent: bool) -> Result<(), Error> {
for part in split_commas(sel) {
let chars: Vec<char> = part.chars().collect();
let mut i = 0;
let mut at_compound_start = true;
let mut depth = 0i32; while i < chars.len() {
let c = chars[i];
match c {
'\\' => {
i += 2;
at_compound_start = false;
continue;
}
'"' | '\'' => {
i = skip_string(&chars, i);
at_compound_start = false;
continue;
}
'[' if depth == 0 => {
let end = matching_bracket(&chars, i);
validate_attribute(&chars[i + 1..end])?;
i = end + 1;
at_compound_start = false;
continue;
}
'(' if depth == 0 => {
if !paren_follows_pseudo(&chars, i) {
return Err(Error::unpositioned("expected selector."));
}
depth += 1;
at_compound_start = false;
}
')' if depth == 0 => {
return Err(Error::unpositioned("Unexpected \")\"."));
}
'[' | '(' => {
depth += 1;
at_compound_start = false;
}
']' | ')' => {
depth -= 1;
at_compound_start = false;
}
_ if depth > 0 => {}
' ' | '\t' | '\n' | '\r' => at_compound_start = true,
'>' | '+' | '~' => at_compound_start = true,
'&' => {
if !at_compound_start {
return Err(Error::unpositioned(
"\"&\" may only used at the beginning of a compound selector.",
));
}
let next = chars.get(i + 1).copied();
let is_suffix = matches!(next, Some(n) if n.is_ascii_alphanumeric() || n == '-' || n == '_' || n == '\\');
if is_suffix && !has_parent {
return Err(Error::unpositioned(
"A top-level selector may not contain a parent selector with a suffix.",
));
}
at_compound_start = false;
}
'%' => {
let prev_is_digit = i > 0 && chars[i - 1].is_ascii_digit();
if !prev_is_digit {
let next = chars.get(i + 1).copied();
let starts_ident = matches!(next, Some(n) if n.is_ascii_alphabetic() || n == '-' || n == '_' || n == '\\');
if !starts_ident {
return Err(Error::unpositioned("Expected identifier."));
}
}
at_compound_start = false;
}
'#' | '.' if !(c == '.' && i > 0 && chars[i - 1].is_ascii_digit()) => {
let next = chars.get(i + 1).copied();
let starts_ident = matches!(next, Some(n) if n.is_ascii_alphabetic() || n == '-' || n == '_' || n == '\\' || (n as u32) >= 0x80);
if !starts_ident {
return Err(Error::unpositioned("Expected identifier."));
}
at_compound_start = false;
}
'/' => {
return Err(Error::unpositioned("expected selector."));
}
_ => at_compound_start = false,
}
i += 1;
}
}
Ok(())
}
fn skip_string(chars: &[char], start: usize) -> usize {
let quote = chars[start];
let mut i = start + 1;
while i < chars.len() {
match chars[i] {
'\\' => i += 2,
c if c == quote => return i + 1,
_ => i += 1,
}
}
chars.len()
}
fn matching_bracket(chars: &[char], open: usize) -> usize {
let mut i = open + 1;
while i < chars.len() {
match chars[i] {
'\\' => i += 2,
'"' | '\'' => i = skip_string(chars, i),
']' => return i,
_ => i += 1,
}
}
chars.len()
}
fn validate_attribute(inner: &[char]) -> Result<(), Error> {
let err = || Error::unpositioned("expected \"]\".");
let mut i = 0;
let skip_ws = |i: &mut usize| {
while *i < inner.len() && inner[*i].is_whitespace() {
*i += 1;
}
};
skip_ws(&mut i);
while i < inner.len() {
let c = inner[i];
if c == '\\' {
i += 2;
} else if is_name_char(c) || c == '|' || c == '*' {
i += 1;
} else {
break;
}
}
skip_ws(&mut i);
if i >= inner.len() {
return Ok(()); }
let op_ok = match inner[i] {
'=' => true,
'~' | '|' | '^' | '$' | '*' => inner.get(i + 1) == Some(&'='),
_ => false,
};
if !op_ok {
return Err(err());
}
i += if inner[i] == '=' { 1 } else { 2 };
skip_ws(&mut i);
match inner.get(i) {
Some('"') | Some('\'') => i = skip_string(inner, i),
Some(_) => {
while i < inner.len() {
let c = inner[i];
if c == '\\' {
i += 2;
} else if c.is_whitespace() {
break;
} else {
i += 1;
}
}
}
None => return Err(err()),
}
skip_ws(&mut i);
if i >= inner.len() {
return Ok(()); }
if inner[i].is_ascii_alphabetic() && i + 1 == inner.len() {
return Ok(());
}
Err(err())
}
fn is_name_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '-' || c == '_' || !c.is_ascii()
}
fn is_name_start(c: char) -> bool {
c.is_ascii_alphabetic() || c == '_' || !c.is_ascii()
}
fn is_plain_css_identifier(s: &str) -> bool {
let chars: Vec<char> = s.chars().collect();
if chars.is_empty() {
return false;
}
let mut i = 0;
if chars[0] == '-' {
i = 1;
}
match chars.get(i) {
Some(&c) if is_name_start(c) => i += 1,
_ => return false,
}
chars[i..].iter().all(|&c| is_name_char(c))
}
fn normalize_attribute_text(inner: &str) -> String {
let chars: Vec<char> = inner.chars().collect();
let fallback = || inner.trim().to_string();
let mut i = 0;
let skip_ws = |i: &mut usize| {
while *i < chars.len() && chars[*i].is_whitespace() {
*i += 1;
}
};
skip_ws(&mut i);
let name_start = i;
while i < chars.len() {
let c = chars[i];
if c == '\\' {
i += 2;
} else if is_name_char(c) || c == '|' || c == '*' {
i += 1;
} else {
break;
}
}
if i == name_start {
return fallback();
}
let name: String = chars[name_start..i.min(chars.len())].iter().collect();
skip_ws(&mut i);
if i >= chars.len() {
return name; }
let op: String = match chars[i] {
'=' => {
i += 1;
"=".to_string()
}
c @ ('~' | '|' | '^' | '$' | '*') if chars.get(i + 1) == Some(&'=') => {
i += 2;
format!("{c}=")
}
_ => return fallback(),
};
skip_ws(&mut i);
let value_start = i;
match chars.get(i) {
Some('"') | Some('\'') => i = skip_string(&chars, i),
Some(_) => {
while i < chars.len() {
let c = chars[i];
if c == '\\' {
i += 2;
} else if c.is_whitespace() {
break;
} else {
i += 1;
}
}
}
None => return fallback(),
}
let raw_value: String = chars[value_start..i.min(chars.len())].iter().collect();
let value = unquote_plain_attribute_value(&raw_value);
skip_ws(&mut i);
if i >= chars.len() {
return format!("{name}{op}{value}");
}
if chars[i].is_ascii_alphabetic() && i + 1 == chars.len() {
return format!("{name}{op}{value} {}", chars[i]);
}
fallback()
}
fn unquote_plain_attribute_value(raw: &str) -> String {
let bytes: Vec<char> = raw.chars().collect();
if bytes.len() >= 2 {
let q = bytes[0];
if (q == '"' || q == '\'') && bytes[bytes.len() - 1] == q {
let content: String = bytes[1..bytes.len() - 1].iter().collect();
if is_plain_css_identifier(&content) {
return content;
}
}
}
raw.to_string()
}
fn root_rule_contains_target(nodes: &[OutNode], target: &crate::selector::Simple) -> bool {
nodes.iter().any(|node| match node {
OutNode::Rule { selectors, .. } => selectors.iter().any(|s| {
crate::selector::parse_list(s)
.map(|cs| crate::selector::list_contains_simple(&cs, target))
.unwrap_or(false)
}),
_ => false,
})
}
fn rewrite_nodes_scoped(
nodes: &mut Vec<OutNode>,
scope: &str,
all: &[crate::selector::Extension],
origins: &[String],
closures: &HashMap<String, std::collections::HashSet<String>>,
) {
let visible: Vec<crate::selector::Extension> = all
.iter()
.zip(origins.iter())
.filter(|(e, o)| {
let reachable =
o.as_str() == scope || closures.get(o.as_str()).is_some_and(|c| c.contains(scope));
let private_ok = match &e.target {
Some(crate::selector::Simple::Placeholder(n)) if n.starts_with('-') || n.starts_with('_') => {
o.as_str() == scope
}
_ => true,
};
reachable && private_ok
})
.map(|(e, _)| e.clone())
.collect();
rewrite_with_scopes(nodes, &visible, scope, all, origins, closures);
}
fn rewrite_with_scopes(
nodes: &mut Vec<OutNode>,
visible: &[crate::selector::Extension],
scope: &str,
all: &[crate::selector::Extension],
origins: &[String],
closures: &HashMap<String, std::collections::HashSet<String>>,
) {
for node in nodes.iter_mut() {
match node {
OutNode::ModuleScope { key, nodes } => {
let key = key.clone();
rewrite_nodes_scoped(nodes, &key, all, origins, closures);
}
OutNode::AtRule { name, body, .. } if !is_keyframes_name(name) => {
rewrite_with_scopes(body, visible, scope, all, origins, closures);
}
_ => {}
}
}
rewrite_nodes(nodes, visible, scope);
}
fn rewrite_nodes(nodes: &mut Vec<OutNode>, extensions: &[crate::selector::Extension], scope: &str) {
let mut i = 0;
while i < nodes.len() {
let drop = match &mut nodes[i] {
OutNode::ModuleScope { nodes, .. } => nodes.iter().all(|n| matches!(n, OutNode::Blank)),
OutNode::Rule {
selectors,
linebreaks,
extend_base,
..
} => {
let new_sel = extend_selector_list(selectors, linebreaks, extensions, scope, *extend_base);
match new_sel {
Some((s, b)) => {
*linebreaks = b;
*selectors = s;
false
}
None => true,
}
}
OutNode::AtRule {
name,
body,
has_block,
..
} => {
*has_block && body.is_empty() && (name == "media" || name == "supports")
}
_ => false,
};
if drop {
nodes.remove(i);
if i < nodes.len() && matches!(nodes[i], OutNode::Blank) {
nodes.remove(i);
} else if i > 0 && matches!(nodes[i - 1], OutNode::Blank) {
nodes.remove(i - 1);
continue;
}
} else {
i += 1;
}
}
}
fn is_keyframes_name(name: &str) -> bool {
let lower = name.to_ascii_lowercase();
lower == "keyframes" || lower.ends_with("-keyframes")
}
fn default_namespace(url: &str, pos: Pos) -> Result<String, Error> {
let last = url.rsplit('/').next().unwrap_or(url);
let last = last.strip_prefix('_').unwrap_or(last);
let stem = match last.split_once('.') {
Some((before, _)) => before,
None => last,
};
if !is_valid_namespace(stem) {
return Err(Error::at(
format!("The default namespace \"{stem}\" is not a valid Sass identifier."),
pos,
));
}
Ok(stem.to_string())
}
fn forward_var_visibility(
show: &Option<Vec<crate::ast::ForwardMember>>,
hide: &Option<Vec<crate::ast::ForwardMember>>,
) -> impl Fn(&str) -> bool {
let show = member_set(show, true);
let hide = member_set(hide, true);
move |name: &str| -> bool {
let n = normalize_var_name(name);
if let Some(s) = &show {
return s.contains(&n);
}
if let Some(h) = &hide {
return !h.contains(&n);
}
true
}
}
fn normalize_var_name(name: &str) -> String {
name.replace('_', "-")
}
fn is_private_member(name: &str) -> bool {
name.starts_with('-') || name.starts_with('_')
}
fn is_builtin_mixin(module: &str, name: &str) -> bool {
if module != "meta" {
return false;
}
matches!(normalize_arg_name(name).as_str(), "load-css" | "apply")
}
fn member_set(
members: &Option<Vec<crate::ast::ForwardMember>>,
vars: bool,
) -> Option<std::collections::HashSet<String>> {
members.as_ref().map(|list| {
list.iter()
.filter_map(|m| match (m, vars) {
(crate::ast::ForwardMember::Var(n), true) => Some(normalize_var_name(n)),
(crate::ast::ForwardMember::Name(n), false) => Some(normalize_var_name(n)),
_ => None,
})
.collect()
})
}
fn is_valid_namespace(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c) if c == '_' || c == '-' || c.is_ascii_alphabetic() || !c.is_ascii() => {}
_ => return false,
}
chars.all(|c| c == '_' || c == '-' || c.is_ascii_alphanumeric() || !c.is_ascii())
}
fn extend_selector_list(
selectors: &[String],
breaks: &[bool],
extensions: &[crate::selector::Extension],
scope: &str,
extend_base: usize,
) -> Option<(Vec<String>, Vec<bool>)> {
let has_placeholder = selectors.iter().any(|s| s.contains('%'));
if extensions.is_empty() && !has_placeholder {
return Some((selectors.to_vec(), breaks.to_vec()));
}
let joined = selectors.join(", ");
let Some(parsed) = crate::selector::parse_list(&joined) else {
return Some((selectors.to_vec(), breaks.to_vec()));
};
let result = crate::selector::extend_selectors(&parsed, breaks, extensions, scope, extend_base);
if result.all_placeholders {
return None;
}
Some((result.selectors, result.breaks))
}
fn comma_linebreaks(sel: &str, nested: bool) -> Vec<bool> {
let mut out = Vec::new();
let mut pending_nl = false;
let segs = split_commas(sel);
for (i, seg) in segs.iter().enumerate() {
if seg.trim().is_empty() {
pending_nl = pending_nl || (i > 0 && seg.contains('\n'));
continue;
}
let leading_nl = seg.chars().take_while(|c| c.is_whitespace()).any(|c| c == '\n');
let prev_trailing_nl = i > 0
&& segs[i - 1]
.chars()
.rev()
.take_while(|c| c.is_whitespace())
.any(|c| c == '\n');
let newline_before = i > 0 && (leading_nl || prev_trailing_nl);
out.push((newline_before || pending_nl) && !(nested && part_has_parent_ref(seg)));
pending_nl = false;
}
out
}
fn part_has_parent_ref(part: &str) -> bool {
let mut bracket = 0i32;
let mut quote: Option<char> = None;
let mut chars = part.chars();
while let Some(c) = chars.next() {
match quote {
Some(q) => {
if c == '\\' {
chars.next();
} else if c == q {
quote = None;
}
}
None => match c {
'"' | '\'' => quote = Some(c),
'[' => bracket += 1,
']' => bracket = (bracket - 1).max(0),
'&' if bracket == 0 => return true,
_ => {}
},
}
}
false
}
fn find_unquoted_at(sel: &str) -> Option<usize> {
if !sel.as_bytes().contains(&b'@') {
return None;
}
let mut quote: Option<char> = None;
let mut iter = sel.chars().enumerate();
while let Some((i, c)) = iter.next() {
match quote {
Some(q) => {
if c == '\\' {
iter.next();
} else if c == q {
quote = None;
}
}
None => match c {
'"' | '\'' => quote = Some(c),
'\\' => {
iter.next();
}
'@' => return Some(i),
_ => {}
},
}
}
None
}
fn replace_parent_refs(part: &str, parent: &str) -> String {
let mut out = String::with_capacity(part.len() + parent.len());
let mut bracket = 0i32;
let mut quote: Option<char> = None;
let mut chars = part.chars();
while let Some(c) = chars.next() {
match quote {
Some(q) => {
out.push(c);
if c == '\\' {
if let Some(n) = chars.next() {
out.push(n);
}
} else if c == q {
quote = None;
}
}
None => match c {
'"' | '\'' => {
quote = Some(c);
out.push(c);
}
'[' => {
bracket += 1;
out.push(c);
}
']' => {
bracket = (bracket - 1).max(0);
out.push(c);
}
'&' if bracket == 0 => out.push_str(parent),
_ => out.push(c),
},
}
}
out
}
fn resolve_selectors_opt(sel: &str, parents: &[String], implicit_parent: bool) -> Result<Vec<String>, Error> {
let parts: Vec<String> = split_commas(sel)
.into_iter()
.map(|p| trim_selector_part(p).to_string())
.filter(|p| !p.is_empty())
.collect();
let check_compound_parent = |part: &str, parent: &str| -> Result<(), Error> {
let trimmed = parent.trim_end();
if !matches!(trimmed.chars().last(), Some('>' | '+' | '~')) {
return Ok(());
}
let chars: Vec<char> = part.chars().collect();
for (i, &c) in chars.iter().enumerate() {
if c == '&' {
if let Some(&next) = chars.get(i + 1) {
if next.is_alphanumeric()
|| matches!(next, '.' | '#' | ':' | '[' | '%' | '\\' | '-' | '_')
{
return Err(Error::unpositioned(format!(
"Selector \"{trimmed}\" can't be used as a parent in a compound selector."
)));
}
}
}
}
Ok(())
};
let substitute_pseudo_refs = |part: &str| -> Option<String> {
if !part.contains('&') {
return None;
}
let chars: Vec<char> = part.chars().collect();
let mut depth = 0i32;
let mut out = String::new();
let mut i = 0;
let mut replaced = false;
while i < chars.len() {
let c = chars[i];
match c {
'(' => depth += 1,
')' => depth -= 1,
'&' => {
if depth == 0 {
return None;
}
let mut suffix = String::new();
let mut j = i + 1;
while j < chars.len() && (chars[j].is_alphanumeric() || matches!(chars[j], '-' | '_')) {
suffix.push(chars[j]);
j += 1;
}
if matches!(chars.get(j), Some('.' | '#' | ':' | '[' | '%' | '\\' | '&')) {
return None;
}
let expansion = parents
.iter()
.map(|p| format!("{p}{suffix}"))
.collect::<Vec<_>>()
.join(", ");
out.push_str(&expansion);
replaced = true;
i = j;
continue;
}
_ => {}
}
out.push(c);
i += 1;
}
if replaced {
Some(out)
} else {
None
}
};
let split_parent_refs = |part: &str| -> Option<Vec<String>> {
let mut segments = vec![String::new()];
let mut depth = 0i32;
let mut quote: Option<char> = None;
for c in part.chars() {
if let Some(q) = quote {
segments.last_mut().unwrap().push(c);
if c == q {
quote = None;
}
continue;
}
match c {
'"' | '\'' => quote = Some(c),
'(' | '[' => depth += 1,
')' | ']' => depth -= 1,
'&' if depth == 0 => {
segments.push(String::new());
continue;
}
_ => {}
}
segments.last_mut().unwrap().push(c);
}
if segments.len() >= 3 {
Some(segments)
} else {
None
}
};
let expand_cartesian = |segments: &[String], result: &mut Vec<String>| {
let k = segments.len() - 1;
let n = parents.len();
let mut idx = vec![0usize; k];
loop {
let mut s = String::new();
for (i, seg) in segments[..k].iter().enumerate() {
s.push_str(seg);
s.push_str(&parents[idx[i]]);
}
s.push_str(&segments[k]);
result.push(normalize_selector(&s));
let mut j = k;
loop {
if j == 0 {
return;
}
j -= 1;
idx[j] += 1;
if idx[j] < n {
break;
}
idx[j] = 0;
}
}
};
let mut result = Vec::new();
if parents.is_empty() {
for part in &parts {
result.push(normalize_selector(part));
}
} else if !implicit_parent {
for part in &parts {
if let Some(s) = substitute_pseudo_refs(part) {
result.push(normalize_selector(&s));
} else if let Some(segments) = split_parent_refs(part) {
for parent in parents {
check_compound_parent(part, parent)?;
}
expand_cartesian(&segments, &mut result);
} else if part_has_parent_ref(part) {
for parent in parents {
check_compound_parent(part, parent)?;
result.push(normalize_selector(&replace_parent_refs(part, parent)));
}
} else {
result.push(normalize_selector(part));
}
}
} else {
for (pi, parent) in parents.iter().enumerate() {
for part in &parts {
if let Some(s) = substitute_pseudo_refs(part) {
if pi == 0 {
result.push(normalize_selector(&s));
}
continue;
}
if let Some(segments) = split_parent_refs(part) {
if pi == 0 {
for parent in parents {
check_compound_parent(part, parent)?;
}
expand_cartesian(&segments, &mut result);
}
continue;
}
let combined = if part_has_parent_ref(part) {
check_compound_parent(part, parent)?;
replace_parent_refs(part, parent)
} else {
format!("{parent} {part}")
};
result.push(normalize_selector(&combined));
}
}
}
Ok(result)
}
fn split_commas(s: &str) -> Vec<&str> {
if !s.as_bytes().contains(&b',') {
return vec![s];
}
let mut out = Vec::new();
let mut paren = 0i32;
let mut bracket = 0i32;
let mut start = 0usize;
for (idx, c) in s.char_indices() {
match c {
'(' => paren += 1,
')' => paren -= 1,
'[' => bracket += 1,
']' => bracket -= 1,
',' if paren == 0 && bracket == 0 => {
out.push(&s[start..idx]);
start = idx + 1; }
_ => {}
}
}
out.push(&s[start..]);
out
}
pub(crate) fn normalize_selector(s: &str) -> String {
if is_canonical_plain(s) {
return s.to_string();
}
normalize_selector_slow(s)
}
fn is_canonical_plain(s: &str) -> bool {
let b = s.as_bytes();
if b.is_empty() || b[0] == b' ' || b[b.len() - 1] == b' ' {
return false;
}
let mut prev_space = false;
for &c in b {
match c {
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'-' | b'.' | b'#' | b'%' => {
prev_space = false;
}
b' ' => {
if prev_space {
return false;
}
prev_space = true;
}
_ => return false,
}
}
true
}
fn normalize_selector_slow(s: &str) -> String {
let cs: Vec<char> = s.chars().collect();
let mut collapsed = String::with_capacity(s.len());
let mut prev_space = true; let mut ci = 0;
while ci < cs.len() {
let c = cs[ci];
if c == '\\' && ci + 1 < cs.len() && cs[ci + 1].is_ascii_hexdigit() {
collapsed.push('\\');
ci += 1;
let mut digits = 0;
while digits < 6 && ci < cs.len() && cs[ci].is_ascii_hexdigit() {
collapsed.push(cs[ci]);
ci += 1;
digits += 1;
}
if ci < cs.len() && cs[ci].is_whitespace() {
collapsed.push(' ');
ci += 1;
}
prev_space = false;
continue;
}
if c == '\\' && ci + 1 < cs.len() {
collapsed.push('\\');
collapsed.push(cs[ci + 1]);
ci += 2;
prev_space = false;
continue;
}
if c.is_whitespace() {
if !prev_space {
collapsed.push(' ');
}
prev_space = true;
ci += 1;
continue;
}
collapsed.push(c);
prev_space = false;
ci += 1;
}
if prev_space && collapsed.ends_with(' ') {
collapsed.pop();
}
let chars: Vec<char> = collapsed.chars().collect();
let mut out = String::new();
let mut i = 0;
let mut mid_compound = false;
while i < chars.len() {
let c = chars[i];
match c {
'[' => {
let end = matching_bracket(&chars, i);
if end < chars.len() {
let whole: String = chars[i..=end].iter().collect();
out.push_str(&crate::selector::normalize_attribute(&whole));
} else {
let inner: String = chars[i + 1..].iter().collect();
out.push('[');
out.push_str(&normalize_attribute_text(&inner));
}
i = end + 1;
mid_compound = true;
continue;
}
'.' | '#' | '%' => {
out.push(c);
i += 1;
copy_name(&chars, &mut i, &mut out);
mid_compound = true;
continue;
}
':' => {
let start = out.len();
copy_pseudo(&chars, &mut i, &mut out);
let text = out[start..].to_string();
let canon = crate::selector::normalize_nth(&text)
.or_else(|| crate::selector::normalize_pseudo_arg(&text));
if let Some(canon) = canon {
out.truncate(start);
out.push_str(&canon);
}
mid_compound = true;
continue;
}
'*' if chars.get(i + 1) != Some(&'|') || chars.get(i + 2) == Some(&'=') => {
out.push('*');
i += 1;
mid_compound = true;
continue;
}
'>' | '~' | '+' => {
while out.ends_with(' ') {
out.pop();
}
out.push(' ');
out.push(c);
out.push(' ');
i += 1;
while i < chars.len() && chars[i] == ' ' {
i += 1;
}
mid_compound = false;
continue;
}
' ' | '\t' | '\n' | '\r' => {
out.push(c);
i += 1;
mid_compound = false;
continue;
}
_ if type_selector_starts_at(&chars, i) => {
if mid_compound && !out.ends_with(' ') {
out.push(' ');
}
copy_type_selector(&chars, &mut i, &mut out);
mid_compound = true;
continue;
}
_ => {
out.push(c);
i += 1;
mid_compound = false;
}
}
}
let t = out.trim();
let start = out.len() - out.trim_start().len();
let end = start + t.len();
if out[end..].starts_with(' ') && (ends_with_hex_escape(t) || ends_with_escaping_backslash(t)) {
out[start..=end].to_string()
} else {
t.to_string()
}
}
fn ends_with_hex_escape(t: &str) -> bool {
let b = t.as_bytes();
let mut i = b.len();
let mut digits = 0;
while i > 0 && (b[i - 1] as char).is_ascii_hexdigit() && digits < 6 {
i -= 1;
digits += 1;
}
digits > 0 && i > 0 && b[i - 1] == b'\\'
}
fn ends_with_escaping_backslash(t: &str) -> bool {
let b = t.as_bytes();
let mut n = 0;
while n < b.len() && b[b.len() - 1 - n] == b'\\' {
n += 1;
}
n % 2 == 1
}
fn trim_selector_part(p: &str) -> &str {
let t0 = p.trim_start();
let t = t0.trim_end();
if t.len() < t0.len() && (ends_with_hex_escape(t) || ends_with_escaping_backslash(t)) {
&t0[..t.len() + 1]
} else {
t
}
}
enum SelToken<'a> {
Combinator,
Compound(&'a str),
}
fn tokenize_complex(s: &str) -> Vec<SelToken<'_>> {
let mut tokens = Vec::new();
let mut paren = 0i32;
let mut bracket = 0i32;
let mut start = 0usize; let mut it = s.char_indices();
while let Some((idx, c)) = it.next() {
match c {
'\\' => {
it.next();
}
'"' | '\'' => {
while let Some((_, c2)) = it.next() {
match c2 {
'\\' => {
it.next();
}
q if q == c => break,
_ => {}
}
}
}
'(' => paren += 1,
')' => paren -= 1,
'[' => bracket += 1,
']' => bracket -= 1,
'>' | '+' | '~' if paren == 0 && bracket == 0 => {
let t = trim_selector_part(&s[start..idx]);
if !t.is_empty() {
tokens.push(SelToken::Compound(t));
}
tokens.push(SelToken::Combinator);
start = idx + 1; }
_ => {}
}
}
let t = trim_selector_part(&s[start..]);
if !t.is_empty() {
tokens.push(SelToken::Compound(t));
}
tokens
}
fn complex_selector_is_bogus(s: &str, in_pseudo: bool, allow_leading: bool) -> bool {
if !has_bogus_trigger(s) {
return false;
}
let tokens = tokenize_complex(s);
if tokens.is_empty() {
return false;
}
let mut prev_combinator = false;
for t in &tokens {
match t {
SelToken::Combinator => {
if prev_combinator {
return true;
}
prev_combinator = true;
}
SelToken::Compound(_) => prev_combinator = false,
}
}
if in_pseudo {
if !allow_leading && matches!(tokens.first(), Some(SelToken::Combinator)) {
return true;
}
if matches!(tokens.last(), Some(SelToken::Combinator)) {
return true;
}
}
for t in &tokens {
if let SelToken::Compound(comp) = t {
if compound_has_bogus_pseudo(comp) {
return true;
}
}
}
false
}
fn complex_selector_block_is_bogus(s: &str) -> bool {
if !has_bogus_trigger(s) {
return false;
}
if complex_selector_is_bogus(s, false, false) {
return true;
}
let tokens = tokenize_complex(s);
matches!(tokens.last(), Some(SelToken::Combinator))
}
fn has_bogus_trigger(s: &str) -> bool {
s.bytes().any(|c| matches!(c, b'>' | b'+' | b'~' | b'('))
}
fn is_selector_pseudo(name: &str) -> bool {
matches!(
name.to_ascii_lowercase().as_str(),
"not" | "is" | "matches" | "where" | "current" | "any" | "has" | "host" | "host-context" | "slotted"
)
}
fn compound_has_bogus_pseudo(compound: &str) -> bool {
let chars: Vec<char> = compound.chars().collect();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c == '\\' {
i += 2;
continue;
}
if c == '[' {
i = matching_bracket(&chars, i) + 1;
continue;
}
if c == ':' {
let mut j = i + 1;
if j < chars.len() && chars[j] == ':' {
j += 1;
}
let name_start = j;
while j < chars.len() && (is_name_char(chars[j]) || chars[j] == '\\') {
if chars[j] == '\\' {
j += 1;
}
j += 1;
}
let name: String = chars[name_start..j.min(chars.len())].iter().collect();
if j < chars.len() && chars[j] == '(' {
let open = j;
let mut depth = 0i32;
let mut k = open;
while k < chars.len() {
match chars[k] {
'\\' => {
k += 2;
continue;
}
'"' | '\'' => {
k = skip_string(&chars, k);
continue;
}
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
k += 1;
}
if is_selector_pseudo(&name) {
let allow_leading = name.eq_ignore_ascii_case("has");
let arg: String = chars[open + 1..k.min(chars.len())].iter().collect();
for part in split_commas(&arg) {
let part = part.trim();
if !part.is_empty() && complex_selector_is_bogus(part, true, allow_leading) {
return true;
}
}
}
i = k + 1;
continue;
}
i = j;
continue;
}
i += 1;
}
false
}
fn copy_name(chars: &[char], i: &mut usize, out: &mut String) {
let start = *i;
let mut has_escape = false;
while *i < chars.len() {
let c = chars[*i];
if c == '\\' {
has_escape = true;
*i += 1;
if *i < chars.len() {
let esc = chars[*i];
*i += 1;
if esc.is_ascii_hexdigit() {
let mut digits = 1;
while digits < 6 && *i < chars.len() && chars[*i].is_ascii_hexdigit() {
*i += 1;
digits += 1;
}
if *i < chars.len() && chars[*i].is_whitespace() {
*i += 1;
}
}
}
} else if is_name_char(c) {
*i += 1;
} else {
break;
}
}
if has_escape {
let raw: String = chars[start..*i].iter().collect();
out.push_str(&crate::selector::canonicalize_ident(&raw));
} else {
out.extend(chars[start..*i].iter());
}
}
fn copy_pseudo(chars: &[char], i: &mut usize, out: &mut String) {
out.push(chars[*i]); *i += 1;
let is_element = *i < chars.len() && chars[*i] == ':';
if is_element {
out.push(':');
*i += 1;
}
let name_start = *i;
copy_name(chars, i, out);
let name: String = chars[name_start..*i].iter().collect();
if *i < chars.len() && chars[*i] == '(' {
out.push('(');
*i += 1;
let trim_trailing = !is_element || is_selector_pseudo_element(&name);
while *i < chars.len() && chars[*i] == ' ' {
*i += 1;
}
let mut depth = 1i32;
while *i < chars.len() {
let c = chars[*i];
match c {
'\\' => {
out.push(c);
*i += 1;
if *i < chars.len() {
out.push(chars[*i]);
*i += 1;
}
continue;
}
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
if trim_trailing {
while out.ends_with(' ') {
out.pop();
}
}
out.push(')');
*i += 1;
break;
}
}
_ => {}
}
out.push(c);
*i += 1;
}
}
}
fn is_selector_pseudo_element(name: &str) -> bool {
let lower = name.to_ascii_lowercase();
let unvendored = lower
.strip_prefix('-')
.map_or(lower.as_str(), |rest| match rest.find('-') {
Some(idx) => &rest[idx + 1..],
None => lower.as_str(),
});
matches!(unvendored, "slotted" | "cue" | "cue-region")
}
fn copy_type_selector(chars: &[char], i: &mut usize, out: &mut String) {
if chars[*i] == '*' {
out.push('*');
*i += 1;
} else if chars[*i] == '|' {
} else {
copy_name(chars, i, out);
}
if *i < chars.len() && chars[*i] == '|' && chars.get(*i + 1) != Some(&'=') {
out.push('|');
*i += 1;
if *i < chars.len() && chars[*i] == '*' {
out.push('*');
*i += 1;
} else {
copy_name(chars, i, out);
}
}
}
fn type_selector_starts_at(chars: &[char], i: usize) -> bool {
let c = chars[i];
if c == '*' {
return chars.get(i + 1) == Some(&'|') && chars.get(i + 2) != Some(&'=');
}
if c == '|' {
return chars.get(i + 1) != Some(&'=');
}
if is_name_start(c) {
return true;
}
if c == '-' {
return matches!(chars.get(i + 1), Some(&n) if is_name_start(n) || n == '-' || n == '\\');
}
c == '\\'
}
impl ResolvedQuery {
fn render(&self) -> String {
let mut s = String::new();
if let Some(m) = &self.modifier {
s.push_str(m);
s.push(' ');
}
if let Some(t) = &self.mtype {
s.push_str(t);
if !self.conditions.is_empty() {
s.push_str(" and ");
}
}
let sep = if self.conjunction_and { " and " } else { " or " };
s.push_str(&self.conditions.join(sep));
s
}
}
fn media_query_has_interp(q: &MediaQuery) -> bool {
fn tpl(t: &[TplPiece]) -> bool {
t.iter().any(|p| matches!(p, TplPiece::Interp(_)))
}
fn expr(e: &Expr) -> bool {
match e {
Expr::Interp(_) => true,
Expr::Ident(t) | Expr::QuotedString(t) => tpl(t),
Expr::Paren(inner) | Expr::Unary { operand: inner, .. } => expr(inner),
Expr::Binary { lhs, rhs, .. } | Expr::Div { lhs, rhs, .. } => expr(lhs) || expr(rhs),
Expr::List { items, .. } => items.iter().any(expr),
_ => false,
}
}
fn in_parens(c: &MediaInParens) -> bool {
match c {
MediaInParens::Feature(f) => match &**f {
MediaFeature::Decl { name, value } => expr(name) || value.as_ref().is_some_and(expr),
MediaFeature::Range {
first, second, rest, ..
} => expr(first) || expr(second) || rest.as_ref().is_some_and(|(_, e)| expr(e)),
},
MediaInParens::Not(inner) => in_parens(inner),
MediaInParens::Group { conditions, .. } => conditions.iter().any(in_parens),
MediaInParens::Interp(_) => true,
}
}
match q {
MediaQuery::Type {
mtype, conditions, ..
} => tpl(mtype) || conditions.iter().any(in_parens),
MediaQuery::Condition { conditions, .. } => conditions.iter().any(in_parens),
}
}
fn css_media_parse_list(text: &str) -> Result<Vec<ResolvedQuery>, Error> {
let mut out = Vec::new();
for part in split_top_level_media_commas(text) {
let part = part.trim();
if part.is_empty() {
continue;
}
let mut q = css_media_parse_one(part)?;
if q.mtype.is_none() && q.modifier.is_none() && q.conditions.len() == 1 {
let c = q.conditions[0].clone();
if let Some(inner) = c.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
let t = inner.trim();
let balanced = {
let mut d = 0i32;
let mut ok = true;
for ch in t.chars() {
match ch {
'(' => d += 1,
')' => {
d -= 1;
if d < 0 {
ok = false;
break;
}
}
_ => {}
}
}
ok && d == 0
};
if balanced && (t.starts_with("not ") || t.starts_with("not(")) {
q.conditions[0] = t.to_string();
}
}
}
out.push(q);
}
Ok(out)
}
fn split_top_level_media_commas(s: &str) -> Vec<String> {
let mut parts = Vec::new();
let mut cur = String::new();
let mut depth = 0i32;
for c in s.chars() {
match c {
'(' | '[' => {
depth += 1;
cur.push(c);
}
')' | ']' => {
depth -= 1;
cur.push(c);
}
',' if depth == 0 => parts.push(std::mem::take(&mut cur)),
_ => cur.push(c),
}
}
parts.push(cur);
parts
}
fn css_media_parse_one(t: &str) -> Result<ResolvedQuery, Error> {
let chars: Vec<char> = t.chars().collect();
let mut i = 0usize;
let skip_ws = |i: &mut usize| {
while *i < chars.len() && chars[*i].is_whitespace() {
*i += 1;
}
};
let take_paren = |i: &mut usize| -> Result<String, Error> {
let start = *i;
let mut depth = 0i32;
while *i < chars.len() {
match chars[*i] {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
*i += 1;
return Ok(chars[start..*i].iter().collect());
}
}
_ => {}
}
*i += 1;
}
Err(Error::unpositioned("expected \")\"."))
};
let take_ident = |i: &mut usize| -> String {
let start = *i;
while *i < chars.len() && !chars[*i].is_whitespace() && chars[*i] != '(' {
*i += 1;
}
chars[start..*i].iter().collect()
};
skip_ws(&mut i);
if i < chars.len() && chars[i] == '(' {
let mut conditions = vec![take_paren(&mut i)?];
let mut conjunction_and = true;
loop {
skip_ws(&mut i);
if i >= chars.len() {
break;
}
let word = take_ident(&mut i);
skip_ws(&mut i);
match word.to_ascii_lowercase().as_str() {
"and" => conditions.push(take_paren(&mut i)?),
"or" => {
conjunction_and = false;
conditions.push(take_paren(&mut i)?);
}
_ => return Err(Error::unpositioned("expected \"and\" or \"or\".")),
}
}
return Ok(ResolvedQuery {
modifier: None,
mtype: None,
conditions,
conjunction_and,
});
}
let id1 = take_ident(&mut i);
skip_ws(&mut i);
if id1.eq_ignore_ascii_case("not") && i < chars.len() && chars[i] == '(' {
let cond = take_paren(&mut i)?;
return Ok(ResolvedQuery {
modifier: None,
mtype: None,
conditions: vec![format!("not {cond}")],
conjunction_and: true,
});
}
let mut modifier = None;
let mut mtype = id1;
if i < chars.len() && chars[i] != '(' {
let save = i;
let id2 = take_ident(&mut i);
if !id2.is_empty() && !id2.eq_ignore_ascii_case("and") {
modifier = Some(std::mem::replace(&mut mtype, id2));
} else {
i = save;
}
}
let mut conditions = Vec::new();
loop {
skip_ws(&mut i);
if i >= chars.len() {
break;
}
let word = take_ident(&mut i);
if !word.eq_ignore_ascii_case("and") {
return Err(Error::unpositioned("expected \"and\"."));
}
skip_ws(&mut i);
conditions.push(take_paren(&mut i)?);
}
Ok(ResolvedQuery {
modifier,
mtype: Some(mtype),
conditions,
conjunction_and: true,
})
}
fn serialize_media_queries(queries: &[ResolvedQuery]) -> String {
queries
.iter()
.map(ResolvedQuery::render)
.collect::<Vec<_>>()
.join(", ")
}
fn merge_media_query_lists(outer: &[ResolvedQuery], inner: &[ResolvedQuery]) -> Option<Vec<ResolvedQuery>> {
let mut merged = Vec::new();
for a in outer {
for b in inner {
match merge_media_query(a, b) {
MergeResult::Empty => continue,
MergeResult::Unrepresentable => return None,
MergeResult::Query(q) => merged.push(q),
}
}
}
Some(merged)
}
fn merge_media_query(this: &ResolvedQuery, other: &ResolvedQuery) -> MergeResult {
if !this.conjunction_and || !other.conjunction_and {
return MergeResult::Unrepresentable;
}
let our_modifier = this.modifier.as_ref().map(|s| s.to_ascii_lowercase());
let our_type = this.mtype.as_ref().map(|s| s.to_ascii_lowercase());
let their_modifier = other.modifier.as_ref().map(|s| s.to_ascii_lowercase());
let their_type = other.mtype.as_ref().map(|s| s.to_ascii_lowercase());
if our_type.is_none() && their_type.is_none() {
let mut conditions = this.conditions.clone();
conditions.extend(other.conditions.iter().cloned());
return MergeResult::Query(ResolvedQuery {
modifier: None,
mtype: None,
conditions,
conjunction_and: true,
});
}
let our_not = our_modifier.as_deref() == Some("not");
let their_not = their_modifier.as_deref() == Some("not");
let is_all = |t: &Option<String>| t.as_deref() == Some("all");
let (modifier, mtype, conditions): (Option<String>, Option<String>, Vec<String>);
if our_not != their_not {
if our_type == their_type {
let (neg, pos) = if our_not {
(&this.conditions, &other.conditions)
} else {
(&other.conditions, &this.conditions)
};
if neg.iter().all(|c| pos.contains(c)) {
return MergeResult::Empty;
}
return MergeResult::Unrepresentable;
} else if our_type.is_none() || is_all(&our_type) || their_type.is_none() || is_all(&their_type) {
return MergeResult::Unrepresentable;
}
if our_not {
modifier = their_modifier.clone();
mtype = their_type.clone();
conditions = other.conditions.clone();
} else {
modifier = our_modifier.clone();
mtype = our_type.clone();
conditions = this.conditions.clone();
}
} else if our_not {
if our_type != their_type {
return MergeResult::Unrepresentable;
}
let (more, fewer) = if this.conditions.len() > other.conditions.len() {
(&this.conditions, &other.conditions)
} else {
(&other.conditions, &this.conditions)
};
if !fewer.iter().all(|c| more.contains(c)) {
return MergeResult::Unrepresentable;
}
modifier = our_modifier.clone();
mtype = our_type.clone();
conditions = more.clone();
} else if our_type.is_none() || is_all(&our_type) {
mtype = if (their_type.is_none() || is_all(&their_type)) && our_type.is_none() {
None
} else {
their_type.clone()
};
let mut c = this.conditions.clone();
c.extend(other.conditions.iter().cloned());
conditions = c;
modifier = their_modifier.clone();
} else if their_type.is_none() || is_all(&their_type) {
let mut c = this.conditions.clone();
c.extend(other.conditions.iter().cloned());
conditions = c;
modifier = our_modifier.clone();
mtype = our_type.clone();
} else if our_type != their_type {
return MergeResult::Empty;
} else {
modifier = our_modifier.clone().or_else(|| their_modifier.clone());
let mut c = this.conditions.clone();
c.extend(other.conditions.iter().cloned());
conditions = c;
mtype = our_type.clone();
}
let final_type = match &mtype {
None => None,
Some(_) if mtype == our_type => this.mtype.clone(),
Some(_) => other.mtype.clone(),
};
MergeResult::Query(ResolvedQuery {
modifier,
mtype: final_type,
conditions,
conjunction_and: true,
})
}
enum CondEval {
Bool(bool),
Css(RCond),
}
enum RCond {
Css(String),
Not(Box<RCond>),
And(Vec<RCond>),
Or(Vec<RCond>),
Paren(Box<RCond>),
}
impl RCond {
fn to_css(&self) -> String {
match self {
RCond::Css(s) => s.clone(),
RCond::Not(c) => format!("not {}", c.to_css()),
RCond::And(items) => items.iter().map(RCond::to_css).collect::<Vec<_>>().join(" and "),
RCond::Or(items) => items.iter().map(RCond::to_css).collect::<Vec<_>>().join(" or "),
RCond::Paren(c) => format!("({})", c.to_css()),
}
}
}
fn combine_residuals(mut residuals: Vec<RCond>, is_and: bool) -> CondEval {
match residuals.len() {
0 => CondEval::Bool(is_and),
1 => {
let single = residuals.pop().unwrap_or(RCond::Css(String::new()));
let unwrapped = match single {
RCond::Paren(inner) => *inner,
other => other,
};
CondEval::Css(unwrapped)
}
_ => {
if is_and {
CondEval::Css(RCond::And(residuals))
} else {
CondEval::Css(RCond::Or(residuals))
}
}
}
}
fn serialize_if_value(v: &Value) -> String {
match v {
Value::List(_) => format!("({})", v.to_css(false)),
Value::Null => "null".to_string(),
other => other.to_css(false),
}
}