use crate::error::Error;
use crate::scanner::Pos;
use crate::value::{List, ListSep, Number, SassStr, Value};
pub(super) const NAMES: &[&str] = &[
"length",
"nth",
"set-nth",
"join",
"append",
"index",
"list-separator",
"is-bracketed",
"zip",
];
pub(super) fn try_call(
name: &str,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
) -> Option<Result<Value, Error>> {
Some(match name {
"length" => fn_length(pos_args, named, pos),
"nth" => fn_nth(pos_args, named, pos),
"set-nth" => fn_set_nth(pos_args, named, pos),
"join" => fn_join(pos_args, named, pos),
"append" => fn_append(pos_args, named, pos),
"index" => fn_index(pos_args, named, pos),
"list-separator" => fn_list_separator(pos_args, named, pos),
"is-bracketed" => fn_is_bracketed(pos_args, named, pos),
"zip" => fn_zip(pos_args, named),
_ => return None,
})
}
pub(super) fn call_module_member(
member: &str,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
) -> Option<Result<Value, Error>> {
Some(match member {
"slash" => fn_slash(pos_args, named, pos),
_ => return None,
})
}
fn fn_slash(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
if !named.is_empty() {
return Err(Error::at(
"No arguments named ($".to_string() + &named[0].0 + ").",
pos,
));
}
if pos_args.len() < 2 {
return Err(Error::at("At least two elements are required.".to_string(), pos));
}
Ok(Value::List(List {
items: pos_args.to_vec().into(),
sep: ListSep::Slash,
bracketed: false,
keywords: None,
}))
}
fn as_items(v: &Value) -> (Vec<Value>, ListSep) {
match v {
Value::List(l) => (l.items.to_vec(), l.sep),
Value::Map(m) => {
let items = m
.entries
.iter()
.map(|(k, val)| {
Value::List(List {
items: vec![k.clone(), val.clone()].into(),
sep: ListSep::Space,
bracketed: false,
keywords: None,
})
})
.collect();
(items, ListSep::Comma)
}
other => (vec![other.clone()], ListSep::Space),
}
}
fn list_len(v: &Value) -> usize {
match v {
Value::List(l) => l.items.len(),
_ => 1,
}
}
fn unitless(value: f64) -> Value {
Value::Number(Number::unitless(value))
}
fn resolve_index(
params: &[&str],
pos_args: &[Value],
named: &[(String, Value)],
i: usize,
fname: &str,
len: usize,
pos: Pos,
) -> Result<usize, Error> {
let v = super::require(params, pos_args, named, i, fname, pos)?;
let pname = params.get(i).copied().unwrap_or("");
let raw = match v {
Value::Number(n) => n.value,
other => {
return Err(Error::at(
format!("${pname}: {} is not a number.", other.to_css(false)),
pos,
))
}
};
if raw.fract() != 0.0 {
return Err(Error::at(
format!("${pname}: {} is not an int.", crate::value::fmt_num(raw, false)),
pos,
));
}
let index = raw as i64;
if index == 0 {
return Err(Error::at(format!("${pname}: List index may not be 0."), pos));
}
let len_i = len as i64;
if index.abs() > len_i {
return Err(Error::at(
format!(
"${pname}: Invalid index {} for a list with {} element{}.",
crate::value::fmt_num(raw, false),
len,
if len == 1 { "" } else { "s" }
),
pos,
));
}
let zero_based = if index > 0 { index - 1 } else { len_i + index };
Ok(zero_based as usize)
}
fn parse_separator(v: &Value, pos: Pos) -> Result<Option<ListSep>, Error> {
if let Value::Str(SassStr { text, .. }) = v {
match text.as_ref() {
"auto" => return Ok(None),
"comma" => return Ok(Some(ListSep::Comma)),
"space" => return Ok(Some(ListSep::Space)),
"slash" => return Ok(Some(ListSep::Slash)),
_ => {}
}
}
Err(Error::at(
"$separator: Must be \"space\", \"comma\", \"slash\", or \"auto\".",
pos,
))
}
fn fn_length(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
validate_args(&["list"], pos_args, named, pos)?;
let v = super::require(&["list"], pos_args, named, 0, "length", pos)?;
Ok(unitless(list_len(v) as f64))
}
fn fn_nth(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = ["list", "n"];
validate_args(¶ms, pos_args, named, pos)?;
let list = super::require(¶ms, pos_args, named, 0, "nth", pos)?;
let (items, _) = as_items(list);
let idx = resolve_index(¶ms, pos_args, named, 1, "nth", items.len(), pos)?;
items
.get(idx)
.cloned()
.ok_or_else(|| Error::at("Internal index error.", pos))
}
fn fn_set_nth(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = ["list", "n", "value"];
validate_args(¶ms, pos_args, named, pos)?;
let list = super::require(¶ms, pos_args, named, 0, "set-nth", pos)?;
let (mut items, sep) = as_items(list);
let idx = resolve_index(¶ms, pos_args, named, 1, "set-nth", items.len(), pos)?;
let value = super::require(¶ms, pos_args, named, 2, "set-nth", pos)?.clone();
if let Some(slot) = items.get_mut(idx) {
*slot = value;
}
let bracketed = matches!(list, Value::List(l) if l.bracketed);
Ok(Value::List(List::new(items, sep, bracketed)))
}
fn settled_sep(v: &Value) -> Option<ListSep> {
match v {
Value::List(l) if l.sep != ListSep::Undecided => Some(l.sep),
Value::Map(m) if !m.entries.is_empty() => Some(ListSep::Comma),
_ => None,
}
}
fn fn_join(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = ["list1", "list2", "separator", "bracketed"];
validate_args(¶ms, pos_args, named, pos)?;
let list1 = super::require(¶ms, pos_args, named, 0, "join", pos)?;
let list2 = super::require(¶ms, pos_args, named, 1, "join", pos)?;
let (items1, _) = as_items(list1);
let (items2, _) = as_items(list2);
let sep = match super::arg(¶ms, pos_args, named, 2) {
Some(v) => match parse_separator(v, pos)? {
Some(s) => s,
None => join_auto_separator(list1, list2),
},
None => join_auto_separator(list1, list2),
};
let bracketed = match super::arg(¶ms, pos_args, named, 3) {
None => matches!(list1, Value::List(l) if l.bracketed),
Some(Value::Str(s)) if !s.quoted && &*s.text == "auto" => {
matches!(list1, Value::List(l) if l.bracketed)
}
Some(v) => v.is_truthy(),
};
let mut items = items1;
items.extend(items2);
Ok(Value::List(List::new(items, sep, bracketed)))
}
fn too_many(passed: usize, max: usize, pos: Pos) -> Error {
Error::at(
format!(
"Only {} argument{} allowed, but {} {} passed.",
max,
if max == 1 { "" } else { "s" },
passed,
if passed == 1 { "was" } else { "were" }
),
pos,
)
}
fn validate_args(
params: &[&str],
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
) -> Result<(), Error> {
if let Some((n, _)) = named.iter().find(|(n, _)| !params.contains(&n.as_str())) {
return Err(Error::at(format!("No parameter named ${n}."), pos));
}
let total = pos_args.len() + named.len();
if total > params.len() {
return Err(too_many(total, params.len(), pos));
}
Ok(())
}
fn join_auto_separator(list1: &Value, list2: &Value) -> ListSep {
settled_sep(list1)
.or_else(|| settled_sep(list2))
.unwrap_or(ListSep::Space)
}
fn fn_append(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = ["list", "val", "separator"];
validate_args(¶ms, pos_args, named, pos)?;
let list = super::require(¶ms, pos_args, named, 0, "append", pos)?;
let val = super::require(¶ms, pos_args, named, 1, "append", pos)?.clone();
let (mut items, _) = as_items(list);
let sep = match super::arg(¶ms, pos_args, named, 2) {
Some(v) => match parse_separator(v, pos)? {
Some(s) => s,
None => settled_sep(list).unwrap_or(ListSep::Space),
},
None => settled_sep(list).unwrap_or(ListSep::Space),
};
let bracketed = matches!(list, Value::List(l) if l.bracketed);
items.push(val);
Ok(Value::List(List::new(items, sep, bracketed)))
}
fn fn_index(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = ["list", "value"];
validate_args(¶ms, pos_args, named, pos)?;
let list = super::require(¶ms, pos_args, named, 0, "index", pos)?;
let value = super::require(¶ms, pos_args, named, 1, "index", pos)?;
let (items, _) = as_items(list);
match items.iter().position(|item| item.sass_eq(value)) {
Some(i) => Ok(unitless((i + 1) as f64)),
None => Ok(Value::Null),
}
}
fn fn_list_separator(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
validate_args(&["list"], pos_args, named, pos)?;
let v = super::require(&["list"], pos_args, named, 0, "list-separator", pos)?;
let sep = settled_sep(v).unwrap_or(ListSep::Space);
let text = match sep {
ListSep::Comma => "comma",
ListSep::Space | ListSep::Undecided => "space",
ListSep::Slash => "slash",
};
Ok(Value::Str(SassStr {
text: text.into(),
quoted: false,
}))
}
fn fn_is_bracketed(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
validate_args(&["list"], pos_args, named, pos)?;
let v = super::require(&["list"], pos_args, named, 0, "is-bracketed", pos)?;
let bracketed = matches!(v, Value::List(l) if l.bracketed);
Ok(Value::Bool(bracketed))
}
fn fn_zip(pos_args: &[Value], named: &[(String, Value)]) -> Result<Value, Error> {
let lists: Vec<Vec<Value>> = pos_args
.iter()
.chain(named.iter().map(|(_, v)| v))
.map(|v| as_items(v).0)
.collect();
let rows = lists.iter().map(|l| l.len()).min().unwrap_or(0);
let mut out = Vec::with_capacity(rows);
for i in 0..rows {
let row: Vec<Value> = lists.iter().map(|l| l[i].clone()).collect();
out.push(Value::List(List {
items: row.into(),
sep: ListSep::Space,
bracketed: false,
keywords: None,
}));
}
Ok(Value::List(List {
items: out.into(),
sep: ListSep::Comma,
bracketed: false,
keywords: None,
}))
}
#[cfg(test)]
mod tests {
use super::*;
fn pos() -> Pos {
Pos { line: 1, col: 1 }
}
fn n(value: f64) -> Value {
Value::Number(Number::unitless(value))
}
fn num_unit(value: f64, unit: &str) -> Value {
Value::Number(Number::with_unit(value, unit.to_string()))
}
fn s(text: &str) -> Value {
Value::Str(SassStr {
text: text.into(),
quoted: false,
})
}
fn list(items: Vec<Value>, sep: ListSep) -> Value {
Value::List(List {
items: items.into(),
sep,
bracketed: false,
keywords: None,
})
}
fn call(name: &str, args: &[Value]) -> Value {
try_call(name, args, &[], pos())
.expect("name owned by list family")
.expect("no error")
}
fn call_err(name: &str, args: &[Value]) -> Error {
try_call(name, args, &[], pos())
.expect("name owned by list family")
.expect_err("expected error")
}
#[test]
fn length_counts_elements() {
assert_eq!(call("length", &[list(vec![], ListSep::Space)]).to_css(false), "0");
assert_eq!(call("length", &[num_unit(5.0, "px")]).to_css(false), "1");
assert_eq!(
call(
"length",
&[list(
vec![num_unit(1.0, "px"), num_unit(2.0, "px"), num_unit(3.0, "px")],
ListSep::Space
)]
)
.to_css(false),
"3"
);
}
#[test]
fn nth_one_based_and_negative() {
let l = list(vec![s("a"), s("b"), s("c")], ListSep::Space);
assert_eq!(call("nth", &[l.clone(), n(1.0)]).to_css(false), "a");
assert_eq!(call("nth", &[l.clone(), n(-1.0)]).to_css(false), "c");
assert_eq!(call("nth", &[s("solo"), n(1.0)]).to_css(false), "solo");
}
#[test]
fn nth_index_errors() {
let l = list(vec![s("a"), s("b"), s("c")], ListSep::Space);
assert_eq!(
call_err("nth", &[l.clone(), n(0.0)]).message,
"$n: List index may not be 0."
);
assert_eq!(
call_err("nth", &[l.clone(), n(5.0)]).message,
"$n: Invalid index 5 for a list with 3 elements."
);
assert_eq!(call_err("nth", &[l, n(1.5)]).message, "$n: 1.5 is not an int.");
}
#[test]
fn set_nth_replaces_and_keeps_separator() {
let l = list(vec![s("a"), s("b"), s("c")], ListSep::Space);
assert_eq!(
call("set-nth", &[l.clone(), n(2.0), s("x")]).to_css(false),
"a x c"
);
assert_eq!(call("set-nth", &[l, n(-1.0), s("z")]).to_css(false), "a b z");
assert_eq!(call("set-nth", &[s("a"), n(1.0), s("z")]).to_css(false), "z");
}
#[test]
fn join_default_uses_list1_separator() {
let a = list(vec![num_unit(1.0, "px"), num_unit(2.0, "px")], ListSep::Space);
let b = list(vec![num_unit(3.0, "px"), num_unit(4.0, "px")], ListSep::Space);
assert_eq!(call("join", &[a, b]).to_css(false), "1px 2px 3px 4px");
let c = list(vec![s("a"), s("b")], ListSep::Comma);
let d = list(vec![s("c"), s("d")], ListSep::Comma);
assert_eq!(call("join", &[c, d]).to_css(false), "a, b, c, d");
}
#[test]
fn join_single_list_borrows_list2_separator() {
let a = s("a");
let b = list(vec![s("b"), s("c")], ListSep::Comma);
assert_eq!(call("join", &[a, b]).to_css(false), "a, b, c");
}
#[test]
fn join_explicit_separator() {
let a = list(vec![num_unit(1.0, "px"), num_unit(2.0, "px")], ListSep::Space);
assert_eq!(
call("join", &[a, num_unit(3.0, "px"), s("comma")]).to_css(false),
"1px, 2px, 3px"
);
}
#[test]
fn append_default_and_explicit() {
let a = list(vec![num_unit(1.0, "px"), num_unit(2.0, "px")], ListSep::Space);
assert_eq!(
call("append", &[a.clone(), num_unit(3.0, "px")]).to_css(false),
"1px 2px 3px"
);
let c = list(vec![s("a"), s("b")], ListSep::Comma);
assert_eq!(call("append", &[c, s("c")]).to_css(false), "a, b, c");
assert_eq!(
call("append", &[a, num_unit(3.0, "px"), s("comma")]).to_css(false),
"1px, 2px, 3px"
);
}
#[test]
fn append_to_empty_list() {
assert_eq!(
call("append", &[list(vec![], ListSep::Space), s("x")]).to_css(false),
"x"
);
}
#[test]
fn index_finds_or_null() {
let l = list(vec![s("a"), s("b"), s("c")], ListSep::Space);
assert_eq!(call("index", &[l.clone(), s("b")]).to_css(false), "2");
assert!(matches!(call("index", &[l, s("z")]), Value::Null));
let nums = list(
vec![num_unit(1.0, "px"), num_unit(2.0, ""), num_unit(3.0, "px")],
ListSep::Space,
);
assert!(matches!(call("index", &[nums, num_unit(2.0, "px")]), Value::Null));
}
#[test]
fn list_separator_reports_keyword() {
assert_eq!(
call("list-separator", &[list(vec![s("a"), s("b")], ListSep::Comma)]).to_css(false),
"comma"
);
assert_eq!(
call("list-separator", &[list(vec![s("a"), s("b")], ListSep::Space)]).to_css(false),
"space"
);
assert_eq!(call("list-separator", &[s("a")]).to_css(false), "space");
assert_eq!(
call("list-separator", &[list(vec![], ListSep::Comma)]).to_css(false),
"comma"
);
assert_eq!(
call("list-separator", &[list(vec![], ListSep::Space)]).to_css(false),
"space"
);
}
#[test]
fn is_bracketed_reports_flag() {
let bracketed = Value::List(List {
items: vec![s("a"), s("b")].into(),
sep: ListSep::Space,
bracketed: true,
keywords: None,
});
assert!(matches!(call("is-bracketed", &[bracketed]), Value::Bool(true)));
assert!(matches!(
call("is-bracketed", &[list(vec![s("a"), s("b")], ListSep::Space)]),
Value::Bool(false)
));
assert!(matches!(call("is-bracketed", &[s("a")]), Value::Bool(false)));
assert!(matches!(
call("is-bracketed", &[list(vec![], ListSep::Space)]),
Value::Bool(false)
));
}
#[test]
fn zip_interleaves_to_shortest() {
let a = list(vec![n(1.0), n(2.0), n(3.0)], ListSep::Space);
let b = list(vec![s("c"), s("d"), s("e")], ListSep::Space);
assert_eq!(call("zip", &[a, b]).to_css(false), "1 c, 2 d, 3 e");
let a = list(vec![n(1.0), n(2.0), n(3.0)], ListSep::Space);
let b = list(vec![s("c"), s("d")], ListSep::Space);
assert_eq!(call("zip", &[a, b]).to_css(false), "1 c, 2 d");
let a = list(vec![s("a"), s("b"), s("c")], ListSep::Space);
assert_eq!(call("zip", &[a]).to_css(false), "a, b, c");
}
#[test]
fn rejects_unknown_names_and_bad_separator() {
assert!(try_call("frobnicate", &[s("x")], &[], pos()).is_none());
let err = call_err("join", &[s("a"), s("b"), s("dash")]);
assert_eq!(
err.message,
"$separator: Must be \"space\", \"comma\", \"slash\", or \"auto\"."
);
}
}