From eb478b632db9e62e483f2964f1d031620c089c4f Mon Sep 17 00:00:00 2001 From: ConnorSkees <39542938+ConnorSkees@users.noreply.github.com> Date: Mon, 30 Mar 2020 15:43:15 -0400 Subject: [PATCH] initial implementation of maps --- src/builtin/map.rs | 77 +++++++++++++++++++++++++++++++++++++++++++++ src/builtin/mod.rs | 1 + src/lib.rs | 2 +- src/value/map.rs | 53 +++++++++++++++++++++++++++++++ src/value/mod.rs | 41 ++++++++++++++++++------ src/value/ops.rs | 2 ++ src/value/parse.rs | 63 +++++++++++++++++++++++++++++++------ tests/map.rs | 78 ++++++++++++++++++++++++++++++++++++++++++++++ tests/values.rs | 14 +++------ 9 files changed, 300 insertions(+), 31 deletions(-) create mode 100644 src/value/map.rs create mode 100644 tests/map.rs diff --git a/src/builtin/map.rs b/src/builtin/map.rs index 8b13789..5832ec2 100644 --- a/src/builtin/map.rs +++ b/src/builtin/map.rs @@ -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) { + 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)) + }), + ); +} diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index 7eaee7d..570c16f 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -23,6 +23,7 @@ pub(crate) static GLOBAL_FUNCTIONS: Lazy> = Lazy::new(| let mut m = HashMap::new(); color::register(&mut m); list::register(&mut m); + map::register(&mut m); math::register(&mut m); meta::register(&mut m); string::register(&mut m); diff --git a/src/lib.rs b/src/lib.rs index f9099ff..fc4257c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,7 +60,7 @@ clippy::module_name_repetitions, // this is too pedantic -- it is sometimes useful to break up `impl`s clippy::multiple_inherent_impl, - + // temporarily allowed while under heavy development. // eventually these allows should be refactored away // to no longer be necessary diff --git a/src/value/map.rs b/src/value/map.rs new file mode 100644 index 0000000..c98c0b7 --- /dev/null +++ b/src/value/map.rs @@ -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> { + 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 { + self.0.into_iter().map(|(k, ..)| k).collect() + } + + pub fn values(self) -> Vec { + 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)); + } +} diff --git a/src/value/mod.rs b/src/value/mod.rs index 8ac8fd5..e31b513 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -1,18 +1,21 @@ +use std::cmp::Ordering; use std::fmt::{self, Display, Write}; use std::iter::Iterator; -use std::cmp::Ordering; use crate::color::Color; use crate::common::{Brackets, ListSeparator, Op, QuoteKind}; use crate::error::SassResult; use crate::unit::{Unit, UNIT_CONVERSION_TABLE}; + +pub(crate) use map::SassMap; pub(crate) use number::Number; +mod map; mod number; mod ops; mod parse; -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum Value { Important, True, @@ -25,8 +28,9 @@ pub(crate) enum Value { BinaryOp(Box, Op, Box), Paren(Box), Ident(String, QuoteKind), + Map(SassMap), // Returned by `get-function()` - // Function(String), + // Function(String) } impl Display for Value { @@ -40,6 +44,14 @@ impl Display for Value { } _ => write!(f, "{}{}", num, unit), }, + Self::Map(map) => write!( + f, + "({})", + map.iter() + .map(|(k, v)| format!("{}: {}", k, v)) + .collect::>() + .join(", ") + ), Self::List(vals, sep, brackets) => match brackets { Brackets::None => write!( f, @@ -145,6 +157,7 @@ impl Value { // Self::Function(..) => Ok("function"), Self::True | Self::False => Ok("bool"), Self::Null => Ok("null"), + Self::Map(..) => Ok("map"), Self::BinaryOp(..) | Self::Paren(..) | Self::UnaryOp(..) => self.clone().eval()?.kind(), } } @@ -185,16 +198,16 @@ impl Value { Op::Div => *lhs / *rhs, Op::Rem => *lhs % *rhs, Op::GreaterThan => match lhs.cmp(&rhs, op)? { - Ordering::Greater => Ok(Self::True), - Ordering::Less | Ordering::Equal=> Ok(Self::False), + Ordering::Greater => Ok(Self::True), + Ordering::Less | Ordering::Equal => Ok(Self::False), }, Op::GreaterThanEqual => match lhs.cmp(&rhs, op)? { Ordering::Greater | Ordering::Equal => Ok(Self::True), Ordering::Less => Ok(Self::False), }, Op::LessThan => match lhs.cmp(&rhs, op)? { - Ordering::Less => Ok(Self::True), - Ordering::Greater | Ordering::Equal=> Ok(Self::False), + Ordering::Less => Ok(Self::True), + Ordering::Greater | Ordering::Equal => Ok(Self::False), }, Op::LessThanEqual => match lhs.cmp(&rhs, op)? { Ordering::Less | Ordering::Equal => Ok(Self::True), @@ -225,12 +238,20 @@ impl Value { } else if unit2 == &Unit::None { num.cmp(num2) } 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()), }) } } diff --git a/src/value/ops.rs b/src/value/ops.rs index 06c85eb..52b75ac 100644 --- a/src/value/ops.rs +++ b/src/value/ops.rs @@ -11,6 +11,7 @@ impl Add for Value { fn add(self, mut other: Self) -> Self::Output { other = other.eval()?; Ok(match self { + Self::Map(..) => todo!(), Self::Important | Self::True | Self::False => match other { Self::Ident(s, QuoteKind::Double) | Self::Ident(s, QuoteKind::Single) => { 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::List(..) => Value::Ident(format!("{}{}", s1, other), quotes1), Self::UnaryOp(..) | Self::BinaryOp(..) | Self::Paren(..) => todo!(), + Self::Map(..) => todo!(), }, Self::List(..) => match other { Self::Ident(s, q) => Value::Ident(format!("{}{}", self, s), q.normalize()), diff --git a/src/value/parse.rs b/src/value/parse.rs index 86ee9f0..cd17122 100644 --- a/src/value/parse.rs +++ b/src/value/parse.rs @@ -20,6 +20,7 @@ use crate::utils::{ use crate::value::Value; use crate::Token; +use super::map::SassMap; use super::number::Number; fn parse_hex>( @@ -127,7 +128,7 @@ impl Value { None => return Ok(left), }; match next.kind { - ';' | ')' | ']' => Ok(left), + ';' | ')' | ']' | ':' => Ok(left), ',' => { toks.next(); devour_whitespace(toks); @@ -195,16 +196,16 @@ impl Value { match q { '>' => Op::GreaterThanEqual, '<' => Op::LessThanEqual, - _ => unreachable!() + _ => unreachable!(), } } else { match q { '>' => Op::GreaterThan, '<' => Op::LessThan, - _ => unreachable!() + _ => unreachable!(), } }; - devour_whitespace(toks); + devour_whitespace(toks); let right = Self::from_tokens(toks, scope, super_selector)?; Ok(Value::BinaryOp(Box::new(left), op, Box::new(right))) } @@ -359,20 +360,62 @@ impl Value { '(' => { toks.next(); devour_whitespace(toks); - if toks.peek().unwrap().kind == ')' { + if toks.peek().ok_or("expected \")\".")?.kind == ')' { toks.next(); + devour_whitespace(toks); return Ok(Value::List( Vec::new(), ListSeparator::Space, Brackets::None, )); } - let val = Self::from_tokens(toks, scope, super_selector)?; - let next = toks.next(); - if next.is_none() || next.unwrap().kind != ')' { - return Err("expected \")\".".into()); + let mut map = SassMap::new(); + let mut key = Self::from_tokens(toks, scope, super_selector)?; + match toks.next().ok_or("expected \")\".")?.kind { + ')' => 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!() + } + 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::Paren(Box::new(val))) + Ok(Value::Map(map)) } '&' => { toks.next(); diff --git a/tests/map.rs b/tests/map.rs new file mode 100644 index 0000000..8f909e5 --- /dev/null +++ b/tests/map.rs @@ -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" +); diff --git a/tests/values.rs b/tests/values.rs index 7dc1ba1..3a53027 100644 --- a/tests/values.rs +++ b/tests/values.rs @@ -52,16 +52,6 @@ test!( "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!( undefined_function_call_is_ident, "a {\n color: foo();\n}\n" @@ -122,3 +112,7 @@ test!( "a {\n color: -1 * 2;\n}\n", "a {\n color: -2;\n}\n" ); +error!( + value_missing_closing_paren, + "a {\n color: (red;\n}\n", "Error: expected \")\"." +);