diff --git a/src/common.rs b/src/common.rs index d723c66..03474e4 100644 --- a/src/common.rs +++ b/src/common.rs @@ -13,6 +13,9 @@ pub enum Op { Mul, Div, Rem, + And, + Or, + Not, } impl Display for Op { @@ -29,6 +32,9 @@ impl Display for Op { Self::Mul => write!(f, "*"), Self::Div => write!(f, "/"), Self::Rem => write!(f, "%"), + Self::And => write!(f, "and"), + Self::Or => write!(f, "or"), + Self::Not => write!(f, "not"), } } } @@ -42,14 +48,15 @@ impl Op { /// If precedence is equal, the leftmost operation is evaluated first pub fn precedence(&self) -> usize { match self { + Self::And | Self::Or | Self::Not => 0, Self::Equal | Self::NotEqual | Self::GreaterThan | Self::GreaterThanEqual | Self::LessThan - | Self::LessThanEqual => 0, - Self::Plus | Self::Minus => 1, - Self::Mul | Self::Div | Self::Rem => 2, + | Self::LessThanEqual => 1, + Self::Plus | Self::Minus => 2, + Self::Mul | Self::Div | Self::Rem => 3, } } } diff --git a/src/css.rs b/src/css.rs index d6a03d4..9a1bb0a 100644 --- a/src/css.rs +++ b/src/css.rs @@ -36,13 +36,15 @@ impl Toplevel { Toplevel::RuleSet(selector, Vec::new()) } - fn push_style(&mut self, s: Style) { + fn push_style(&mut self, mut s: Style) -> SassResult<()> { + s.value = s.value.eval()?; if s.value.is_null() { - return; + return Ok(()); } if let Toplevel::RuleSet(_, entries) = self { entries.push(BlockEntry::Style(Box::new(s))); } + Ok(()) } fn push_comment(&mut self, s: String) { @@ -62,12 +64,12 @@ impl Css { Css { blocks: Vec::new() } } - pub fn from_stylesheet(s: StyleSheet) -> Self { + pub fn from_stylesheet(s: StyleSheet) -> SassResult { Css::new().parse_stylesheet(s) } - fn parse_stmt(&mut self, stmt: Stmt) -> Vec { - match stmt { + fn parse_stmt(&mut self, stmt: Stmt) -> SassResult> { + Ok(match stmt { Stmt::RuleSet(RuleSet { selector, super_selector, @@ -75,16 +77,16 @@ impl Css { }) => { let selector = super_selector.zip(&selector).remove_placeholders(); if selector.is_empty() { - return Vec::new(); + return Ok(Vec::new()); } let mut vals = vec![Toplevel::new_rule(selector)]; for rule in rules { match rule { - Stmt::RuleSet(_) => vals.extend(self.parse_stmt(rule)), + Stmt::RuleSet(_) => vals.extend(self.parse_stmt(rule)?), Stmt::Style(s) => vals .get_mut(0) .expect("expected block to exist") - .push_style(*s), + .push_style(*s)?, Stmt::MultilineComment(s) => vals .get_mut(0) .expect("expected block to exist") @@ -97,13 +99,13 @@ impl Css { Stmt::MultilineComment(s) => vec![Toplevel::MultilineComment(s)], Stmt::Style(_) => panic!("expected toplevel element, found style"), Stmt::AtRule(r) => vec![Toplevel::AtRule(r)], - } + }) } - fn parse_stylesheet(mut self, s: StyleSheet) -> Css { + fn parse_stylesheet(mut self, s: StyleSheet) -> SassResult { let mut is_first = true; for stmt in s.0 { - let v = self.parse_stmt(stmt); + let v = self.parse_stmt(stmt)?; // this is how we print newlines between unrelated styles // it could probably be refactored if !v.is_empty() { @@ -116,7 +118,7 @@ impl Css { self.blocks.extend(v); } } - self + Ok(self) } pub fn pretty_print(self, buf: &mut W, nesting: usize) -> SassResult<()> { @@ -149,7 +151,7 @@ impl Css { } else { writeln!(buf, "{}@{} {} {{", padding, u.name, u.params)?; } - Css::from_stylesheet(StyleSheet::from_stmts(u.body)) + Css::from_stylesheet(StyleSheet::from_stmts(u.body))? .pretty_print(buf, nesting + 1)?; writeln!(buf, "{}}}", padding)?; } diff --git a/src/lib.rs b/src/lib.rs index cb66777..d153ccd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -264,7 +264,7 @@ impl StyleSheet { /// ``` #[inline] pub fn print_as_css(self, buf: &mut W) -> SassResult<()> { - Css::from_stylesheet(self).pretty_print(buf, 0) + Css::from_stylesheet(self)?.pretty_print(buf, 0) } } diff --git a/src/value/mod.rs b/src/value/mod.rs index 8a04ece..2762d52 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -215,11 +215,23 @@ impl Value { Ordering::Less | Ordering::Equal => Ok(Self::True), Ordering::Greater => Ok(Self::False), }, + Op::Not => unreachable!(), + Op::And => Ok(if lhs.is_true()? { + rhs.eval()? + } else { + lhs.eval()? + }), + Op::Or => Ok(if lhs.is_true()? { + lhs.eval()? + } else { + rhs.eval()? + }), }, Self::Paren(v) => v.eval(), Self::UnaryOp(op, val) => match op { Op::Plus => val.unary_op_plus(), Op::Minus => -*val, + Op::Not => Ok(Self::bool(!val.eval()?.is_true()?)), _ => unreachable!(), }, _ => Ok(self), diff --git a/src/value/parse.rs b/src/value/parse.rs index acda46f..3f41453 100644 --- a/src/value/parse.rs +++ b/src/value/parse.rs @@ -144,6 +144,11 @@ fn eat_op>( space_separated: &mut Vec, ) -> SassResult<()> { match op { + Op::Not => { + devour_whitespace(iter); + let right = single_value(iter, scope, super_selector)?; + space_separated.push(Value::UnaryOp(op, Box::new(right))); + } Op::Plus => { if let Some(left) = space_separated.pop() { devour_whitespace(iter); @@ -168,6 +173,18 @@ fn eat_op>( space_separated.push(Value::UnaryOp(op, Box::new(right))); } } + Op::And | Op::Or => { + devour_whitespace(iter); + if iter.peek().is_none() { + space_separated.push(Value::Ident(op.to_string(), QuoteKind::None)); + } else if let Some(left) = space_separated.pop() { + devour_whitespace(iter); + let right = single_value(iter, scope, super_selector)?; + space_separated.push(Value::BinaryOp(Box::new(left), op, Box::new(right))); + } else { + return Err("Expected expression.".into()); + } + } _ => { if let Some(left) = space_separated.pop() { devour_whitespace(iter); @@ -186,13 +203,17 @@ fn single_value>( scope: &Scope, super_selector: &Selector, ) -> SassResult { - Ok(match iter.next().unwrap() { + Ok(match iter.next().ok_or("Expected expression.")? { IntermediateValue::Value(v) => v, IntermediateValue::Op(op) => match op { Op::Minus => { devour_whitespace(iter); (-single_value(iter, scope, super_selector)?)? } + Op::Not => { + devour_whitespace(iter); + Value::UnaryOp(op, Box::new(single_value(iter, scope, super_selector)?)) + } _ => todo!(), }, IntermediateValue::Whitespace => unreachable!(), @@ -484,9 +505,9 @@ impl Value { "true" => Ok(IntermediateValue::Value(Value::True)), "false" => Ok(IntermediateValue::Value(Value::False)), "null" => Ok(IntermediateValue::Value(Value::Null)), - // "not" => Ok(IntermediateValue::Op(Op::Not)), - // "and" => Ok(IntermediateValue::Op(Op::And)), - // "or" => Ok(IntermediateValue::Op(Op::Or)), + "not" => Ok(IntermediateValue::Op(Op::Not)), + "and" => Ok(IntermediateValue::Op(Op::And)), + "or" => Ok(IntermediateValue::Op(Op::Or)), _ => Ok(IntermediateValue::Value(Value::Ident(s, QuoteKind::None))), } } diff --git a/tests/and.rs b/tests/and.rs new file mode 100644 index 0000000..36d89f2 --- /dev/null +++ b/tests/and.rs @@ -0,0 +1,42 @@ +#![cfg(test)] + +#[macro_use] +mod macros; + +test!( + one_and_two, + "a {\n color: 1 and 2;\n}\n", + "a {\n color: 2;\n}\n" +); +test!( + two_and_one, + "a {\n color: 2 and 1;\n}\n", + "a {\n color: 1;\n}\n" +); +test!( + true_and_true, + "a {\n color: true and true;\n}\n", + "a {\n color: true;\n}\n" +); +test!( + true_and_false, + "a {\n color: true and false;\n}\n", + "a {\n color: false;\n}\n" +); +test!( + false_and_true, + "a {\n color: false and true;\n}\n", + "a {\n color: false;\n}\n" +); +test!( + false_and_false, + "a {\n color: false and false;\n}\n", + "a {\n color: false;\n}\n" +); +test!(null_and_one, "a {\n color: null and 1;\n}\n", ""); +test!(one_and_null, "a {\n color: 1 and null;\n}\n", ""); +test!( + one_and_two_and_three, + "a {\n color: 1 and 2 and 3;\n}\n", + "a {\n color: 3;\n}\n" +); diff --git a/tests/not.rs b/tests/not.rs new file mode 100644 index 0000000..858c31b --- /dev/null +++ b/tests/not.rs @@ -0,0 +1,40 @@ +#![cfg(test)] + +#[macro_use] +mod macros; + +test!( + not_number, + "a {\n color: not 1;\n}\n", + "a {\n color: false;\n}\n" +); +test!( + not_true, + "a {\n color: not true;\n}\n", + "a {\n color: false;\n}\n" +); +test!( + not_false, + "a {\n color: not false;\n}\n", + "a {\n color: true;\n}\n" +); +test!( + not_null, + "a {\n color: not null;\n}\n", + "a {\n color: true;\n}\n" +); +test!( + not_unquoted, + "a {\n color: not foo;\n}\n", + "a {\n color: false;\n}\n" +); +test!( + not_not_true, + "a {\n color: not not true;\n}\n", + "a {\n color: true;\n}\n" +); +test!( + not_not_false, + "a {\n color: not not false;\n}\n", + "a {\n color: false;\n}\n" +); diff --git a/tests/or.rs b/tests/or.rs new file mode 100644 index 0000000..f0ade8e --- /dev/null +++ b/tests/or.rs @@ -0,0 +1,50 @@ +#![cfg(test)] + +#[macro_use] +mod macros; + +test!( + one_or_two, + "a {\n color: 1 or 2;\n}\n", + "a {\n color: 1;\n}\n" +); +test!( + two_or_one, + "a {\n color: 2 or 1;\n}\n", + "a {\n color: 2;\n}\n" +); +test!( + true_or_true, + "a {\n color: true or true;\n}\n", + "a {\n color: true;\n}\n" +); +test!( + true_or_false, + "a {\n color: true or false;\n}\n", + "a {\n color: true;\n}\n" +); +test!( + false_or_true, + "a {\n color: false or true;\n}\n", + "a {\n color: true;\n}\n" +); +test!( + false_or_false, + "a {\n color: false or false;\n}\n", + "a {\n color: false;\n}\n" +); +test!( + null_or_one, + "a {\n color: null or 1;\n}\n", + "a {\n color: 1;\n}\n" +); +test!( + one_or_null, + "a {\n color: 1 or null;\n}\n", + "a {\n color: 1;\n}\n" +); +test!( + one_or_two_or_three, + "a {\n color: 1 or 2 or 3;\n}\n", + "a {\n color: 1;\n}\n" +); diff --git a/tests/values.rs b/tests/values.rs index 3a53027..426dc53 100644 --- a/tests/values.rs +++ b/tests/values.rs @@ -24,7 +24,10 @@ test!( test!(preserves_keyword_auto, "a {\n color: auto;\n}\n"); test!(preserves_keyword_initial, "a {\n color: initial;\n}\n"); test!(preserves_keyword_infinity, "a {\n color: infinity;\n}\n"); -test!(preserves_keyword_not, "a {\n color: not;\n}\n"); +error!( + keyword_not_expects_expression, + "a {\n color: not;\n}\n", "Error: Expected expression." +); test!(preserves_keyword_and, "a {\n color: and;\n}\n"); test!(preserves_keyword_or, "a {\n color: or;\n}\n"); test!(preserves_keyword_unset, "a {\n color: unset;\n}\n");