From f7d9d4432aa9eb71d964e5a6261059c50c387d20 Mon Sep 17 00:00:00 2001 From: ConnorSkees <39542938+ConnorSkees@users.noreply.github.com> Date: Tue, 31 Mar 2020 01:00:25 -0400 Subject: [PATCH] initial implementation of @each --- src/atrule/mod.rs | 80 +++++++++++++++++++++++++++++++++++++++++++++-- src/lib.rs | 13 ++++---- tests/each.rs | 36 +++++++++++++++++++++ 3 files changed, 120 insertions(+), 9 deletions(-) create mode 100644 tests/each.rs diff --git a/src/atrule/mod.rs b/src/atrule/mod.rs index 787c626..3c24e02 100644 --- a/src/atrule/mod.rs +++ b/src/atrule/mod.rs @@ -3,7 +3,7 @@ use std::iter::Peekable; use num_traits::cast::ToPrimitive; -use crate::common::Pos; +use crate::common::{Brackets, ListSeparator, Pos}; use crate::error::SassResult; use crate::scope::Scope; use crate::selector::Selector; @@ -39,6 +39,7 @@ pub(crate) enum AtRule { Content, Unknown(UnknownAtRule), For(Vec), + Each(Vec), If(If), } @@ -103,7 +104,82 @@ impl AtRule { devour_whitespace(toks); AtRule::Charset } - AtRuleKind::Each => todo!("@each not yet implemented"), + AtRuleKind::Each => { + let mut stmts = Vec::new(); + devour_whitespace(toks); + let mut vars = Vec::new(); + loop { + match toks.next().ok_or("expected \"$\".")?.kind { + '$' => vars.push(eat_ident(toks, scope, super_selector)?), + _ => return Err("expected \"$\".".into()), + } + devour_whitespace(toks); + if toks.peek().ok_or("expected \"$\".")?.kind == ',' { + toks.next(); + devour_whitespace(toks); + } else { + break; + } + } + if toks.peek().is_none() + || eat_ident(toks, scope, super_selector)?.to_ascii_lowercase() != "in" + { + return Err("Expected \"in\".".into()); + } + devour_whitespace(toks); + let iterator = match Value::from_tokens( + &mut read_until_open_curly_brace(toks).into_iter().peekable(), + scope, + super_selector, + )? { + Value::List(v, ..) => v, + Value::Map(m) => m + .into_iter() + .map(|(k, v)| Value::List(vec![k, v], ListSeparator::Space, Brackets::None)) + .collect(), + v => vec![v], + }; + toks.next(); + devour_whitespace(toks); + let mut body = read_until_closing_curly_brace(toks); + body.push(toks.next().unwrap()); + devour_whitespace(toks); + + for row in iterator { + let this_iterator = match row { + Value::List(v, ..) => v, + Value::Map(m) => m + .into_iter() + .map(|(k, v)| { + Value::List(vec![k, v], ListSeparator::Space, Brackets::None) + }) + .collect(), + v => vec![v], + }; + + if vars.len() == 1 { + scope.insert_var( + &vars[0], + Value::List(this_iterator, ListSeparator::Space, Brackets::None), + )?; + } else { + for (var, val) in vars.clone().into_iter().zip( + this_iterator + .into_iter() + .chain(std::iter::once(Value::Null).cycle()), + ) { + scope.insert_var(&var, val)?; + } + } + + stmts.extend(eat_stmts( + &mut body.clone().into_iter().peekable(), + scope, + super_selector, + )?); + } + AtRule::Each(stmts) + } AtRuleKind::Extend => todo!("@extend not yet implemented"), AtRuleKind::If => AtRule::If(If::from_tokens(toks)?), AtRuleKind::Else => todo!("@else not yet implemented"), diff --git a/src/lib.rs b/src/lib.rs index fc4257c..cb66777 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -382,7 +382,7 @@ impl<'a> StyleSheetParser<'a> { AtRule::Return(_) => { return Err("This at-rule is not allowed here.".into()) } - AtRule::For(s) => rules.extend(s), + AtRule::Each(s) | AtRule::For(s) => rules.extend(s), AtRule::Content => return Err("@content is only allowed within mixin declarations.".into()), AtRule::If(i) => { rules.extend(i.eval(&mut Scope::new(), &Selector::new())?); @@ -412,7 +412,7 @@ impl<'a> StyleSheetParser<'a> { match expr { Expr::Style(s) => stmts.push(Stmt::Style(s)), Expr::AtRule(a) => match a { - AtRule::For(s) => stmts.extend(s), + AtRule::Each(s) | AtRule::For(s) => stmts.extend(s), AtRule::If(i) => stmts.extend(i.eval(scope, super_selector)?), AtRule::Content => { return Err("@content is only allowed within mixin declarations.".into()) @@ -558,11 +558,9 @@ pub(crate) fn eat_expr>( continue; } else if values.is_empty() && peeked.kind == '*' { toks.next(); - return Ok(Some(Expr::MultilineComment(eat_comment( - toks, - scope, - super_selector, - )?))); + let comment = eat_comment(toks, scope, super_selector)?; + devour_whitespace(toks); + return Ok(Some(Expr::MultilineComment(comment))); } else { values.push(tok); } @@ -593,6 +591,7 @@ pub(crate) fn eat_expr>( c @ AtRule::Content => Ok(Some(Expr::AtRule(c))), f @ AtRule::If(..) => Ok(Some(Expr::AtRule(f))), f @ AtRule::For(..) => Ok(Some(Expr::AtRule(f))), + f @ AtRule::Each(..) => Ok(Some(Expr::AtRule(f))), u @ AtRule::Unknown(..) => Ok(Some(Expr::AtRule(u))), }; } diff --git a/tests/each.rs b/tests/each.rs new file mode 100644 index 0000000..030c68e --- /dev/null +++ b/tests/each.rs @@ -0,0 +1,36 @@ +#![cfg(test)] + +#[macro_use] +mod macros; + +test!( + each_space_separated_inner, + "a {\n @each $i in 1 2 3 {\n color: $i;\n }\n}\n", + "a {\n color: 1;\n color: 2;\n color: 3;\n}\n" +); +test!( + each_space_separated_outer, + "@each $i in 1 2 3 {\n a {\n color: $i;\n }\n}\n", + "a {\n color: 1;\n}\n\na {\n color: 2;\n}\n\na {\n color: 3;\n}\n" +); +test!( + each_two_variables_one_null, + "a {\n @each $i, $c in 1 2 3 {\n color: $i;\n }\n}\n", + "a {\n color: 1;\n color: 2;\n color: 3;\n}\n" +); +test!( + each_one_var_in_one_map, + "a {\n @each $i in (a: b) {\n color: $i;\n }\n}\n", + "a {\n color: a b;\n}\n" +); +test!( + each_two_vars_in_one_map, + "a {\n @each $i, $c in (a: b) {\n color: $i;\n }\n}\n", + "a {\n color: a;\n}\n" +); +// blocked on better parsing of comma and space separated lists +// test!( +// each_two_vars_in_3_2_list, +// "a {\n @each $i, $c in (1 2 3, 4 5) {\n color: $i, $c;\n }\n}\n", +// "a {\n color: 1, 2;\n color: 4, 5;\n}\n" +// );