use super::{check, CallError, FunctionMap};
use crate::css::Value;
use crate::value::ListSeparator;
use crate::Scope;
pub fn create_module() -> Scope {
let mut f = Scope::builtin_module("sass:list");
def!(f, append(list, val, separator = b"auto"), |s| {
let (mut list, sep, bra) = get_list(s.get(name!(list))?);
let sep = s
.get_map(name!(separator), check_separator)?
.or(sep)
.unwrap_or(ListSeparator::Space);
list.push(s.get(name!(val))?);
Ok(Value::List(list, Some(sep), bra))
});
def!(f, index(list, value), |s| match s.get(name!(list))? {
Value::List(v, _, _) => {
let value = s.get(name!(value))?;
for (i, v) in v.iter().enumerate() {
if v == &value {
return Ok(Value::scalar(i + 1));
}
}
Ok(Value::Null)
}
Value::Map(map) => match s.get(name!(value))? {
Value::List(ref l, Some(ListSeparator::Space), _)
if l.len() == 2 =>
{
for (i, (k, v)) in map.iter().enumerate() {
if *k == l[0] && *v == l[1] {
return Ok(Value::scalar(i + 1));
}
}
Ok(Value::Null)
}
_ => Ok(Value::Null),
},
v => {
if v == s.get(name!(value))? {
Ok(Value::scalar(1))
} else {
Ok(Value::Null)
}
}
});
def!(f, is_bracketed(list), |s| Ok(match s.get(name!(list))? {
Value::List(_, _, true) => Value::True,
_ => Value::False,
}));
def!(
f,
join(list1, list2, separator = b"auto", bracketed = b"auto"),
|s| {
let (mut list1, sep1, bra1) = get_list(s.get(name!(list1))?);
let (mut list2, sep2, _bra2) = get_list(s.get(name!(list2))?);
let sep = s
.get_map(name!(separator), check_separator)?
.or(sep1)
.or(sep2)
.unwrap_or(ListSeparator::Space);
list1.append(&mut list2);
let bra = match s.get(name!(bracketed))? {
Value::Literal(ref s) if s.value() == "auto" => bra1,
b => b.is_true(),
};
Ok(Value::List(list1, Some(sep), bra))
}
);
def!(f, length(list), |s| match s.get(name!(list))? {
Value::ArgList(args) => Ok(Value::scalar(args.len() as i64)),
Value::List(v, _, _) => Ok(Value::scalar(v.len() as i64)),
Value::Map(m) => Ok(Value::scalar(m.len() as i64)),
Value::Null => Ok(Value::scalar(0)),
_ => Ok(Value::scalar(1)),
});
def!(f, separator(list), |s| {
let sep = match s.get(name!(list))? {
Value::List(_, Some(ListSeparator::Comma), _)
| Value::ArgList(..) => "comma",
Value::List(_, Some(ListSeparator::Slash), _) => "slash",
Value::Map(ref map) if !map.is_empty() => "comma",
_ => "space",
};
Ok(sep.into())
});
def_va!(f, slash(elements), |s| {
let list = s.get_va(name!(elements))?;
if list.len() < 2 {
return Err(CallError::msg(
"At least two elements are required.",
));
}
Ok(Value::List(list, Some(ListSeparator::Slash), false))
});
def!(f, nth(list, n), |s| {
match s.get(name!(list))? {
Value::ArgList(arg) => {
let n = s.get_map(name!(n), |v| index_of(v, arg.len()))?;
Ok(arg.positional.get(n).cloned().unwrap_or_else(|| {
arg.named.get_item(n - arg.positional.len()).map_or(
Value::Null,
|(k, v)| {
Value::List(
vec![Value::from(k.as_ref()), v.clone()],
Some(ListSeparator::Space),
false,
)
},
)
}))
}
Value::List(list, _, _) => {
let n = s.get_map(name!(n), |v| index_of(v, list.len()))?;
Ok(list[n].clone())
}
Value::Map(map) => {
let n = s.get_map(name!(n), |v| index_of(v, map.len()))?;
if let Some((k, v)) = map.get_item(n) {
Ok(Value::List(
vec![k.clone(), v.clone()],
Some(ListSeparator::Space),
false,
))
} else {
Ok(Value::Null)
}
}
v => s.get_map(name!(n), |v| index_of(v, 1)).map(|_| v),
}
});
def!(f, set_nth(list, n, value), |s| {
let (mut list, sep, bra) = get_list(s.get(name!(list))?);
let i = s.get_map(name!(n), |v| index_of(v, list.len()))?;
list[i] = s.get(name!(value))?;
Ok(Value::List(list, sep, bra))
});
def_va!(f, zip(lists), |s| {
let lists = s
.get_va(name!(lists))?
.into_iter()
.map(Value::iter_items)
.collect::<Vec<_>>();
let len = lists.iter().map(Vec::len).min().unwrap_or(0);
let result = (0..len)
.map(|i| {
let items = lists.iter().map(|v| v[i].clone()).collect();
Value::List(items, Some(ListSeparator::Space), false)
})
.collect();
Ok(Value::List(result, Some(ListSeparator::Comma), false))
});
f
}
pub fn expose(m: &Scope, global: &mut FunctionMap) {
for (gname, lname) in &[
(name!(append), name!(append)),
(name!(index), name!(index)),
(name!(is_bracketed), name!(is_bracketed)),
(name!(join), name!(join)),
(name!(length), name!(length)),
(name!(list_separator), name!(separator)),
(name!(nth), name!(nth)),
(name!(set_nth), name!(set_nth)),
(name!(zip), name!(zip)),
] {
global.insert(gname.clone(), m.get_lfunction(lname));
}
}
fn check_separator(v: Value) -> Result<Option<ListSeparator>, String> {
match String::try_from(v)?.as_ref() {
"comma" => Ok(Some(ListSeparator::Comma)),
"slash" => Ok(Some(ListSeparator::Slash)),
"space" => Ok(Some(ListSeparator::Space)),
"auto" => Ok(None),
_ => {
Err("Must be \"space\", \"comma\", \"slash\", or \"auto\"."
.into())
}
}
}
fn get_list(value: Value) -> (Vec<Value>, Option<ListSeparator>, bool) {
match value {
Value::ArgList(args) => {
let mut vec = args.positional;
vec.extend(args.named.into_iter().map(|(k, v)| {
Value::List(
vec![Value::from(k.as_ref()), v],
Some(ListSeparator::Space),
false,
)
}));
(vec, Some(ListSeparator::Comma), false)
}
Value::List(v, s, bra) => (v, s, bra),
Value::Map(map) => {
if map.is_empty() {
(vec![], None, false)
} else {
(
map.into_iter()
.map(|(k, v)| {
Value::List(
vec![k, v],
Some(ListSeparator::Space),
false,
)
})
.collect(),
Some(ListSeparator::Comma),
false,
)
}
}
v => (vec![v], None, false),
}
}
fn index_of(v: Value, len: usize) -> Result<usize, String> {
let n = check::unitless_int(v)?;
if n.is_positive() && n as usize <= len {
Ok((n - 1) as usize)
} else if n.is_negative() && n >= -(len as i64) {
Ok((len as i64 + n) as usize)
} else if n == 0 {
Err("List index may not be 0.".into())
} else {
Err(format!("Invalid index {n} for a list with {len} elements."))
}
}
#[cfg(test)]
mod test {
#[test]
fn append_a() {
check_val("append(10px 20px, 30px);", "10px 20px 30px")
}
#[test]
fn append_b() {
check_val("append((blue, red), green);", "blue, red, green")
}
#[test]
fn append_c() {
check_val("append(10px 20px, 30px 40px);", "10px 20px 30px 40px")
}
#[test]
fn append_d() {
check_val("append(10px, 20px, comma);", "10px, 20px")
}
#[test]
fn append_e() {
check_val("append((blue, red), green, space);", "blue red green")
}
mod join {
use super::check_val;
#[test]
fn both_bracketed() {
check_val("join([foo bar], [baz bang]);", "[foo bar baz bang]")
}
#[test]
fn first_bracketed() {
check_val("join([foo bar], baz bang);", "[foo bar baz bang]")
}
#[test]
fn second_bracketed() {
check_val("join(foo bar, [baz bang]);", "foo bar baz bang")
}
#[test]
fn bracketed_true() {
check_val("join(foo, bar, $bracketed: true);", "[foo bar]")
}
#[test]
fn bracketed_false() {
check_val("join([foo], [bar], $bracketed: false);", "foo bar")
}
#[test]
fn separator_and_bracketed() {
check_val(
"join(foo, bar, $separator: comma, $bracketed: true);",
"[foo, bar]",
)
}
#[test]
fn bracketed_and_separator() {
check_val(
"join(foo, bar, $bracketed: true, $separator: comma);",
"[foo, bar]",
)
}
#[test]
fn separator_and_bracketed_positional() {
check_val("join(foo, bar, comma, true);", "[foo, bar]")
}
#[test]
fn unusual_bracketed_type() {
check_val("join(foo, bar, $bracketed: foo);", "[foo bar]")
}
#[test]
fn bracketed_null() {
check_val("join([foo], [bar], $bracketed: null);", "foo bar")
}
}
#[test]
fn is_bracketed() {
check_val("is_bracketed([foo]);", "true");
}
#[test]
fn zip() {
check_val(
"zip(1px 1px 3px, solid dashed solid, red green blue);",
"1px solid red, 1px dashed green, 3px solid blue",
)
}
fn check_val(src: &str, correct: &str) {
use crate::variablescope::test::do_evaluate;
assert_eq!(do_evaluate(&[], src.as_bytes()), correct)
}
}