initial implementation of maps

This commit is contained in:
ConnorSkees 2020-03-30 15:43:15 -04:00
parent 206c3f8179
commit eb478b632d
9 changed files with 300 additions and 31 deletions

View File

@ -1 +1,78 @@
use std::collections::HashMap;
use super::Builtin;
use crate::common::{Brackets, ListSeparator};
use crate::value::Value;
pub(crate) fn register(f: &mut HashMap<String, Builtin>) {
f.insert(
"map-get".to_owned(),
Box::new(|args, _| {
max_args!(args, 2);
let map = match arg!(args, 0, "map") {
Value::Map(m) => m,
v => return Err(format!("$map: {} is not a map.", v).into()),
};
let key = arg!(args, 1, "key");
Ok(map.get(key)?.unwrap_or(Value::Null).clone())
}),
);
f.insert(
"map-has-key".to_owned(),
Box::new(|args, _| {
max_args!(args, 2);
let map = match arg!(args, 0, "map") {
Value::Map(m) => m,
v => return Err(format!("$map: {} is not a map.", v).into()),
};
let key = arg!(args, 1, "key");
Ok(Value::bool(map.get(key)?.is_some()))
}),
);
f.insert(
"map-keys".to_owned(),
Box::new(|args, _| {
max_args!(args, 1);
let map = match arg!(args, 0, "map") {
Value::Map(m) => m,
v => return Err(format!("$map: {} is not a map.", v).into()),
};
Ok(Value::List(
map.keys(),
ListSeparator::Space,
Brackets::None,
))
}),
);
f.insert(
"map-values".to_owned(),
Box::new(|args, _| {
max_args!(args, 1);
let map = match arg!(args, 0, "map") {
Value::Map(m) => m,
v => return Err(format!("$map: {} is not a map.", v).into()),
};
Ok(Value::List(
map.values(),
ListSeparator::Space,
Brackets::None,
))
}),
);
f.insert(
"map-merge".to_owned(),
Box::new(|args, _| {
max_args!(args, 2);
let mut map1 = match arg!(args, 0, "map1") {
Value::Map(m) => m,
v => return Err(format!("$map1: {} is not a map.", v).into()),
};
let map2 = match arg!(args, 1, "map2") {
Value::Map(m) => m,
v => return Err(format!("$map2: {} is not a map.", v).into()),
};
map1.merge(map2);
Ok(Value::Map(map1))
}),
);
}

View File

@ -23,6 +23,7 @@ pub(crate) static GLOBAL_FUNCTIONS: Lazy<HashMap<String, Builtin>> = Lazy::new(|
let mut m = HashMap::new(); let mut m = HashMap::new();
color::register(&mut m); color::register(&mut m);
list::register(&mut m); list::register(&mut m);
map::register(&mut m);
math::register(&mut m); math::register(&mut m);
meta::register(&mut m); meta::register(&mut m);
string::register(&mut m); string::register(&mut m);

53
src/value/map.rs Normal file
View File

@ -0,0 +1,53 @@
use std::slice::Iter;
use super::Value;
use crate::error::SassResult;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SassMap(Vec<(Value, Value)>);
impl SassMap {
pub fn new() -> SassMap {
SassMap(Vec::new())
}
pub fn get(self, key: Value) -> SassResult<Option<Value>> {
for (k, v) in self.0 {
if k.equals(key.clone())? {
return Ok(Some(v));
}
}
Ok(None)
}
#[allow(dead_code)]
pub fn remove(&mut self, key: &Value) {
self.0.retain(|(ref k, ..)| k != key);
}
pub fn merge(&mut self, other: SassMap) {
self.0.extend(other.0);
}
pub fn iter(&self) -> Iter<(Value, Value)> {
self.0.iter()
}
pub fn keys(self) -> Vec<Value> {
self.0.into_iter().map(|(k, ..)| k).collect()
}
pub fn values(self) -> Vec<Value> {
self.0.into_iter().map(|(.., v)| v).collect()
}
pub fn insert(&mut self, key: Value, value: Value) {
for &mut (ref k, ref mut v) in &mut self.0 {
if k == &key {
*v = value;
return;
}
}
self.0.push((key, value));
}
}

View File

@ -1,18 +1,21 @@
use std::cmp::Ordering;
use std::fmt::{self, Display, Write}; use std::fmt::{self, Display, Write};
use std::iter::Iterator; use std::iter::Iterator;
use std::cmp::Ordering;
use crate::color::Color; use crate::color::Color;
use crate::common::{Brackets, ListSeparator, Op, QuoteKind}; use crate::common::{Brackets, ListSeparator, Op, QuoteKind};
use crate::error::SassResult; use crate::error::SassResult;
use crate::unit::{Unit, UNIT_CONVERSION_TABLE}; use crate::unit::{Unit, UNIT_CONVERSION_TABLE};
pub(crate) use map::SassMap;
pub(crate) use number::Number; pub(crate) use number::Number;
mod map;
mod number; mod number;
mod ops; mod ops;
mod parse; mod parse;
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Value { pub(crate) enum Value {
Important, Important,
True, True,
@ -25,8 +28,9 @@ pub(crate) enum Value {
BinaryOp(Box<Value>, Op, Box<Value>), BinaryOp(Box<Value>, Op, Box<Value>),
Paren(Box<Value>), Paren(Box<Value>),
Ident(String, QuoteKind), Ident(String, QuoteKind),
Map(SassMap),
// Returned by `get-function()` // Returned by `get-function()`
// Function(String), // Function(String)
} }
impl Display for Value { impl Display for Value {
@ -40,6 +44,14 @@ impl Display for Value {
} }
_ => write!(f, "{}{}", num, unit), _ => write!(f, "{}{}", num, unit),
}, },
Self::Map(map) => write!(
f,
"({})",
map.iter()
.map(|(k, v)| format!("{}: {}", k, v))
.collect::<Vec<String>>()
.join(", ")
),
Self::List(vals, sep, brackets) => match brackets { Self::List(vals, sep, brackets) => match brackets {
Brackets::None => write!( Brackets::None => write!(
f, f,
@ -145,6 +157,7 @@ impl Value {
// Self::Function(..) => Ok("function"), // Self::Function(..) => Ok("function"),
Self::True | Self::False => Ok("bool"), Self::True | Self::False => Ok("bool"),
Self::Null => Ok("null"), Self::Null => Ok("null"),
Self::Map(..) => Ok("map"),
Self::BinaryOp(..) | Self::Paren(..) | Self::UnaryOp(..) => self.clone().eval()?.kind(), Self::BinaryOp(..) | Self::Paren(..) | Self::UnaryOp(..) => self.clone().eval()?.kind(),
} }
} }
@ -225,12 +238,20 @@ impl Value {
} else if unit2 == &Unit::None { } else if unit2 == &Unit::None {
num.cmp(num2) num.cmp(num2)
} else { } else {
num.cmp(&(num2.clone() * UNIT_CONVERSION_TABLE[&unit.to_string()][&unit2.to_string()].clone())) num.cmp(
&(num2.clone()
* UNIT_CONVERSION_TABLE[&unit.to_string()][&unit2.to_string()]
.clone()),
)
} }
} }
_ => return Err(format!("Undefined operation \"{} {} {}\".", self, op, other).into()), _ => {
return Err(
format!("Undefined operation \"{} {} {}\".", self, op, other).into(),
)
}
}, },
_ => return Err(format!("Undefined operation \"{} {} {}\".", self, op, other).into()) _ => return Err(format!("Undefined operation \"{} {} {}\".", self, op, other).into()),
}) })
} }
} }

View File

@ -11,6 +11,7 @@ impl Add for Value {
fn add(self, mut other: Self) -> Self::Output { fn add(self, mut other: Self) -> Self::Output {
other = other.eval()?; other = other.eval()?;
Ok(match self { Ok(match self {
Self::Map(..) => todo!(),
Self::Important | Self::True | Self::False => match other { Self::Important | Self::True | Self::False => match other {
Self::Ident(s, QuoteKind::Double) | Self::Ident(s, QuoteKind::Single) => { Self::Ident(s, QuoteKind::Double) | Self::Ident(s, QuoteKind::Single) => {
Value::Ident(format!("{}{}", self, s), QuoteKind::Double) Value::Ident(format!("{}{}", self, s), QuoteKind::Double)
@ -78,6 +79,7 @@ impl Add for Value {
Self::Color(c) => Value::Ident(format!("{}{}", s1, c), quotes1.normalize()), Self::Color(c) => Value::Ident(format!("{}{}", s1, c), quotes1.normalize()),
Self::List(..) => Value::Ident(format!("{}{}", s1, other), quotes1), Self::List(..) => Value::Ident(format!("{}{}", s1, other), quotes1),
Self::UnaryOp(..) | Self::BinaryOp(..) | Self::Paren(..) => todo!(), Self::UnaryOp(..) | Self::BinaryOp(..) | Self::Paren(..) => todo!(),
Self::Map(..) => todo!(),
}, },
Self::List(..) => match other { Self::List(..) => match other {
Self::Ident(s, q) => Value::Ident(format!("{}{}", self, s), q.normalize()), Self::Ident(s, q) => Value::Ident(format!("{}{}", self, s), q.normalize()),

View File

@ -20,6 +20,7 @@ use crate::utils::{
use crate::value::Value; use crate::value::Value;
use crate::Token; use crate::Token;
use super::map::SassMap;
use super::number::Number; use super::number::Number;
fn parse_hex<I: Iterator<Item = Token>>( fn parse_hex<I: Iterator<Item = Token>>(
@ -127,7 +128,7 @@ impl Value {
None => return Ok(left), None => return Ok(left),
}; };
match next.kind { match next.kind {
';' | ')' | ']' => Ok(left), ';' | ')' | ']' | ':' => Ok(left),
',' => { ',' => {
toks.next(); toks.next();
devour_whitespace(toks); devour_whitespace(toks);
@ -195,13 +196,13 @@ impl Value {
match q { match q {
'>' => Op::GreaterThanEqual, '>' => Op::GreaterThanEqual,
'<' => Op::LessThanEqual, '<' => Op::LessThanEqual,
_ => unreachable!() _ => unreachable!(),
} }
} else { } else {
match q { match q {
'>' => Op::GreaterThan, '>' => Op::GreaterThan,
'<' => Op::LessThan, '<' => Op::LessThan,
_ => unreachable!() _ => unreachable!(),
} }
}; };
devour_whitespace(toks); devour_whitespace(toks);
@ -359,20 +360,62 @@ impl Value {
'(' => { '(' => {
toks.next(); toks.next();
devour_whitespace(toks); devour_whitespace(toks);
if toks.peek().unwrap().kind == ')' { if toks.peek().ok_or("expected \")\".")?.kind == ')' {
toks.next(); toks.next();
devour_whitespace(toks);
return Ok(Value::List( return Ok(Value::List(
Vec::new(), Vec::new(),
ListSeparator::Space, ListSeparator::Space,
Brackets::None, Brackets::None,
)); ));
} }
let val = Self::from_tokens(toks, scope, super_selector)?; let mut map = SassMap::new();
let next = toks.next(); let mut key = Self::from_tokens(toks, scope, super_selector)?;
if next.is_none() || next.unwrap().kind != ')' { match toks.next().ok_or("expected \")\".")?.kind {
return Err("expected \")\".".into()); ')' => return Ok(Value::Paren(Box::new(key))),
':' => {}
_ => unreachable!(),
};
loop {
devour_whitespace(toks);
match Self::from_tokens(toks, scope, super_selector)? {
Value::List(mut v, ListSeparator::Comma, Brackets::None) => {
devour_whitespace(toks);
match v.len() {
1 => {
map.insert(key, v.pop().unwrap());
if toks.peek().is_some() && toks.peek().unwrap().kind == ')' {
toks.next();
} else {
todo!()
} }
Ok(Value::Paren(Box::new(val))) break;
}
2 => {
let next_key = v.pop().unwrap();
map.insert(key, v.pop().unwrap());
key = next_key;
if toks.next().ok_or("expected \")\".")?.kind == ':' {
continue;
} else {
todo!()
}
}
_ => todo!(),
}
}
v => {
map.insert(key, v);
if toks.peek().is_some() && toks.peek().unwrap().kind == ')' {
toks.next();
break;
} else {
todo!()
}
}
}
}
Ok(Value::Map(map))
} }
'&' => { '&' => {
toks.next(); toks.next();

78
tests/map.rs Normal file
View File

@ -0,0 +1,78 @@
#![cfg(test)]
#[macro_use]
mod macros;
test!(
map_get_key_exists,
"a {\n color: map-get((a: b), a);\n}\n",
"a {\n color: b;\n}\n"
);
test!(
map_get_key_does_not_exist,
"a {\n color: map-get((a: b), foo);\n}\n",
""
);
error!(
map_get_non_map,
"a {\n color: map-get(foo, foo);\n}\n", "Error: $map: foo is not a map."
);
test!(
map_has_key_true,
"a {\n color: map-has-key((a: b), a);\n}\n",
"a {\n color: true;\n}\n"
);
test!(
map_has_key_false,
"a {\n color: map-has-key((a: b), foo);\n}\n",
"a {\n color: false;\n}\n"
);
error!(
map_has_key_non_map,
"a {\n color: map-has-key(foo, foo);\n}\n", "Error: $map: foo is not a map."
);
test!(
map_keys_one,
"a {\n color: map-keys((a: b));\n}\n",
"a {\n color: a;\n}\n"
);
error!(
map_keys_non_map,
"a {\n color: map-keys(foo);\n}\n", "Error: $map: foo is not a map."
);
test!(
map_values_one,
"a {\n color: map-values((a: b));\n}\n",
"a {\n color: b;\n}\n"
);
error!(
map_values_non_map,
"a {\n color: map-values(foo);\n}\n", "Error: $map: foo is not a map."
);
test!(
map_merge_one,
"a {\n color: inspect(map-merge((a: b), (c: d)));\n}\n",
"a {\n color: (a: b, c: d);\n}\n"
);
error!(
map_merge_map1_non_map,
"a {\n color: map-merge(foo, (a: b));\n}\n", "Error: $map1: foo is not a map."
);
error!(
map_merge_map2_non_map,
"a {\n color: map-merge((a: b), foo);\n}\n", "Error: $map2: foo is not a map."
);
test!(
map_dbl_quoted_key,
"a {\n color: map-get((\"a\": b), \"a\"));\n}\n",
"a {\n color: b;\n}\n"
);
test!(
map_key_quoting_ignored,
"a {\n color: map-get((\"a\": b), 'a'));\n}\n",
"a {\n color: b;\n}\n"
);
test!(
map_arbitrary_number_of_entries,
"a {\n color: inspect((a: b, c: d, e: f, g: h, i: j, h: k, l: m, n: o));\n}\n",
"a {\n color: (a: b, c: d, e: f, g: h, i: j, h: k, l: m, n: o);\n}\n"
);

View File

@ -52,16 +52,6 @@ test!(
"a {\n color: (foo);\n}\n", "a {\n color: (foo);\n}\n",
"a {\n color: foo;\n}\n" "a {\n color: foo;\n}\n"
); );
test!(
subs_dbl_quoted_ident_dimension,
"a {\n color: \"foo\" - 1px;\n}\n",
"a {\n color: \"foo\"-1px;\n}\n"
);
test!(
subs_sgl_quoted_ident_dimension,
"a {\n color: 'foo' - 1px;\n}\n",
"a {\n color: \"foo\"-1px;\n}\n"
);
test!( test!(
undefined_function_call_is_ident, undefined_function_call_is_ident,
"a {\n color: foo();\n}\n" "a {\n color: foo();\n}\n"
@ -122,3 +112,7 @@ test!(
"a {\n color: -1 * 2;\n}\n", "a {\n color: -1 * 2;\n}\n",
"a {\n color: -2;\n}\n" "a {\n color: -2;\n}\n"
); );
error!(
value_missing_closing_paren,
"a {\n color: (red;\n}\n", "Error: expected \")\"."
);