use crate::error::Error;
use crate::scanner::Pos;
use crate::selector::{self, Complex};
use crate::value::{List, ListSep, SassStr, Value};
pub(super) fn try_call(
name: &str,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
) -> Option<Result<Value, Error>> {
Some(match name {
"selector-nest" => fn_nest(pos_args, named, pos),
"selector-append" => fn_append(pos_args, named, pos),
"selector-extend" => fn_extend(pos_args, named, pos),
"selector-replace" => fn_replace(pos_args, named, pos),
"selector-unify" => fn_unify(pos_args, named, pos),
"is-superselector" => fn_is_superselector(pos_args, named, pos),
"simple-selectors" => fn_simple_selectors(pos_args, named, pos),
"selector-parse" => fn_parse(pos_args, named, pos),
_ => return None,
})
}
fn check_arity(pos_args: &[Value], max: usize, pos: Pos) -> Result<(), Error> {
if pos_args.len() > max {
return Err(Error::at(
format!(
"Only {} argument{} allowed, but {} {} passed.",
max,
if max == 1 { "" } else { "s" },
pos_args.len(),
if pos_args.len() == 1 { "was" } else { "were" }
),
pos,
));
}
Ok(())
}
fn value_to_selector_string(v: &Value, pname: &str, pos: Pos) -> Result<String, Error> {
fn space_or_string(v: &Value) -> Option<String> {
match v {
Value::Str(s) => Some(s.text.clone()),
Value::List(l) if l.sep == ListSep::Space && !l.items.is_empty() => {
let mut parts = Vec::with_capacity(l.items.len());
for item in &l.items {
match item {
Value::Str(s) => parts.push(s.text.clone()),
_ => return None,
}
}
Some(parts.join(" "))
}
_ => None,
}
}
fn render(v: &Value) -> Option<String> {
match v {
Value::Str(s) => Some(s.text.clone()),
Value::List(l) if l.items.is_empty() => None,
Value::List(l) => match l.sep {
ListSep::Comma => {
let mut parts = Vec::with_capacity(l.items.len());
for item in &l.items {
parts.push(space_or_string(item)?);
}
Some(parts.join(", "))
}
ListSep::Space | ListSep::Undecided => space_or_string(v),
ListSep::Slash => None,
},
_ => None,
}
}
render(v).ok_or_else(|| {
Error::at(
format!(
"${pname}: {} is not a valid selector: it must be a string,\na list of strings, or a list of lists of strings.",
v.to_css(true)
),
pos,
)
})
}
fn selector_list_arg(
params: &[&str],
pos_args: &[Value],
named: &[(String, Value)],
i: usize,
fname: &str,
pos: Pos,
) -> Result<Vec<Complex>, Error> {
let v = super::require(params, pos_args, named, i, fname, pos)?;
let pname = params.get(i).copied().unwrap_or("selector");
let text = value_to_selector_string(v, pname, pos)?;
parse_selector_text(&text, pname, pos)
}
fn parse_selector_text(text: &str, pname: &str, pos: Pos) -> Result<Vec<Complex>, Error> {
if text.trim().is_empty() {
return Err(Error::at(format!("${pname}: expected selector."), pos));
}
let normalized = crate::eval::normalize_selector(text);
selector::parse_list(&normalized).ok_or_else(|| Error::at(format!("${pname}: expected selector."), pos))
}
fn selectors_to_value(complexes: &[Complex]) -> Value {
let items: Vec<Value> = complexes.iter().map(complex_to_value).collect();
Value::List(List {
items,
sep: ListSep::Comma,
bracketed: false,
keywords: None,
})
}
fn complex_to_value(c: &Complex) -> Value {
let parts = selector::complex_to_list_parts(c);
let items: Vec<Value> = parts
.into_iter()
.map(|text| Value::Str(SassStr { text, quoted: false }))
.collect();
Value::List(List {
items,
sep: ListSep::Space,
bracketed: false,
keywords: None,
})
}
fn resolve_parents_str(child: &str, parents: &[String], fname: &str, pos: Pos) -> Result<Vec<String>, Error> {
let parent_str = parents.join(", ");
let parent_strs: Vec<String> = parents.to_vec();
let child_complexes = split_top_commas(child);
for cc in &child_complexes {
validate_parent_placement(cc)?;
}
let mut groups: Vec<Vec<String>> = Vec::new();
for cc in &child_complexes {
if has_parent_ref(cc) {
groups.push(expand_parent_refs(cc, &parent_strs, &parent_str));
} else {
groups.push(parent_strs.iter().map(|p| format!("{p} {}", cc.trim())).collect());
}
}
let mut out: Vec<String> = Vec::new();
for resolved in flatten_vertically(groups) {
if has_parent_ref(&resolved) {
out.push(resolved);
continue;
}
let parsed = selector::parse_list(&resolved)
.ok_or_else(|| Error::at(format!("Invalid selector produced by {fname}(): {resolved}"), pos))?;
out.extend(parsed.iter().map(Complex::render));
}
if out.is_empty() {
return Ok(parse_selector_text(&parent_str, "selectors", pos)?
.iter()
.map(Complex::render)
.collect());
}
Ok(out)
}
fn selector_strings_to_value(complexes: &[String]) -> Value {
let items: Vec<Value> = complexes
.iter()
.map(|c| {
let parts = split_top_ws(c);
Value::List(List {
items: parts
.into_iter()
.map(|text| Value::Str(SassStr { text, quoted: false }))
.collect(),
sep: ListSep::Space,
bracketed: false,
keywords: None,
})
})
.collect();
Value::List(List {
items,
sep: ListSep::Comma,
bracketed: false,
keywords: None,
})
}
fn split_top_ws(s: &str) -> Vec<String> {
let mut out = Vec::new();
let mut cur = String::new();
let mut depth = 0i32;
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
match c {
'\\' => {
cur.push(c);
if let Some(n) = chars.next() {
cur.push(n);
}
}
'"' | '\'' => {
cur.push(c);
let q = c;
for n in chars.by_ref() {
cur.push(n);
if n == q {
break;
}
}
}
'(' | '[' => {
depth += 1;
cur.push(c);
}
')' | ']' => {
depth -= 1;
cur.push(c);
}
c if c.is_whitespace() && depth == 0 => {
if !cur.is_empty() {
out.push(std::mem::take(&mut cur));
}
}
_ => cur.push(c),
}
}
if !cur.is_empty() {
out.push(cur);
}
out
}
fn flatten_vertically(groups: Vec<Vec<String>>) -> Vec<String> {
if groups.len() == 1 {
return groups.into_iter().next().unwrap_or_default();
}
let mut queues: Vec<std::collections::VecDeque<String>> = groups.into_iter().map(|g| g.into()).collect();
let mut out = Vec::new();
while !queues.is_empty() {
queues.retain_mut(|q| {
if let Some(v) = q.pop_front() {
out.push(v);
}
!q.is_empty()
});
}
out
}
fn validate_parent_placement(s: &str) -> Result<(), Error> {
let chars: Vec<char> = s.chars().collect();
let mut i = 0usize;
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;
}
'"' | '\'' => {
let q = c;
i += 1;
while i < chars.len() {
if chars[i] == '\\' {
i += 2;
continue;
}
if chars[i] == q {
i += 1;
break;
}
i += 1;
}
at_compound_start = false;
continue;
}
'[' | '(' => {
depth += 1;
at_compound_start = false;
}
']' | ')' => {
depth -= 1;
at_compound_start = false;
}
_ if depth > 0 => {}
' ' | '\t' | '\n' | '\r' | '>' | '+' | '~' => at_compound_start = true,
'&' => {
if !at_compound_start {
return Err(Error::unpositioned(
"\"&\" may only used at the beginning of a compound selector.",
));
}
at_compound_start = false;
}
_ => at_compound_start = false,
}
i += 1;
}
Ok(())
}
fn has_parent_ref(s: &str) -> bool {
let mut bracket = 0i32;
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
match c {
'\\' => {
chars.next();
}
'"' | '\'' => {
let q = c;
for n in chars.by_ref() {
if n == '\\' {
continue;
}
if n == q {
break;
}
}
}
'[' => bracket += 1,
']' => bracket -= 1,
'&' if bracket == 0 => return true,
_ => {}
}
}
false
}
fn expand_parent_refs(s: &str, parents: &[String], parent_list: &str) -> Vec<String> {
let mut variants: Vec<String> = vec![String::new()];
let mut chars = s.chars().peekable();
let mut paren = 0i32;
let push_all = |variants: &mut Vec<String>, text: &str| {
for v in variants.iter_mut() {
v.push_str(text);
}
};
while let Some(c) = chars.next() {
match c {
'\\' => {
let mut t = String::from(c);
if let Some(n) = chars.next() {
t.push(n);
}
push_all(&mut variants, &t);
}
'"' | '\'' => {
let mut t = String::from(c);
let q = c;
for n in chars.by_ref() {
t.push(n);
if n == q {
break;
}
}
push_all(&mut variants, &t);
}
'(' => {
paren += 1;
push_all(&mut variants, "(");
}
')' => {
paren -= 1;
push_all(&mut variants, ")");
}
'&' if paren > 0 => push_all(&mut variants, parent_list),
'&' => {
variants = variants
.iter()
.flat_map(|v| parents.iter().map(move |p| format!("{v}{p}")))
.collect();
}
_ => push_all(&mut variants, &c.to_string()),
}
}
variants
}
fn split_top_commas(s: &str) -> Vec<String> {
let mut out = Vec::new();
let mut cur = String::new();
let mut paren = 0i32;
let mut bracket = 0i32;
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
match c {
'\\' => {
cur.push(c);
if let Some(n) = chars.next() {
cur.push(n);
}
}
'"' | '\'' => {
cur.push(c);
let q = c;
for n in chars.by_ref() {
cur.push(n);
if n == q {
break;
}
}
}
'(' => {
paren += 1;
cur.push(c);
}
')' => {
paren -= 1;
cur.push(c);
}
'[' => {
bracket += 1;
cur.push(c);
}
']' => {
bracket -= 1;
cur.push(c);
}
',' if paren == 0 && bracket == 0 => out.push(std::mem::take(&mut cur)),
_ => cur.push(c),
}
}
out.push(cur);
out.into_iter().filter(|p| !p.trim().is_empty()).collect()
}
fn fn_nest(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let mut all: Vec<Value> = pos_args.to_vec();
for (_, v) in named {
all.push(v.clone());
}
if all.is_empty() {
return Err(Error::at(
"$selectors: At least one selector must be passed.".to_string(),
pos,
));
}
let first = value_to_selector_string(&all[0], "selectors", pos)?;
let mut acc: Vec<String> = if has_parent_ref(&first) {
let parts = split_top_commas(&first);
for cc in &parts {
validate_parent_placement(cc)?;
let chars: Vec<char> = cc.chars().collect();
for (i, ch) in chars.iter().enumerate() {
if *ch == '&' {
if let Some(n) = chars.get(i + 1) {
if n.is_alphanumeric() || *n == '-' || *n == '_' || *n == '\\' {
return Err(Error::at(
"A top-level selector may not contain a parent selector with a suffix."
.to_string(),
pos,
));
}
}
}
}
}
parts.iter().map(|p| p.trim().to_string()).collect()
} else {
parse_arg_selector(&all[0], pos)?
.iter()
.map(Complex::render)
.collect()
};
for v in &all[1..] {
let child = value_to_selector_string(v, "selectors", pos)?;
acc = resolve_parents_str(&child, &acc, "selector-nest", pos)?;
}
Ok(selector_strings_to_value(&acc))
}
fn parse_arg_selector(v: &Value, pos: Pos) -> Result<Vec<Complex>, Error> {
let text = value_to_selector_string(v, "selectors", pos)?;
parse_selector_text(&text, "selectors", pos)
}
fn fn_append(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let mut all: Vec<Value> = pos_args.to_vec();
for (_, v) in named {
all.push(v.clone());
}
if all.is_empty() {
return Err(Error::at(
"$selectors: At least one selector must be passed.".to_string(),
pos,
));
}
let mut acc = parse_arg_selector(&all[0], pos)?;
for v in &all[1..] {
let suffix = parse_arg_selector(v, pos)?;
acc = append_lists(&acc, &suffix, pos)?;
}
Ok(selectors_to_value(&acc))
}
fn append_lists(prefix: &[Complex], suffix: &[Complex], pos: Pos) -> Result<Vec<Complex>, Error> {
let mut out: Vec<Complex> = Vec::new();
for p in prefix {
if !p.trailing.is_empty() {
return Err(Error::at(
format!(
"Selector \"{}\" can't be used as a parent in a compound selector.",
p.render()
),
pos,
));
}
let p_one = p.render();
for s in suffix {
let s_str = s.render();
if s.components.is_empty() {
return Err(Error::at(format!("Can't append {s_str} to {p_one}."), pos));
}
if let Some(first) = s.components.first() {
let leading_blocks_append = matches!(
first.compound.simples.first(),
Some(crate::selector::Simple::Universal { .. })
) || matches!(
first.compound.simples.first(),
Some(crate::selector::Simple::Type(t)) if t.contains('|')
);
if first.combinator().is_some() || leading_blocks_append {
return Err(Error::at(format!("Can't append {s_str} to {p_one}."), pos));
}
}
let combined = format!("{p_one}{s_str}");
let parsed = selector::parse_list(&combined)
.ok_or_else(|| Error::at(format!("Can't append {s_str} to {p_one}."), pos))?;
out.extend(parsed);
}
}
Ok(out)
}
fn fn_extend(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
extend_or_replace(pos_args, named, pos, false, "selector-extend")
}
fn fn_replace(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
extend_or_replace(pos_args, named, pos, true, "selector-replace")
}
fn extend_or_replace(
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
replace: bool,
fname: &str,
) -> Result<Value, Error> {
let params: &[&str] = if replace {
&["selector", "original", "replacement"]
} else {
&["selector", "extendee", "extender"]
};
check_arity(pos_args, 3, pos)?;
let selector = selector_list_arg(params, pos_args, named, 0, fname, pos)?;
let target_list = selector_list_arg(params, pos_args, named, 1, fname, pos)?;
let extender = selector_list_arg(params, pos_args, named, 2, fname, pos)?;
let targets = compound_targets(&target_list, pos)?;
let complexes = selector::extend_compound_target(&selector, &targets, &extender, replace);
if complexes.is_empty() {
return Err(Error::at(format!("{fname}() produced an empty selector."), pos));
}
Ok(selectors_to_value(&complexes))
}
fn compound_targets(list: &[Complex], pos: Pos) -> Result<Vec<crate::selector::Compound>, Error> {
let mut out = Vec::with_capacity(list.len());
for complex in list {
if complex.components.len() == 1 && complex.components[0].combinator().is_none() {
out.push(complex.components[0].compound.clone());
} else {
return Err(Error::at(
format!("Can't extend complex selector {}.", complex.render()),
pos,
));
}
}
Ok(out)
}
fn fn_unify(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = &["selector1", "selector2"];
check_arity(pos_args, 2, pos)?;
let s1 = selector_list_arg(params, pos_args, named, 0, "selector-unify", pos)?;
let s2 = selector_list_arg(params, pos_args, named, 1, "selector-unify", pos)?;
match selector::unify_lists(&s1, &s2) {
Some(unified) => Ok(selectors_to_value(&unified)),
None => Ok(Value::Null),
}
}
fn fn_is_superselector(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = &["super", "sub"];
check_arity(pos_args, 2, pos)?;
let sup = selector_list_arg(params, pos_args, named, 0, "is-superselector", pos)?;
let sub = selector_list_arg(params, pos_args, named, 1, "is-superselector", pos)?;
Ok(Value::Bool(selector::list_is_superselector(&sup, &sub)))
}
fn fn_simple_selectors(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = &["selector"];
check_arity(pos_args, 1, pos)?;
let v = super::require(params, pos_args, named, 0, "simple-selectors", pos)?;
let text = value_to_selector_string(v, "selector", pos)?;
if text.trim().is_empty() {
return Err(Error::at("$selector: expected selector.".to_string(), pos));
}
let simples = selector::parse_compound_simples(&text)
.ok_or_else(|| Error::at("$selector: expected selector.".to_string(), pos))?;
let items: Vec<Value> = simples
.into_iter()
.map(|text| Value::Str(SassStr { text, quoted: false }))
.collect();
Ok(Value::List(List {
items,
sep: ListSep::Comma,
bracketed: false,
keywords: None,
}))
}
fn fn_parse(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = &["selector"];
check_arity(pos_args, 1, pos)?;
let list = selector_list_arg(params, pos_args, named, 0, "selector-parse", pos)?;
Ok(selectors_to_value(&list))
}