use crate::error::Error;
use crate::scanner::Pos;
use crate::value::{List, ListSep, Map, Number, Value};
pub(super) fn try_call(
name: &str,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
) -> Option<Result<Value, Error>> {
Some(match name {
"map-get" => fn_map_get(pos_args, named, pos),
"map-keys" => fn_map_keys(pos_args, named, pos),
"map-values" => fn_map_values(pos_args, named, pos),
"map-has-key" => fn_map_has_key(pos_args, named, pos),
"map-merge" => fn_map_merge(pos_args, named, pos),
"map-remove" => fn_map_remove(pos_args, named, pos),
"length" if first_is_map(pos_args, named) => fn_length(pos_args, named, pos),
"nth" if first_is_map(pos_args, named) => fn_nth(pos_args, named, pos),
_ => 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 {
"set" => fn_map_set(pos_args, named, pos),
"deep-merge" => fn_map_deep_merge(pos_args, named, pos),
"deep-remove" => fn_map_deep_remove(pos_args, named, pos),
_ => return None,
})
}
fn first_is_map(pos_args: &[Value], named: &[(String, Value)]) -> bool {
if let Some(v) = pos_args.first() {
return matches!(v, Value::Map(_));
}
named
.iter()
.any(|(n, v)| (n == "list" || n == "map") && matches!(v, Value::Map(_)))
}
fn as_map(v: &Value, fname: &str, pos: Pos) -> Result<Vec<(Value, Value)>, Error> {
match v {
Value::Map(m) => Ok(m.entries.clone()),
Value::List(l) if l.items.is_empty() => Ok(Vec::new()),
other => Err(Error::at(
format!("$map: {} is not a map for `{fname}`.", other.to_css(false)),
pos,
)),
}
}
fn unitless(value: f64) -> Value {
Value::Number(Number::unitless(value))
}
fn comma_list(items: Vec<Value>) -> Value {
Value::List(List {
items,
sep: ListSep::Comma,
bracketed: false,
keywords: None,
})
}
fn key_path<'v>(
pos_args: &'v [Value],
named: &'v [(String, Value)],
fname: &str,
pos: Pos,
) -> Result<Vec<&'v Value>, Error> {
let mut keys: Vec<&Value> = Vec::new();
if let Some(first) = pos_args.get(1) {
keys.push(first);
} else if let Some((_, v)) = named.iter().find(|(n, _)| n == "key") {
keys.push(v);
}
if keys.is_empty() {
return Err(Error::at(format!("Missing argument $key for {fname}()."), pos));
}
keys.extend(pos_args.iter().skip(2));
Ok(keys)
}
fn fn_map_get(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let map_v = super::require(&["map"], pos_args, named, 0, "map-get", pos)?;
let mut entries = as_map(map_v, "map-get", pos)?;
let keys = key_path(pos_args, named, "map-get", pos)?;
for (i, key) in keys.iter().enumerate() {
let found = entries
.iter()
.find(|(k, _)| k.sass_eq(key))
.map(|(_, v)| v.clone());
match found {
Some(v) => {
if i + 1 == keys.len() {
return Ok(v);
}
match &v {
Value::Map(m) => entries = m.entries.clone(),
Value::List(l) if l.items.is_empty() => entries = Vec::new(),
_ => return Ok(Value::Null),
}
}
None => return Ok(Value::Null),
}
}
Ok(Value::Null)
}
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 fn_map_keys(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
check_arity(pos_args, 1, pos)?;
let map_v = super::require(&["map"], pos_args, named, 0, "map-keys", pos)?;
let entries = as_map(map_v, "map-keys", pos)?;
Ok(comma_list(entries.into_iter().map(|(k, _)| k).collect()))
}
fn fn_map_values(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
check_arity(pos_args, 1, pos)?;
let map_v = super::require(&["map"], pos_args, named, 0, "map-values", pos)?;
let entries = as_map(map_v, "map-values", pos)?;
Ok(comma_list(entries.into_iter().map(|(_, v)| v).collect()))
}
fn fn_map_has_key(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let map_v = super::require(&["map"], pos_args, named, 0, "map-has-key", pos)?;
let mut entries = as_map(map_v, "map-has-key", pos)?;
let keys = key_path(pos_args, named, "map-has-key", pos)?;
for (i, key) in keys.iter().enumerate() {
let found = entries
.iter()
.find(|(k, _)| k.sass_eq(key))
.map(|(_, v)| v.clone());
match found {
Some(v) => {
if i + 1 == keys.len() {
return Ok(Value::Bool(true));
}
match &v {
Value::Map(m) => entries = m.entries.clone(),
Value::List(l) if l.items.is_empty() => entries = Vec::new(),
_ => return Ok(Value::Bool(false)),
}
}
None => return Ok(Value::Bool(false)),
}
}
Ok(Value::Bool(false))
}
fn shallow_merge(a: Vec<(Value, Value)>, b: Vec<(Value, Value)>) -> Map {
let mut map = Map { entries: a };
for (k, v) in b {
map.insert(k, v);
}
map
}
fn entries_of(v: &Value) -> Option<Vec<(Value, Value)>> {
match v {
Value::Map(m) => Some(m.entries.clone()),
Value::List(l) if l.items.is_empty() => Some(Vec::new()),
_ => None,
}
}
fn deep_merge(a: Vec<(Value, Value)>, b: Vec<(Value, Value)>) -> Map {
let mut map = Map { entries: a };
for (k, v) in b {
let merged = match (map.get(&k).and_then(entries_of), entries_of(&v)) {
(Some(existing), Some(incoming)) => Value::Map(deep_merge(existing, incoming)),
_ => v,
};
map.insert(k, merged);
}
map
}
fn modify_map(
entries: Vec<(Value, Value)>,
keys: &[&Value],
transform: &mut dyn FnMut(Option<Value>) -> Value,
) -> Vec<(Value, Value)> {
match keys.split_first() {
None => {
match transform(Some(Value::Map(Map { entries }))) {
Value::Map(m) => m.entries,
_ => Vec::new(),
}
}
Some((key, rest)) => {
let mut map = Map { entries };
let child = map.get(key).cloned();
let new_child = if rest.is_empty() {
transform(child)
} else {
let child_entries = match child {
Some(Value::Map(m)) => m.entries,
Some(Value::List(l)) if l.items.is_empty() => Vec::new(),
_ => Vec::new(),
};
Value::Map(Map {
entries: modify_map(child_entries, rest, transform),
})
};
map.insert((*key).clone(), new_child);
map.entries
}
}
}
fn fn_map_merge(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
merge_impl(pos_args, named, pos, "map-merge", false)
}
fn fn_map_deep_merge(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
merge_impl(pos_args, named, pos, "map-deep-merge", true)
}
fn merge_impl(
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
fname: &str,
deep: bool,
) -> Result<Value, Error> {
let merge = |a: Vec<(Value, Value)>, b: Vec<(Value, Value)>| {
if deep {
deep_merge(a, b)
} else {
shallow_merge(a, b)
}
};
let map1_v = super::require(&["map1"], pos_args, named, 0, fname, pos)?;
let map1 = as_map_named(map1_v, "map1", pos)?;
if pos_args.len() <= 1 {
let map2_v = named
.iter()
.find(|(n, _)| n == "map2")
.map(|(_, v)| v)
.ok_or_else(|| Error::at(format!("Missing argument $map2 for {fname}()."), pos))?;
let map2 = as_map_named(map2_v, "map2", pos)?;
return Ok(Value::Map(merge(map1, map2)));
}
let map2_v = &pos_args[pos_args.len() - 1];
let map2 = as_map_named(map2_v, "map2", pos)?;
let keys: Vec<&Value> = pos_args[1..pos_args.len() - 1].iter().collect();
if keys.is_empty() {
return Ok(Value::Map(merge(map1, map2)));
}
let mut transform = |child: Option<Value>| {
let child_entries = match child {
Some(Value::Map(m)) => m.entries,
Some(Value::List(l)) if l.items.is_empty() => Vec::new(),
_ => Vec::new(),
};
Value::Map(merge(child_entries, map2.clone()))
};
Ok(Value::Map(Map {
entries: modify_map(map1, &keys, &mut transform),
}))
}
fn fn_map_set(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let map_v = super::require(&["map"], pos_args, named, 0, "map-set", pos)?;
let entries = as_map_named(map_v, "map", pos)?;
let named_key = named.iter().find(|(n, _)| n == "key").map(|(_, v)| v);
let named_value = named.iter().find(|(n, _)| n == "value").map(|(_, v)| v);
let (keys, value): (Vec<&Value>, &Value) = match (named_key, named_value) {
(Some(k), Some(v)) => (vec![k], v),
_ => {
if pos_args.len() < 3 {
return Err(Error::at("Expected $args to contain a key.", pos));
}
let value = &pos_args[pos_args.len() - 1];
let keys = pos_args[1..pos_args.len() - 1].iter().collect();
(keys, value)
}
};
let mut transform = |_old: Option<Value>| value.clone();
Ok(Value::Map(Map {
entries: modify_map(entries, &keys, &mut transform),
}))
}
fn fn_map_remove(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let map_v = super::require(&["map"], pos_args, named, 0, "map-remove", pos)?;
let mut entries = as_map_named(map_v, "map", pos)?;
let mut keys: Vec<Value> = pos_args.iter().skip(1).cloned().collect();
if let Some((_, v)) = named.iter().find(|(n, _)| n == "key") {
if !keys.is_empty() {
return Err(Error::at(
"Argument $keys was passed both by position and by name.",
pos,
));
}
keys.push(v.clone());
}
entries.retain(|(k, _)| !keys.iter().any(|rk| rk.sass_eq(k)));
Ok(Value::Map(Map { entries }))
}
fn fn_map_deep_remove(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let map_v = super::require(&["map"], pos_args, named, 0, "map-deep-remove", pos)?;
let entries = as_map_named(map_v, "map", pos)?;
let keys = key_path(pos_args, named, "map-deep-remove", pos)?;
let (last, parents) = match keys.split_last() {
Some(pair) => pair,
None => return Ok(Value::Map(Map { entries })),
};
Ok(Value::Map(Map {
entries: deep_remove(entries, parents, last),
}))
}
fn deep_remove(entries: Vec<(Value, Value)>, parents: &[&Value], last: &Value) -> Vec<(Value, Value)> {
match parents.split_first() {
None => {
let mut entries = entries;
entries.retain(|(k, _)| !k.sass_eq(last));
entries
}
Some((key, rest)) => {
let mut map = Map { entries };
if let Some(Value::Map(child)) = map.get(key).cloned().as_ref() {
let new_child = deep_remove(child.entries.clone(), rest, last);
map.insert((*key).clone(), Value::Map(Map { entries: new_child }));
}
map.entries
}
}
}
fn as_map_named(v: &Value, param: &str, pos: Pos) -> Result<Vec<(Value, Value)>, Error> {
match v {
Value::Map(m) => Ok(m.entries.clone()),
Value::List(l) if l.items.is_empty() => Ok(Vec::new()),
other => Err(Error::at(
format!("${param}: {} is not a map.", other.to_css(false)),
pos,
)),
}
}
fn fn_length(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let map_v = super::require(&["map"], pos_args, named, 0, "length", pos)?;
let entries = as_map(map_v, "length", pos)?;
Ok(unitless(entries.len() as f64))
}
fn fn_nth(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = ["list", "n"];
let map_v = super::require(¶ms, pos_args, named, 0, "nth", pos)?;
let entries = as_map(map_v, "nth", pos)?;
let n = super::require(¶ms, pos_args, named, 1, "nth", pos)?;
let raw = super::num(n, pos)?;
if raw.fract() != 0.0 {
return Err(Error::at(
format!("$n: {} is not an int.", crate::value::fmt_num(raw, false)),
pos,
));
}
let len = entries.len() as i64;
let index = raw as i64;
if index == 0 {
return Err(Error::at("$n: List index may not be 0.", pos));
}
if index.abs() > len {
return Err(Error::at(
format!(
"$n: 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 + index } as usize;
let (k, v) = entries[zero_based].clone();
Ok(Value::List(List {
items: vec![k, v],
sep: ListSep::Space,
bracketed: false,
keywords: None,
}))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::value::SassStr;
fn pos() -> Pos {
Pos { line: 1, col: 1 }
}
fn s(text: &str) -> Value {
Value::Str(SassStr {
text: text.to_string(),
quoted: false,
})
}
fn n(value: f64) -> Value {
unitless(value)
}
fn map(pairs: &[(&str, Value)]) -> Value {
Value::Map(Map {
entries: pairs.iter().map(|(k, v)| (s(k), v.clone())).collect(),
})
}
fn call(name: &str, args: &[Value]) -> Value {
try_call(name, args, &[], pos())
.expect("name owned by map family")
.expect("no error")
}
#[test]
fn map_get_returns_value_or_null() {
let m = map(&[("c", s("d"))]);
assert_eq!(call("map-get", &[m.clone(), s("c")]).to_css(false), "d");
assert!(matches!(call("map-get", &[m, s("z")]), Value::Null));
}
#[test]
fn map_keys_and_values_are_comma_lists() {
let m = map(&[("c", n(1.0)), ("d", n(2.0))]);
assert_eq!(call("map-keys", std::slice::from_ref(&m)).to_css(false), "c, d");
assert_eq!(call("map-values", &[m]).to_css(false), "1, 2");
}
#[test]
fn map_has_key_reports_presence() {
let m = map(&[("c", s("d"))]);
assert_eq!(call("map-has-key", &[m.clone(), s("c")]).to_css(false), "true");
assert_eq!(call("map-has-key", &[m, s("z")]).to_css(false), "false");
}
#[test]
fn map_merge_overwrites_and_appends() {
let a = map(&[("c", s("d"))]);
let b = map(&[("e", s("f"))]);
assert_eq!(call("map-merge", &[a, b]).to_css(false), "(c: d, e: f)");
let a = map(&[("c", n(1.0))]);
let b = map(&[("c", n(2.0))]);
assert_eq!(call("map-merge", &[a, b]).to_css(false), "(c: 2)");
}
#[test]
fn map_remove_drops_keys() {
let m = map(&[("c", s("d")), ("x", s("y"))]);
assert_eq!(call("map-remove", &[m, s("c")]).to_css(false), "(x: y)");
}
#[test]
fn length_and_nth_on_maps() {
let m = map(&[("c", n(1.0)), ("d", n(2.0))]);
assert_eq!(call("length", std::slice::from_ref(&m)).to_css(false), "2");
assert_eq!(call("nth", &[m, n(1.0)]).to_css(false), "c 1");
}
#[test]
fn length_and_nth_decline_non_maps() {
assert!(try_call("length", &[s("x")], &[], pos()).is_none());
assert!(try_call("nth", &[s("x"), n(1.0)], &[], pos()).is_none());
}
#[test]
fn empty_list_acts_as_empty_map() {
let empty = Value::List(List {
items: Vec::new(),
sep: ListSep::Space,
bracketed: false,
keywords: None,
});
assert_eq!(call("map-keys", std::slice::from_ref(&empty)).to_css(false), "");
assert!(matches!(call("map-get", &[empty, s("a")]), Value::Null));
}
}