use std::borrow::Cow;
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::{CanonicalUrl, CanonicalizeContext, Importer, OutputStyle, Syntax};
mod at_rules;
mod binop;
mod calc;
mod control_flow;
mod expr;
mod meta;
mod modules;
mod plain_css;
mod scope;
use binop::*;
pub(crate) use binop::eval_div;
type CachedImport = std::rc::Rc<(String, Syntax, crate::ast::Stylesheet, std::rc::Rc<str>)>;
type PreModuleComments = Rc<RefCell<HashMap<String, Vec<(String, SrcLines)>>>>;
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()))
}
#[derive(Clone)]
pub(crate) struct EnvModules {
pub(self) used_modules: HashMap<String, String>,
pub(self) star_modules: Vec<String>,
pub(self) used_user_modules: HashMap<String, Rc<Module>>,
pub(self) star_user_modules: Vec<Rc<Module>>,
}
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>,
pub env_modules: EnvModules,
}
#[derive(Clone)]
pub(crate) enum RuleSelectors {
Raw(Vec<String>),
Parsed(Rc<[crate::selector::Complex]>),
}
impl RuleSelectors {
pub(crate) fn to_strings(&self) -> std::borrow::Cow<'_, [String]> {
match self {
RuleSelectors::Raw(v) => std::borrow::Cow::Borrowed(v),
RuleSelectors::Parsed(v) => std::borrow::Cow::Owned(v.iter().map(|c| c.render()).collect()),
}
}
fn into_strings(self) -> Vec<String> {
match self {
RuleSelectors::Raw(v) => v,
RuleSelectors::Parsed(v) => v.iter().map(|c| c.render()).collect(),
}
}
}
#[derive(Clone)]
pub(crate) enum OutNode {
Rule {
selectors: RuleSelectors,
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,
},
GroupEnd,
MediaHoist,
AtRootHoist {
target: usize,
},
AtRootPackTight,
}
impl OutNode {
pub(crate) fn plain_rule(selectors: Vec<String>, items: Vec<OutItem>, lines: SrcLines) -> OutNode {
OutNode::Rule {
selectors: RuleSelectors::Raw(selectors),
linebreaks: Vec::new(),
items,
lines,
extend_base: usize::MAX,
}
}
pub(crate) fn childless_at_rule(name: String, prelude: String, lines: SrcLines) -> OutNode {
OutNode::AtRule {
name,
prelude,
body: Vec::new(),
has_block: false,
lines,
}
}
}
#[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 {
body: &'a mut Vec<OutNode>,
group_ends: bool,
},
}
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::childless_at_rule(name, prelude, 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::GroupEnd);
}
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::childless_at_rule(name, prelude, lines))
}
OutItem::NestedRule { selectors, items } => {
body.push(OutNode::plain_rule(selectors, items, SrcLines::default()))
}
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::plain_rule(selectors, items, SrcLines::default())
}
OutItem::ChildlessAtRule { name, prelude, lines } => {
OutNode::childless_at_rule(name, prelude, lines)
}
OutItem::NestedAtRule { name, prelude, items } => OutNode::AtRule {
name,
prelude,
body: vec![OutNode::plain_rule(Vec::new(), items, SrcLines::default())],
has_block: true,
lines: SrcLines::default(),
},
})
.collect(),
has_block: true,
lines: SrcLines::default(),
}),
},
Sink::Top(_) => {}
}
}
fn flush_rule_block(&mut self) -> bool {
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 true;
}
}
let rule = OutNode::Rule {
selectors: RuleSelectors::Raw(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);
return true;
}
}
}
false
}
fn emit_style_rule(&mut self, output: Vec<OutNode>, allow_group_end: bool) {
match self {
Sink::Top(out) => {
let out = &mut **out;
let output = materialize_interior_group_ends(output);
let produced = output
.iter()
.any(|n| !matches!(n, OutNode::Blank | OutNode::AtRootPackTight | OutNode::GroupEnd));
push_group(out, output);
if produced && !out.is_empty() {
out.push(if allow_group_end {
OutNode::GroupEnd
} else {
OutNode::AtRootPackTight
});
}
}
Sink::Rule { .. } => {
self.flush_rule_block();
if let Sink::Rule { nested, .. } = self {
nested.extend(output);
}
}
Sink::AtRoot { body, group_ends } => {
let produced = *group_ends
&& output
.iter()
.any(|n| !matches!(n, OutNode::Blank | OutNode::AtRootPackTight | OutNode::GroupEnd));
body.extend(output);
if produced {
body.push(if allow_group_end {
OutNode::GroupEnd
} else {
OutNode::AtRootPackTight
});
}
}
}
}
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 functions: &'a [crate::host_fn::HostFn],
pub source: &'a str,
pub url: &'a str,
pub glyphs: crate::diag::GlyphSet,
pub warn: Option<&'a crate::WarnHandler>,
}
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>>>,
pre_module_comments: Option<PreModuleComments>,
pre_comment_floor: usize,
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>,
cur_rule_lines: SrcLines,
cur_rule_extend_base: usize,
bogus_selectors: Vec<String>,
placeholder_rules: Vec<(String, String)>,
import_clone: Option<(String, std::collections::HashSet<String>)>,
current_file_dir: Option<String>,
current_canonical: Option<CanonicalUrl>,
in_keyframes: bool,
in_unknown_at_rule: bool,
last_child_invisible: 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>,
file_map_urls: HashMap<String, String>,
}
#[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,
canonical: String,
emitted_main: std::cell::Cell<bool>,
css: Vec<OutNode>,
phantom_css: bool,
}
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_canonical = CanonicalUrl::new(options.url);
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(),
file_map_urls: 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()),
pre_module_comments: None,
pre_comment_floor: 0,
load_css_copies: RefCell::new(Vec::new()),
copy_counter: std::cell::Cell::new(0),
in_keyframes: false,
in_unknown_at_rule: false,
last_child_invisible: false,
at_root_excluding_style_rule: false,
import_clone: None,
current_file_dir: Some(entry_dir),
current_canonical: Some(entry_canonical),
media_hoist: Vec::new(),
at_root_hoist: std::collections::VecDeque::new(),
at_rule_ctx: Vec::new(),
cur_rule_lines: SrcLines::default(),
cur_rule_extend_base: usize::MAX,
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(())
}
pub(crate) fn source_table(
&mut self,
entry_url: &str,
include_sources: bool,
) -> (Vec<String>, Option<Vec<String>>) {
if self.file_ids.is_empty() {
self.file_ids.insert(entry_url.to_string(), 1);
}
let mut by_id: Vec<(u32, &str)> = self
.file_ids
.iter()
.map(|(url, &id)| (id, url.as_str()))
.collect();
by_id.sort_by_key(|&(id, _)| id);
let sources: Vec<String> = by_id
.iter()
.map(|&(_, url)| {
self.file_map_urls
.get(url)
.cloned()
.unwrap_or_else(|| url.to_string())
})
.collect();
let content = if include_sources {
let srcs = self.file_sources.borrow();
Some(
by_id
.iter()
.map(|&(_, url)| srcs.get(url).map(|s| s.to_string()).unwrap_or_default())
.collect(),
)
} else {
None
};
(sources, content)
}
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));
let formatted = format!("{block}\n");
self.emit_diag(crate::WarnEvent {
kind: crate::WarnKind::Warn,
deprecation: true,
deprecation_id: dep.id,
message: &dep.message,
formatted: &formatted,
url: &self.current_url,
line: pos.line,
});
}
fn emit_deprecation_footer(&self) {
if self.deprecations_omitted == 0 {
return;
}
let msg = format!(
"{} repetitive deprecation warnings omitted.",
self.deprecations_omitted
);
let formatted = format!("WARNING: {msg}\nRun in verbose mode to see all warnings.\n");
self.emit_diag(crate::WarnEvent {
kind: crate::WarnKind::Warn,
deprecation: true,
deprecation_id: "",
message: &msg,
formatted: &formatted,
url: "",
line: 0,
});
}
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_diag(&self, ev: crate::WarnEvent<'_>) {
match self.options.warn {
Some(handler) => handler(&ev),
None => eprintln!("{}", ev.formatted),
}
}
fn emit_warn(&mut self, value: &Expr, pos: Pos) -> Result<(), Error> {
let v = self.eval_expr(value)?;
let msg = v.to_message();
let formatted = if self.diag_enabled() {
let frames = self.frames_for(pos);
format!("WARNING: {}\n{}\n", msg, Self::render_frame_block(&frames, 4))
} else {
format!("WARNING: {msg}")
};
self.emit_diag(crate::WarnEvent {
kind: crate::WarnKind::Warn,
deprecation: false,
deprecation_id: "",
message: &msg,
formatted: &formatted,
url: "",
line: 0,
});
Ok(())
}
fn emit_debug(&mut self, value: &Expr, pos: Pos) -> Result<(), Error> {
let v = self.eval_expr(value)?;
let msg = v.to_message();
let url = self.current_url.clone();
let formatted = if self.diag_enabled() {
format!("{url}:{} DEBUG: {msg}", pos.line)
} else {
format!("DEBUG: {msg}")
};
self.emit_diag(crate::WarnEvent {
kind: crate::WarnKind::Debug,
deprecation: false,
deprecation_id: "",
message: &msg,
formatted: &formatted,
url: &url,
line: pos.line,
});
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 compressed(&self) -> bool {
matches!(self.options.style, OutputStyle::Compressed)
}
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);
self.last_child_invisible = false;
}
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);
self.last_child_invisible = false;
}
}
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)?;
self.last_child_invisible = false;
}
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);
self.last_child_invisible = false;
}
}
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 i in 0..items.len() {
self.bind_each(vars, items.get(i));
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)?;
self.last_child_invisible = false;
}
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,
)?;
}
self.last_child_invisible = false;
}
Stmt::CssCustomAtRule { name, prelude, body } => {
self.eval_css_custom_at_rule(name, prelude, body, sink)?;
self.last_child_invisible = false;
}
Stmt::Media { query, body, lines } => {
let stamped = self.stamp(*lines);
self.eval_media(query, body, stamped, parents, sink)?;
self.last_child_invisible = false;
}
Stmt::Supports {
condition,
body,
lines,
} => {
let stamped = self.stamp(*lines);
self.eval_supports(condition, body, stamped, parents, sink)?;
self.last_child_invisible = false;
}
Stmt::AtRoot { query, body } => {
self.eval_at_root(query.as_deref(), body, parents, sink)?;
self.last_child_invisible = false;
}
Stmt::Keyframes {
name,
prelude,
body,
lines,
} => {
let lines = self.stamp(*lines);
self.eval_keyframes(name, prelude, body, lines, sink)?;
self.last_child_invisible = false;
}
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 lbs_fast = self.in_keyframes || (self.current_linebreaks.is_empty() && !sel_str.contains('\n'));
let part_lbs: Vec<bool> = if lbs_fast {
Vec::new()
} else {
comma_linebreaks(&sel_str, false)
};
let parent_lbs: &[bool] = if self.current_linebreaks.len() == parents.len() {
&self.current_linebreaks
} else {
&[]
};
let (current, resolved_lbs): (Vec<String>, Vec<bool>) = if self.in_keyframes {
(
split_commas(&sel_str)
.into_iter()
.map(|p| p.trim().to_string())
.filter(|p| !p.is_empty())
.collect(),
Vec::new(),
)
} else {
resolve_selectors_opt(
&sel_str,
parents,
!self.at_root_excluding_style_rule,
&part_lbs,
parent_lbs,
)?
.into_iter()
.unzip()
};
let full_lbs: Vec<bool> = if lbs_fast { Vec::new() } else { resolved_lbs };
let mut emit_selectors: Vec<String> = Vec::with_capacity(current.len());
let mut emit_linebreaks: Vec<bool> = Vec::with_capacity(current.len());
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,
start_col: (rule.selector_pos.col as u32).saturating_sub(1),
map_file: 0,
map_line: 0,
});
let prev_rule_lines = std::mem::replace(&mut self.cur_rule_lines, rule_lines);
let prev_rule_extend_base = std::mem::replace(&mut self.cur_rule_extend_base, self.extends.len());
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();
self.last_child_invisible = false;
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() {
self.last_child_invisible = false;
}
r
};
self.current_selector = prev_selector;
self.current_linebreaks = prev_linebreaks;
self.at_root_excluding_style_rule = prev_at_root;
self.cur_rule_lines = prev_rule_lines;
self.cur_rule_extend_base = prev_rule_extend_base;
self.pop_scope();
result?;
let body_last_invisible = std::mem::replace(&mut self.last_child_invisible, false);
let this_rule_invisible = nested.is_empty() && !self.in_keyframes;
sink.emit_style_rule(nested, !body_last_invisible);
self.last_child_invisible = this_rule_invisible || body_last_invisible;
Ok(())
}
fn eval_decl(&mut self, d: &Declaration) -> Result<Option<OutItem>, Error> {
let name = trim_owned(self.eval_template(&d.property)?);
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,
start_col: (d.pos.col as u32).saturating_sub(1),
map_file: 0,
map_line: 0,
}),
}))
}
fn eval_custom_decl(&mut self, d: &CustomDecl) -> Result<Option<OutItem>, Error> {
let prop = trim_owned(self.eval_template(&d.property)?);
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,
start_col: (d.pos.col as u32).saturating_sub(1),
map_file: 0,
map_line: 0,
}),
}))
}
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 = trim_owned(self.eval_template(&ps.property)?);
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 = match importer {
Some(imp) => {
let ctx = CanonicalizeContext {
from_import: true,
containing_url: self.current_canonical.as_ref(),
};
match imp.canonicalize(path, &ctx) {
Err(e) => {
crate::arena::resume(saved);
return Err(Error::unpositioned(e.message));
}
Ok(None) => None,
Ok(Some(canon)) => match imp.load(&canon) {
Err(e) => {
crate::arena::resume(saved);
return Err(Error::unpositioned(e.message));
}
Ok(None) => None,
Ok(Some(res)) => {
Some((canon.as_str().to_string(), res.contents, res.syntax))
}
},
}
}
None => None,
};
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 = match parse_with_syntax(&src, syntax) {
Ok(sheet) => sheet,
Err(err) => {
let diag = self.module_diag_url(path, &resolved_key);
let saved_member = self.enter_call(*pos, *length, "@import");
let saved_url = std::mem::replace(&mut self.current_url, diag);
let saved_source = std::mem::replace(
&mut self.current_source,
Rc::from(src.as_str()),
);
let err = self.finalize_error(err);
self.current_url = saved_url;
self.current_source = saved_source;
self.leave_call(saved_member);
return Err(err);
}
};
let e = std::rc::Rc::new((
resolved_key,
syntax,
sheet,
std::rc::Rc::<str>::from(src.as_str()),
));
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);
let saved_member = self.enter_call(*pos, *length, "@import");
let import_diag = if resolved_key.is_empty() {
self.current_url.clone()
} else {
self.module_diag_url(path, resolved_key)
};
if self.diag_enabled() && !resolved_key.is_empty() {
self.file_sources
.borrow_mut()
.entry(import_diag.clone())
.or_insert_with(|| Rc::clone(&entry.3));
}
let saved_import_url = std::mem::replace(&mut self.current_url, import_diag);
let saved_import_source =
std::mem::replace(&mut self.current_source, Rc::clone(&entry.3));
self.current_url_stamp = 0;
if matches!(syntax, Syntax::Css) {
self.loading.push(path.clone());
let result = self
.exec_css(&sheet.stmts, parents, sink)
.map_err(|e| self.finalize_error(e));
self.loading.pop();
self.current_url = saved_import_url;
self.current_source = saved_import_source;
self.current_url_stamp = 0;
self.leave_call(saved_member);
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).into_owned(), (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_canonical = if resolved_key.is_empty() {
self.current_canonical.clone()
} else {
self.current_canonical
.replace(CanonicalUrl::new(resolved_key.clone()))
};
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)
.map_err(|e| self.finalize_error(e));
self.leave_call(saved_member);
self.current_url = saved_import_url;
self.current_source = saved_import_source;
self.current_url_stamp = 0;
self.current_file_dir = saved_dir;
self.current_canonical = saved_canonical;
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 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,
} => {
let selectors = selectors.into_strings();
if selectors.iter().any(|s| part_has_parent_ref(s)) {
preserved.push(OutItem::NestedRule { selectors, items });
} else {
rest.push(OutNode::Rule {
selectors: RuleSelectors::Raw(
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::plain_rule(
parents.to_vec(),
preserved,
SrcLines::default(),
));
}
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,
)
}
#[derive(Clone, Copy, PartialEq)]
enum DeclScope {
Allowed,
Control,
Function,
Mixin,
}
#[derive(Clone, Copy)]
struct ScopeCtx {
decl: DeclScope,
in_style_rule: bool,
defer_extend: bool,
}
impl ScopeCtx {
fn root() -> Self {
ScopeCtx {
decl: DeclScope::Allowed,
in_style_rule: false,
defer_extend: false,
}
}
fn with(self, decl: DeclScope) -> Self {
ScopeCtx { decl, ..self }
}
}
pub(crate) fn validate_declarations(sheet: &Stylesheet) -> Result<(), Error> {
validate_decl_scope(&sheet.stmts, ScopeCtx::root())
}
fn validate_decl_scope(stmts: &[Stmt], ctx: ScopeCtx) -> Result<(), Error> {
let scope = ctx.decl;
for stmt in stmts {
if scope == DeclScope::Function {
match stmt {
Stmt::Rule(r) if is_empty_parens_selector(&r.selector) => {}
Stmt::Rule(_) => {
return Err(Error::unpositioned(
"@function rules may not contain style rules.".to_string(),
));
}
Stmt::Decl(_) | Stmt::CustomDecl(_) | Stmt::PropertySet(_) => {
return Err(Error::unpositioned(
"@function rules may not contain declarations.".to_string(),
));
}
Stmt::VarDecl(_)
| Stmt::Return(_)
| Stmt::Warn { .. }
| Stmt::Debug { .. }
| Stmt::Error { .. }
| Stmt::Comment(..)
| Stmt::If(_)
| Stmt::For { .. }
| Stmt::Each { .. }
| Stmt::While { .. }
| Stmt::FunctionDef(_)
| Stmt::MixinDef(_) => {}
_ => {
return Err(Error::unpositioned(
"This at-rule is not allowed here.".to_string(),
));
}
}
}
match stmt {
Stmt::FunctionDef(c) => {
if let Some(msg) = decl_error(scope, "function") {
return Err(Error::unpositioned(msg));
}
validate_decl_scope(&c.body, ctx.with(DeclScope::Function))?;
}
Stmt::MixinDef(c) => {
if let Some(msg) = decl_error(scope, "mixin") {
return Err(Error::unpositioned(msg));
}
validate_decl_scope(&c.body, ctx.with(DeclScope::Mixin))?;
}
Stmt::Content(_) if scope != DeclScope::Mixin => {
return Err(Error::unpositioned(
"@content is only allowed within mixin declarations.".to_string(),
));
}
Stmt::Extend { .. } if !ctx.defer_extend && !ctx.in_style_rule && scope != DeclScope::Mixin => {
return Err(Error::unpositioned(
"@extend may only be used within style rules.".to_string(),
));
}
Stmt::If(branches) => {
let inner = ctx.with(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, ctx.with(enter_control(scope)))?;
}
Stmt::Rule(r) => {
validate_decl_scope(
&r.body,
ScopeCtx {
in_style_rule: true,
..ctx
},
)?;
}
Stmt::AtRoot { body, .. } => {
validate_decl_scope(
body,
ScopeCtx {
defer_extend: true,
..ctx
},
)?;
}
Stmt::AtRule { body: Some(body), .. }
| Stmt::Media { body, .. }
| Stmt::Supports { body, .. }
| Stmt::Keyframes { body, .. } => validate_decl_scope(body, ctx)?,
Stmt::Include {
content: Some(content),
..
} => validate_decl_scope(
content,
ScopeCtx {
defer_extend: true,
..ctx
},
)?,
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 is_empty_parens_selector(selector: &[TplPiece]) -> bool {
match selector {
[TplPiece::Lit(s)] => s.trim() == "()",
_ => false,
}
}
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 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;
rest.push(OutNode::ModuleScope {
key,
nodes: inner_rest,
});
}
}
_ => 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 mut tail = own.split_off(li + 1);
imports.extend(own.into_iter().filter(|n| !matches!(n, OutNode::Blank)));
while matches!(tail.first(), Some(OutNode::Blank)) {
tail.remove(0);
}
if tail.iter().any(|n| !matches!(n, OutNode::Blank)) {
*css_seen = true;
}
push_group(&mut rest, tail);
return rest;
}
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);
let mut rest = rest;
while matches!(rest.first(), Some(OutNode::Blank)) {
rest.remove(0);
}
out.extend(rest);
while matches!(out.last(), Some(OutNode::Blank)) {
out.pop();
}
}
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);
}
}
pub(super) fn materialize_interior_group_ends(nodes: Vec<OutNode>) -> Vec<OutNode> {
let is_content = |n: &OutNode| {
!matches!(
n,
OutNode::Blank
| OutNode::GroupEnd
| OutNode::MediaHoist
| OutNode::AtRootHoist { .. }
| OutNode::AtRootPackTight
)
};
let mut result: Vec<OutNode> = Vec::with_capacity(nodes.len());
let mut iter = nodes.into_iter().peekable();
while let Some(node) = iter.next() {
if matches!(node, OutNode::GroupEnd) && iter.peek().is_some_and(is_content) {
result.push(OutNode::Blank);
} else {
result.push(node);
}
}
result
}
fn push_group(out: &mut Vec<OutNode>, mut group: Vec<OutNode>) {
if group.is_empty() {
return;
}
if group.len() == 1 && matches!(&group[0], OutNode::AtRootPackTight | OutNode::GroupEnd) {
out.append(&mut group);
return;
}
fn invisible_wrapper(n: &OutNode) -> bool {
match n {
OutNode::Blank => true,
OutNode::ModuleScope { nodes, .. } => nodes.iter().all(invisible_wrapper),
_ => false,
}
}
fn last_effective(n: &OutNode) -> &OutNode {
if let OutNode::ModuleScope { nodes, .. } = n {
if let Some(l) = nodes.iter().rev().find(|x| !invisible_wrapper(x)) {
return last_effective(l);
}
}
n
}
let top_marker = matches!(out.last(), Some(OutNode::GroupEnd));
if top_marker {
out.pop();
}
let last_eff = out.last().map(last_effective);
let prev_group_end = top_marker || matches!(last_eff, Some(OutNode::GroupEnd));
let prev_packs_tight = match last_eff {
Some(OutNode::AtRule { .. } | OutNode::Comment(..)) => true,
Some(
OutNode::Raw(_) | OutNode::MediaHoist | OutNode::AtRootHoist { .. } | OutNode::AtRootPackTight,
) => true,
_ => 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) -> Cow<'_, str> {
if name.contains('\\') {
return Cow::Owned(decode_ident_escapes(name).replace('_', "-"));
}
if name.contains('_') {
Cow::Owned(name.replace('_', "-"))
} else {
Cow::Borrowed(name)
}
}
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,
}
}
pub(super) fn css_value_error_msg(v: &Value) -> Option<String> {
if let Some(m) = find_map(v) {
return Some(format!("{} isn't a valid CSS value.", m.to_css(false)));
}
if let Value::List(l) = v {
if l.items.is_empty() && !l.bracketed {
return Some("() isn't a valid CSS value.".to_string());
}
}
None
}
pub(super) fn interp_checked(v: &Value) -> Result<String, Error> {
if let Some(msg) = css_value_error_msg(v) {
return Err(Error::unpositioned(msg));
}
Ok(v.to_interp())
}
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())
}
struct AtRootBatch {
target: usize,
group_end: bool,
nodes: Vec<OutNode>,
}
fn is_escaping_marker(n: &OutNode, depth: usize) -> bool {
match n {
OutNode::MediaHoist => true,
OutNode::AtRootHoist { target } => *target < 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: selectors.into_strings(),
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
| OutNode::GroupEnd
| OutNode::MediaHoist
| OutNode::AtRootHoist { .. }
| OutNode::AtRootPackTight => {}
}
}
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().contains(&b':') {
validate_pseudo_names(sel)?;
}
if sel.as_bytes().contains(&b'(') && sel.as_bytes().contains(&b':') {
crate::selector::validate_pseudo_args(sel).map_err(Error::unpositioned)?;
}
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_pseudo_names(sel: &str) -> Result<(), Error> {
let chars: Vec<char> = sel.chars().collect();
let mut i = 0usize;
while i < chars.len() {
match chars[i] {
'\\' => {
i += 2;
}
'"' | '\'' => i = skip_string(&chars, i),
'[' => i = matching_bracket(&chars, i) + 1,
':' => {
i += 1;
if chars.get(i) == Some(&':') {
i += 1;
}
if !pseudo_name_starts_at(&chars, i) {
return Err(Error::unpositioned("Expected identifier."));
}
}
_ => i += 1,
}
}
Ok(())
}
fn pseudo_name_starts_at(chars: &[char], i: usize) -> bool {
let is_start = |c: char| c.is_ascii_alphabetic() || c == '_' || c == '\\' || (c as u32) >= 0x80;
match chars.get(i).copied() {
Some(c) if is_start(c) => true,
Some('-') => matches!(chars.get(i + 1).copied(), Some(n) if is_start(n) || n == '-'),
_ => false,
}
}
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.to_strings().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>>,
order: &ExtendOrderCtx,
) {
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();
let plan = crate::selector::build_extend_plan(
&visible,
scope,
if order.has_import_clones {
std::collections::HashMap::new()
} else {
order.rank_for(scope)
},
order.has_import_clones,
);
rewrite_with_scopes(nodes, &plan, scope, all, origins, closures, order);
}
pub(crate) struct ExtendOrderCtx {
pub rev_deps: HashMap<String, Vec<String>>,
pub first_reg: HashMap<String, usize>,
pub has_import_clones: bool,
}
impl ExtendOrderCtx {
fn rank_for(&self, scope: &str) -> std::collections::HashMap<String, usize> {
let mut rank = std::collections::HashMap::new();
let mut next = 0usize;
let mut stack: Vec<String> = Vec::new();
let push_children =
|of: &str, stack: &mut Vec<String>, rank: &std::collections::HashMap<String, usize>| {
let mut kids: Vec<&String> = self
.rev_deps
.get(of)
.map(|v| {
v.iter()
.filter(|k| !rank.contains_key(*k) && k.as_str() != scope)
.collect()
})
.unwrap_or_default();
kids.sort_by_key(|k| self.first_reg.get(*k).copied().unwrap_or(usize::MAX));
for k in kids {
stack.push(k.clone());
}
};
push_children(scope, &mut stack, &rank);
while let Some(m) = stack.pop() {
if rank.contains_key(&m) {
continue;
}
rank.insert(m.clone(), next);
next += 1;
push_children(&m, &mut stack, &rank);
}
rank
}
}
fn rewrite_with_scopes(
nodes: &mut Vec<OutNode>,
plan: &crate::selector::ExtendPlan,
scope: &str,
all: &[crate::selector::Extension],
origins: &[String],
closures: &HashMap<String, std::collections::HashSet<String>>,
order: &ExtendOrderCtx,
) {
for node in nodes.iter_mut() {
match node {
OutNode::ModuleScope { key, nodes } => {
let key = key.clone();
rewrite_nodes_scoped(nodes, &key, all, origins, closures, order);
}
OutNode::AtRule { name, body, .. } if !is_keyframes_name(name) => {
rewrite_with_scopes(body, plan, scope, all, origins, closures, order);
}
_ => {}
}
}
rewrite_nodes(nodes, plan, scope);
}
fn rewrite_nodes(nodes: &mut Vec<OutNode>, plan: &crate::selector::ExtendPlan, scope: &str) {
let mut i = 0;
while i < nodes.len() {
let drop = match &mut nodes[i] {
OutNode::ModuleScope { nodes, .. } => {
fn invisible(nodes: &[OutNode]) -> bool {
nodes.iter().all(|n| match n {
OutNode::Blank | OutNode::GroupEnd | OutNode::AtRootPackTight => true,
OutNode::ModuleScope { nodes, .. } => invisible(nodes),
_ => false,
})
}
invisible(nodes)
}
OutNode::Rule {
selectors,
linebreaks,
extend_base,
..
} => {
{
match extend_selector_list(selectors, linebreaks, plan, scope, *extend_base) {
SelectorRewrite::Unchanged => false,
SelectorRewrite::Changed(s, b) => {
*linebreaks = b;
*selectors = s;
false
}
SelectorRewrite::Drop => 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);
i -= 1;
}
} 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.as_ref());
}
if let Some(h) = &hide {
return !h.contains(n.as_ref());
}
true
}
}
fn normalize_var_name(name: &str) -> Cow<'_, str> {
if name.contains('_') {
Cow::Owned(name.replace('_', "-"))
} else {
Cow::Borrowed(name)
}
}
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_ref(), "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).into_owned()),
(crate::ast::ForwardMember::Name(n), false) => Some(normalize_var_name(n).into_owned()),
_ => 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())
}
enum SelectorRewrite {
Unchanged,
Changed(RuleSelectors, Vec<bool>),
Drop,
}
fn extend_selector_list(
selectors: &RuleSelectors,
breaks: &[bool],
plan: &crate::selector::ExtendPlan,
scope: &str,
extend_base: usize,
) -> SelectorRewrite {
let has_placeholder = match selectors {
RuleSelectors::Raw(v) => v.iter().any(|s| s.contains('%')),
RuleSelectors::Parsed(v) => v.iter().any(crate::selector::complex_has_placeholder),
};
if plan.is_empty() && !has_placeholder {
return SelectorRewrite::Unchanged;
}
let parsed_owned;
let parsed: &[crate::selector::Complex] = match selectors {
RuleSelectors::Raw(v) => {
let joined = v.join(", ");
match crate::selector::parse_list(&joined) {
Some(p) => {
parsed_owned = p;
&parsed_owned
}
None => return SelectorRewrite::Unchanged,
}
}
RuleSelectors::Parsed(v) => v,
};
let result = crate::selector::extend_selectors(parsed, breaks, plan, scope, extend_base);
if result.all_placeholders {
return SelectorRewrite::Drop;
}
SelectorRewrite::Changed(RuleSelectors::Parsed(result.selectors.into()), 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,
part_lbs: &[bool],
parent_lbs: &[bool],
) -> Result<Vec<(String, bool)>, 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, bool)>| {
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]);
let s = substitute_pseudo_refs(&s).unwrap_or(s);
let flag = idx.iter().any(|&pi| parent_lbs.get(pi).copied().unwrap_or(false));
result.push((normalize_selector(&s), flag));
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<(String, bool)> = Vec::new();
if parents.is_empty() {
for (pi, part) in parts.iter().enumerate() {
result.push((
normalize_selector(part),
part_lbs.get(pi).copied().unwrap_or(false),
));
}
} else if !implicit_parent {
for (part_i, part) in parts.iter().enumerate() {
if let Some(s) = substitute_pseudo_refs(part) {
result.push((normalize_selector(&s), false));
} 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 (pi, parent) in parents.iter().enumerate() {
check_compound_parent(part, parent)?;
result.push((
normalize_selector(&replace_parent_refs(part, parent)),
parent_lbs.get(pi).copied().unwrap_or(false),
));
}
} else {
result.push((
normalize_selector(part),
part_lbs.get(part_i).copied().unwrap_or(false),
));
}
}
} else {
let mut rows: Vec<Vec<(String, bool)>> = Vec::with_capacity(parts.len());
for (part_i, part) in parts.iter().enumerate() {
if let Some(s) = substitute_pseudo_refs(part) {
rows.push(vec![(normalize_selector(&s), false)]);
continue;
}
if let Some(segments) = split_parent_refs(part) {
for parent in parents {
check_compound_parent(part, parent)?;
}
let mut row = Vec::new();
expand_cartesian(&segments, &mut row);
rows.push(row);
continue;
}
let has_ref = part_has_parent_ref(part);
let mut row = Vec::with_capacity(parents.len());
for (pi, parent) in parents.iter().enumerate() {
let parent_lb = parent_lbs.get(pi).copied().unwrap_or(false);
let (combined, flag) = if has_ref {
check_compound_parent(part, parent)?;
(replace_parent_refs(part, parent), parent_lb)
} else {
(
format!("{parent} {part}"),
part_lbs.get(part_i).copied().unwrap_or(false) || parent_lb,
)
};
row.push((normalize_selector(&combined), flag));
}
rows.push(row);
}
let longest = rows.iter().map(|r| r.len()).max().unwrap_or(0);
for j in 0..longest {
for row in &rows {
if let Some(entry) = row.get(j) {
result.push(entry.clone());
}
}
}
}
Ok(result)
}
fn trim_owned(s: String) -> String {
if s.trim().len() == s.len() {
s
} else {
s.trim().to_string()
}
}
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 paren = 0i32;
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() {
let mut has_nl = c == '\n';
ci += 1;
while ci < cs.len() && cs[ci].is_whitespace() {
has_nl |= cs[ci] == '\n';
ci += 1;
}
if !prev_space {
if paren > 0 && has_nl && collapsed.ends_with(',') {
collapsed.push('\n');
} else {
collapsed.push(' ');
}
}
prev_space = true;
continue;
}
match c {
'(' => paren += 1,
')' => paren -= 1,
_ => {}
}
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 == '\\'
}
fn push_media_sep(s: &mut String, is_and: bool, compressed: bool) {
let word = if is_and { "and" } else { "or" };
if compressed && s.ends_with(')') {
s.push_str(word);
} else {
s.push(' ');
s.push_str(word);
}
s.push(' ');
}
impl ResolvedQuery {
fn render(&self, compressed: bool) -> 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() {
push_media_sep(&mut s, true, compressed);
}
}
for (i, c) in self.conditions.iter().enumerate() {
if i > 0 {
push_media_sep(&mut s, self.conjunction_and, compressed);
}
s.push_str(c);
}
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], compressed: bool) -> String {
let sep = if compressed { "," } else { ", " };
queries
.iter()
.map(|q| q.render(compressed))
.collect::<Vec<_>>()
.join(sep)
}
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),
}
}