From 827225a143b247be988d53f33f8b4360f8776847 Mon Sep 17 00:00:00 2001 From: Connor Skees Date: Sat, 4 Jul 2020 20:50:53 -0400 Subject: [PATCH] initial implementation of `@keyframes` --- CHANGELOG.md | 14 ++- src/atrule/keyframes.rs | 20 ++++ src/atrule/mod.rs | 1 + src/lib.rs | 3 + src/output.rs | 90 +++++++++++++++- src/parse/function.rs | 1 + src/parse/import.rs | 1 + src/parse/keyframes.rs | 223 +++++++++++++++++++++++++++++++++++++++ src/parse/mixin.rs | 2 + src/parse/mod.rs | 132 +++++++++++++++-------- src/parse/style.rs | 62 +++++------ src/parse/value/parse.rs | 1 + src/utils/number.rs | 5 +- src/value/mod.rs | 1 + tests/keyframes.rs | 111 +++++++++++++++++++ 15 files changed, 583 insertions(+), 84 deletions(-) create mode 100644 src/atrule/keyframes.rs create mode 100644 src/parse/keyframes.rs create mode 100644 tests/keyframes.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7055ab3..2204e0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,14 @@ +# 0.10.0 + +- implement `@keyframes` + # 0.9.3 - - fix parsing bugs for empty bracketed lists - - partially implement inverse units - - remove all remaining `todo!()`s from binary and unary ops - - parse keywords case sensitively - - various optimizations that make bulma about *6x faster* to compile +- fix parsing bugs for empty bracketed lists +- partially implement inverse units +- remove all remaining `todo!()`s from binary and unary ops +- parse keywords case sensitively +- various optimizations that make bulma about _6x faster_ to compile # 0.9.2 diff --git a/src/atrule/keyframes.rs b/src/atrule/keyframes.rs new file mode 100644 index 0000000..e48b56a --- /dev/null +++ b/src/atrule/keyframes.rs @@ -0,0 +1,20 @@ +use crate::parse::Stmt; + +#[derive(Debug, Clone)] +pub(crate) struct Keyframes { + pub name: String, + pub body: Vec, +} + +#[derive(Debug, Clone)] +pub(crate) struct KeyframesRuleSet { + pub selector: Vec, + pub body: Vec, +} + +#[derive(Debug, Clone)] +pub(crate) enum KeyframesSelector { + To, + From, + Percent(Box), +} diff --git a/src/atrule/mod.rs b/src/atrule/mod.rs index c84a063..7c0bda1 100644 --- a/src/atrule/mod.rs +++ b/src/atrule/mod.rs @@ -5,6 +5,7 @@ pub(crate) use supports::SupportsRule; pub(crate) use unknown::UnknownAtRule; mod function; +pub mod keyframes; mod kind; pub mod media; mod mixin; diff --git a/src/lib.rs b/src/lib.rs index 361eac9..828c187 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -155,6 +155,7 @@ pub fn from_path(p: &str) -> Result { at_root: true, at_root_has_selector: false, extender: &mut Extender::new(empty_span), + in_keyframes: false, } .parse() .map_err(|e| raw_to_parse_error(&map, *e))?; @@ -199,6 +200,7 @@ pub fn from_string(p: String) -> Result { at_root: true, at_root_has_selector: false, extender: &mut Extender::new(empty_span), + in_keyframes: false, } .parse() .map_err(|e| raw_to_parse_error(&map, *e))?; @@ -234,6 +236,7 @@ pub fn from_string(p: String) -> std::result::Result { at_root: true, at_root_has_selector: false, extender: &mut Extender::new(empty_span), + in_keyframes: false, } .parse() .map_err(|e| raw_to_parse_error(&map, *e).to_string())?; diff --git a/src/output.rs b/src/output.rs index 102de14..af861e7 100644 --- a/src/output.rs +++ b/src/output.rs @@ -4,7 +4,11 @@ use std::io::Write; use codemap::CodeMap; use crate::{ - atrule::{media::MediaRule, SupportsRule, UnknownAtRule}, + atrule::{ + keyframes::{Keyframes, KeyframesRuleSet, KeyframesSelector}, + media::MediaRule, + SupportsRule, UnknownAtRule, + }, error::SassResult, parse::Stmt, selector::Selector, @@ -23,6 +27,8 @@ enum Toplevel { RuleSet(Selector, Vec), MultilineComment(String), UnknownAtRule(Box), + Keyframes(Box), + KeyframesRuleSet(Vec, Vec), Media { query: String, body: Vec }, Supports { params: String, body: Vec }, Newline, @@ -49,18 +55,26 @@ impl Toplevel { Toplevel::RuleSet(selector, Vec::new()) } + fn new_keyframes_rule(selector: Vec) -> Self { + Toplevel::KeyframesRuleSet(selector, Vec::new()) + } + fn push_style(&mut self, s: Style) { if s.value.is_null() { return; } - if let Toplevel::RuleSet(_, entries) = self { + if let Toplevel::RuleSet(_, entries) | Toplevel::KeyframesRuleSet(_, entries) = self { entries.push(BlockEntry::Style(Box::new(s))); + } else { + panic!() } } fn push_comment(&mut self, s: String) { - if let Toplevel::RuleSet(_, entries) = self { + if let Toplevel::RuleSet(_, entries) | Toplevel::KeyframesRuleSet(_, entries) = self { entries.push(BlockEntry::MultilineComment(s)); + } else { + panic!() } } } @@ -120,6 +134,13 @@ impl Css { Ok(()) })? } + Stmt::Keyframes(k) => { + let Keyframes { name, body } = *k; + vals.push(Toplevel::Keyframes(Box::new(Keyframes { name, body }))) + } + k @ Stmt::KeyframesRuleSet(..) => { + unreachable!("@keyframes ruleset {:?}", k) + } }; } vals @@ -146,6 +167,22 @@ impl Css { } Stmt::Return(..) => unreachable!("@return: {:?}", stmt), Stmt::AtRoot { .. } => unreachable!("@at-root: {:?}", stmt), + Stmt::Keyframes(k) => vec![Toplevel::Keyframes(k)], + Stmt::KeyframesRuleSet(k) => { + let KeyframesRuleSet { body, selector } = *k; + if body.is_empty() { + return Ok(Vec::new()); + } + let mut vals = vec![Toplevel::new_keyframes_rule(selector)]; + for rule in body { + match rule { + Stmt::Style(s) => vals.get_mut(0).unwrap().push_style(s), + Stmt::KeyframesRuleSet(..) => vals.extend(self.parse_stmt(rule)?), + _ => todo!(), + } + } + vals + } }) } @@ -205,6 +242,30 @@ impl Css { } writeln!(buf, "{}}}", padding)?; } + Toplevel::KeyframesRuleSet(selector, body) => { + if body.is_empty() { + continue; + } + has_written = true; + if should_emit_newline { + should_emit_newline = false; + writeln!(buf)?; + } + writeln!( + buf, + "{}{} {{", + padding, + selector + .into_iter() + .map(|s| s.to_string()) + .collect::>() + .join(", ") + )?; + for style in body { + writeln!(buf, "{} {}", padding, style.to_string()?)?; + } + writeln!(buf, "{}}}", padding)?; + } Toplevel::MultilineComment(s) => { has_written = true; writeln!(buf, "{}/*{}*/", padding, s)?; @@ -232,6 +293,29 @@ impl Css { Css::from_stmts(body)?._inner_pretty_print(buf, map, nesting + 1)?; writeln!(buf, "{}}}", padding)?; } + Toplevel::Keyframes(k) => { + let Keyframes { name, body } = *k; + if should_emit_newline { + should_emit_newline = false; + writeln!(buf)?; + } + + write!(buf, "{}@keyframes", padding)?; + + if !name.is_empty() { + write!(buf, " {}", name)?; + } + + if body.is_empty() { + writeln!(buf, " {{}}")?; + continue; + } else { + writeln!(buf, " {{")?; + } + + Css::from_stmts(body)?._inner_pretty_print(buf, map, nesting + 1)?; + writeln!(buf, "{}}}", padding)?; + } Toplevel::Supports { params, body } => { if should_emit_newline { should_emit_newline = false; diff --git a/src/parse/function.rs b/src/parse/function.rs index 05e9852..01fdccd 100644 --- a/src/parse/function.rs +++ b/src/parse/function.rs @@ -94,6 +94,7 @@ impl<'a> Parser<'a> { at_root: false, at_root_has_selector: self.at_root_has_selector, extender: self.extender, + in_keyframes: self.in_keyframes, } .parse()?; diff --git a/src/parse/import.rs b/src/parse/import.rs index c8fd931..cd6a509 100644 --- a/src/parse/import.rs +++ b/src/parse/import.rs @@ -89,6 +89,7 @@ impl<'a> Parser<'a> { at_root: self.at_root, at_root_has_selector: self.at_root_has_selector, extender: self.extender, + in_keyframes: self.in_keyframes, } .parse(); } diff --git a/src/parse/keyframes.rs b/src/parse/keyframes.rs new file mode 100644 index 0000000..fad7f22 --- /dev/null +++ b/src/parse/keyframes.rs @@ -0,0 +1,223 @@ +use std::fmt; + +use peekmore::PeekMore; + +use crate::{ + atrule::keyframes::{Keyframes, KeyframesSelector}, + error::SassResult, + parse::Stmt, + utils::eat_whole_number, + Token, +}; + +use super::Parser; + +impl fmt::Display for KeyframesSelector { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + KeyframesSelector::To => f.write_str("to"), + KeyframesSelector::From => f.write_str("from"), + KeyframesSelector::Percent(p) => write!(f, "{}%", p), + } + } +} + +struct KeyframesSelectorParser<'a, 'b> { + parser: &'a mut Parser<'b>, +} + +impl<'a, 'b> KeyframesSelectorParser<'a, 'b> { + pub fn new(parser: &'a mut Parser<'b>) -> Self { + Self { parser } + } + + fn parse_keyframes_selector(&mut self) -> SassResult> { + let mut selectors = Vec::new(); + self.parser.whitespace_or_comment(); + while let Some(tok) = self.parser.toks.peek().cloned() { + match tok.kind { + 't' | 'T' => { + let mut ident = self.parser.parse_identifier()?; + ident.node.make_ascii_lowercase(); + if ident.node == "to" { + selectors.push(KeyframesSelector::To) + } else { + return Err(("Expected \"to\" or \"from\".", tok.pos).into()); + } + } + 'f' | 'F' => { + let mut ident = self.parser.parse_identifier()?; + ident.node.make_ascii_lowercase(); + if ident.node == "from" { + selectors.push(KeyframesSelector::From) + } else { + return Err(("Expected \"to\" or \"from\".", tok.pos).into()); + } + } + '0'..='9' => { + let mut num = String::new(); + eat_whole_number(self.parser.toks, &mut num); + if !matches!(self.parser.toks.next(), Some(Token { kind: '%', .. })) { + return Err(("expected \"%\".", tok.pos).into()); + } + selectors.push(KeyframesSelector::Percent(num.into_boxed_str())); + } + '{' => break, + '\\' => todo!("escaped chars in @keyframes selector"), + _ => return Err(("Expected \"to\" or \"from\".", tok.pos).into()), + } + self.parser.whitespace_or_comment(); + if let Some(Token { kind: ',', .. }) = self.parser.toks.peek() { + self.parser.toks.next(); + self.parser.whitespace_or_comment(); + } else { + break; + } + } + Ok(selectors) + } +} + +impl<'a> Parser<'a> { + fn parse_keyframes_name(&mut self) -> SassResult { + let mut name = String::new(); + let mut found_open_brace = false; + self.whitespace_or_comment(); + while let Some(tok) = self.toks.next() { + match tok.kind { + '#' => { + if let Some(Token { kind: '{', .. }) = self.toks.peek() { + self.toks.next(); + name.push_str(&self.parse_interpolation_as_string()?); + } else { + name.push('#'); + } + } + ' ' | '\n' | '\t' => { + self.whitespace(); + name.push(' '); + } + '{' => { + found_open_brace = true; + break; + } + _ => name.push(tok.kind), + } + } + + if !found_open_brace { + return Err(("expected \"{\".", self.span_before).into()); + } + + // todo: we can avoid the reallocation by trimming before emitting (in `output.rs`) + Ok(name.trim().to_string()) + } + + pub(super) fn parse_keyframes_selector( + &mut self, + mut string: String, + ) -> SassResult> { + let mut span = if let Some(tok) = self.toks.peek() { + tok.pos() + } else { + return Err(("expected \"{\".", self.span_before).into()); + }; + + self.span_before = span; + + let mut found_curly = false; + + while let Some(tok) = self.toks.next() { + span = span.merge(tok.pos()); + match tok.kind { + '#' => { + if let Some(Token { kind: '{', .. }) = self.toks.peek().cloned() { + self.toks.next(); + string.push_str(&self.parse_interpolation()?.to_css_string(span)?); + } else { + string.push('#'); + } + } + ',' => { + while let Some(c) = string.pop() { + if c == ' ' || c == ',' { + continue; + } + string.push(c); + string.push(','); + break; + } + } + '/' => { + if self.toks.peek().is_none() { + return Err(("Expected selector.", tok.pos()).into()); + } + self.parse_comment()?; + self.whitespace(); + string.push(' '); + } + '{' => { + found_curly = true; + break; + } + c => string.push(c), + } + } + + if !found_curly { + return Err(("expected \"{\".", span).into()); + } + + let sel_toks: Vec = string.chars().map(|x| Token::new(span, x)).collect(); + + let mut iter = sel_toks.into_iter().peekmore(); + + let selector = KeyframesSelectorParser::new(&mut Parser { + toks: &mut iter, + map: self.map, + path: self.path, + scopes: self.scopes, + global_scope: self.global_scope, + super_selectors: self.super_selectors, + span_before: self.span_before, + content: self.content, + in_mixin: self.in_mixin, + in_function: self.in_function, + in_control_flow: self.in_control_flow, + at_root: self.at_root, + at_root_has_selector: self.at_root_has_selector, + extender: self.extender, + in_keyframes: self.in_keyframes, + }) + .parse_keyframes_selector()?; + + Ok(selector) + } + + pub(super) fn parse_keyframes(&mut self) -> SassResult { + let name = self.parse_keyframes_name()?; + + self.whitespace(); + + let body = Parser { + toks: self.toks, + map: self.map, + path: self.path, + scopes: self.scopes, + global_scope: self.global_scope, + super_selectors: self.super_selectors, + span_before: self.span_before, + content: self.content, + in_mixin: self.in_mixin, + in_function: self.in_function, + in_control_flow: self.in_control_flow, + at_root: false, + in_keyframes: true, + at_root_has_selector: self.at_root_has_selector, + extender: self.extender, + } + .parse_stmt()?; + + Ok(Stmt::Keyframes(Box::new(Keyframes { name, body }))) + } +} diff --git a/src/parse/mixin.rs b/src/parse/mixin.rs index 349d70d..16df87a 100644 --- a/src/parse/mixin.rs +++ b/src/parse/mixin.rs @@ -134,6 +134,7 @@ impl<'a> Parser<'a> { at_root: false, at_root_has_selector: self.at_root_has_selector, extender: self.extender, + in_keyframes: self.in_keyframes, } .parse()?; @@ -181,6 +182,7 @@ impl<'a> Parser<'a> { at_root: self.at_root, at_root_has_selector: self.at_root_has_selector, extender: self.extender, + in_keyframes: self.in_keyframes, } .parse()? } else { diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 3be9b2b..7efc1c7 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -5,7 +5,11 @@ use num_traits::cast::ToPrimitive; use peekmore::{PeekMore, PeekMoreIterator}; use crate::{ - atrule::{media::MediaRule, AtRuleKind, Content, SupportsRule, UnknownAtRule}, + atrule::{ + keyframes::{Keyframes, KeyframesRuleSet}, + media::MediaRule, + AtRuleKind, Content, SupportsRule, UnknownAtRule, + }, common::{Brackets, ListSeparator}, error::SassResult, scope::Scope, @@ -31,6 +35,7 @@ pub mod common; mod function; mod ident; mod import; +mod keyframes; mod media; mod mixin; mod style; @@ -57,6 +62,8 @@ pub(crate) enum Stmt { }, Comment(String), Return(Box), + Keyframes(Box), + KeyframesRuleSet(Box), } /// We could use a generic for the toks, but it makes the API @@ -76,6 +83,7 @@ pub(crate) struct Parser<'a> { pub in_mixin: bool, pub in_function: bool, pub in_control_flow: bool, + pub in_keyframes: bool, /// Whether this parser is at the root of the document /// E.g. not inside a style, mixin, or function pub at_root: bool, @@ -193,14 +201,14 @@ impl<'a> Parser<'a> { continue; } AtRuleKind::Media => stmts.push(self.parse_media()?), - AtRuleKind::Unknown(_) | AtRuleKind::Keyframes => { + AtRuleKind::Unknown(_) => { stmts.push(self.parse_unknown_at_rule(kind_string.node)?) } AtRuleKind::Use => todo!("@use not yet implemented"), AtRuleKind::Forward => todo!("@forward not yet implemented"), AtRuleKind::Extend => self.parse_extend()?, AtRuleKind::Supports => stmts.push(self.parse_supports()?), - // AtRuleKind::Keyframes => stmts.push(self.parse_keyframes()?), + AtRuleKind::Keyframes => stmts.push(self.parse_keyframes()?), } } '$' => self.parse_variable_declaration()?, @@ -225,42 +233,72 @@ impl<'a> Parser<'a> { } // dart-sass seems to special-case the error message here? '!' | '{' => return Err(("expected \"}\".", *pos).into()), - _ => match self.is_selector_or_style()? { - SelectorOrStyle::Style(property, value) => { - if let Some(value) = value { - stmts.push(Stmt::Style(Style { property, value })); - } else { - stmts.extend( - self.parse_style_group(property)? - .into_iter() - .map(Stmt::Style), - ); + _ => { + if self.in_keyframes { + match self.is_selector_or_style()? { + SelectorOrStyle::Style(property, value) => { + if let Some(value) = value { + stmts.push(Stmt::Style(Style { property, value })); + } else { + stmts.extend( + self.parse_style_group(property)? + .into_iter() + .map(Stmt::Style), + ); + } + } + SelectorOrStyle::Selector(init) => { + let selector = self.parse_keyframes_selector(init)?; + self.scopes.push(self.scopes.last().clone()); + + let body = self.parse_stmt()?; + self.scopes.pop(); + stmts.push(Stmt::KeyframesRuleSet(Box::new(KeyframesRuleSet { + selector, + body, + }))); + } + } + continue; + } + + match self.is_selector_or_style()? { + SelectorOrStyle::Style(property, value) => { + if let Some(value) = value { + stmts.push(Stmt::Style(Style { property, value })); + } else { + stmts.extend( + self.parse_style_group(property)? + .into_iter() + .map(Stmt::Style), + ); + } + } + SelectorOrStyle::Selector(init) => { + let at_root = self.at_root; + self.at_root = false; + let selector = self + .parse_selector(!self.super_selectors.is_empty(), false, init)? + .resolve_parent_selectors( + self.super_selectors.last(), + !at_root || self.at_root_has_selector, + )?; + self.scopes.push(self.scopes.last().clone()); + self.super_selectors.push(selector.clone()); + + let extended_selector = self.extender.add_selector(selector.0, None); + + let body = self.parse_stmt()?; + self.scopes.pop(); + self.super_selectors.pop(); + self.at_root = self.super_selectors.is_empty(); + stmts.push(Stmt::RuleSet { + selector: extended_selector, + body, + }); } } - SelectorOrStyle::Selector(init) => { - let at_root = self.at_root; - self.at_root = false; - let selector = self - .parse_selector(!self.super_selectors.is_empty(), false, init)? - .resolve_parent_selectors( - self.super_selectors.last(), - !at_root || self.at_root_has_selector, - )?; - self.scopes.push(self.scopes.last().clone()); - self.super_selectors.push(selector.clone()); - - let extended_selector = self.extender.add_selector(selector.0, None); - - let body = self.parse_stmt()?; - self.scopes.pop(); - self.super_selectors.pop(); - self.at_root = self.super_selectors.is_empty(); - stmts.push(Stmt::RuleSet { - selector: extended_selector, - body, - }); - } - }, + } } } Ok(stmts) @@ -343,6 +381,7 @@ impl<'a> Parser<'a> { at_root: self.at_root, at_root_has_selector: self.at_root_has_selector, extender: self.extender, + in_keyframes: self.in_keyframes, }, allows_parent, true, @@ -350,8 +389,6 @@ impl<'a> Parser<'a> { ) .parse()?; - // todo: we should be registering the selector here, but that would require being given - // an `Rc>`, which we haven't implemented yet. Ok(Selector(selector)) } @@ -575,6 +612,7 @@ impl<'a> Parser<'a> { at_root: self.at_root, at_root_has_selector: self.at_root_has_selector, extender: self.extender, + in_keyframes: self.in_keyframes, } .parse(); } @@ -597,6 +635,7 @@ impl<'a> Parser<'a> { at_root: self.at_root, at_root_has_selector: self.at_root_has_selector, extender: self.extender, + in_keyframes: self.in_keyframes, } .parse() } @@ -740,6 +779,7 @@ impl<'a> Parser<'a> { at_root: self.at_root, at_root_has_selector: self.at_root_has_selector, extender: self.extender, + in_keyframes: self.in_keyframes, } .parse()?; if !these_stmts.is_empty() { @@ -762,6 +802,7 @@ impl<'a> Parser<'a> { at_root: self.at_root, at_root_has_selector: self.at_root_has_selector, extender: self.extender, + in_keyframes: self.in_keyframes, } .parse()?, ); @@ -812,6 +853,7 @@ impl<'a> Parser<'a> { at_root: self.at_root, at_root_has_selector: self.at_root_has_selector, extender: self.extender, + in_keyframes: self.in_keyframes, } .parse()?; if !these_stmts.is_empty() { @@ -834,6 +876,7 @@ impl<'a> Parser<'a> { at_root: self.at_root, at_root_has_selector: self.at_root_has_selector, extender: self.extender, + in_keyframes: self.in_keyframes, } .parse()?, ); @@ -944,6 +987,7 @@ impl<'a> Parser<'a> { at_root: self.at_root, at_root_has_selector: self.at_root_has_selector, extender: self.extender, + in_keyframes: self.in_keyframes, } .parse()?; if !these_stmts.is_empty() { @@ -966,6 +1010,7 @@ impl<'a> Parser<'a> { at_root: self.at_root, at_root_has_selector: self.at_root_has_selector, extender: self.extender, + in_keyframes: self.in_keyframes, } .parse()?, ); @@ -1062,6 +1107,7 @@ impl<'a> Parser<'a> { at_root: false, at_root_has_selector: self.at_root_has_selector, extender: self.extender, + in_keyframes: self.in_keyframes, } .parse_stmt()?; @@ -1130,6 +1176,7 @@ impl<'a> Parser<'a> { at_root: true, at_root_has_selector, extender: self.extender, + in_keyframes: self.in_keyframes, } .parse()? .into_iter() @@ -1171,6 +1218,7 @@ impl<'a> Parser<'a> { at_root: self.at_root, at_root_has_selector: self.at_root_has_selector, extender: self.extender, + in_keyframes: self.in_keyframes, } .parse_selector(false, true, String::new())?; @@ -1249,6 +1297,7 @@ impl<'a> Parser<'a> { at_root: false, at_root_has_selector: self.at_root_has_selector, extender: self.extender, + in_keyframes: self.in_keyframes, } .parse()?; @@ -1277,11 +1326,6 @@ impl<'a> Parser<'a> { }))) } - #[allow(dead_code, clippy::unused_self)] - fn parse_keyframes(&mut self) -> SassResult { - todo!("@keyframes not yet implemented") - } - // todo: we should use a specialized struct to represent these fn parse_media_args(&mut self) -> SassResult { let mut params = String::new(); diff --git a/src/parse/style.rs b/src/parse/style.rs index 85c28d1..eec0180 100644 --- a/src/parse/style.rs +++ b/src/parse/style.rs @@ -13,37 +13,6 @@ use super::common::SelectorOrStyle; use super::Parser; impl<'a> Parser<'a> { - /// Determines whether the parser is looking at a style or a selector - /// - /// When parsing the children of a style rule, property declarations, - /// namespaced variable declarations, and nested style rules can all begin - /// with bare identifiers. In order to know which statement type to produce, - /// we need to disambiguate them. We use the following criteria: - /// - /// * If the entity starts with an identifier followed by a period and a - /// dollar sign, it's a variable declaration. This is the simplest case, - /// because `.$` is used in and only in variable declarations. - /// - /// * If the entity doesn't start with an identifier followed by a colon, - /// it's a selector. There are some additional mostly-unimportant cases - /// here to support various declaration hacks. - /// - /// * If the colon is followed by another colon, it's a selector. - /// - /// * Otherwise, if the colon is followed by anything other than - /// interpolation or a character that's valid as the beginning of an - /// identifier, it's a declaration. - /// - /// * If the colon is followed by interpolation or a valid identifier, try - /// parsing it as a declaration value. If this fails, backtrack and parse - /// it as a selector. - /// - /// * If the declaration value is valid but is followed by "{", backtrack and - /// parse it as a selector anyway. This ensures that ".foo:bar {" is always - /// parsed as a selector and never as a property with nested properties - /// beneath it. - // todo: potentially we read the property to a string already since properties - // are more common than selectors? this seems to be annihilating our performance fn parse_style_value_when_no_space_after_semicolon(&mut self) -> Option> { let mut toks = Vec::new(); while let Some(tok) = self.toks.peek() { @@ -94,6 +63,37 @@ impl<'a> Parser<'a> { Some(toks) } + /// Determines whether the parser is looking at a style or a selector + /// + /// When parsing the children of a style rule, property declarations, + /// namespaced variable declarations, and nested style rules can all begin + /// with bare identifiers. In order to know which statement type to produce, + /// we need to disambiguate them. We use the following criteria: + /// + /// * If the entity starts with an identifier followed by a period and a + /// dollar sign, it's a variable declaration. This is the simplest case, + /// because `.$` is used in and only in variable declarations. + /// + /// * If the entity doesn't start with an identifier followed by a colon, + /// it's a selector. There are some additional mostly-unimportant cases + /// here to support various declaration hacks. + /// + /// * If the colon is followed by another colon, it's a selector. + /// + /// * Otherwise, if the colon is followed by anything other than + /// interpolation or a character that's valid as the beginning of an + /// identifier, it's a declaration. + /// + /// * If the colon is followed by interpolation or a valid identifier, try + /// parsing it as a declaration value. If this fails, backtrack and parse + /// it as a selector. + /// + /// * If the declaration value is valid but is followed by "{", backtrack and + /// parse it as a selector anyway. This ensures that ".foo:bar {" is always + /// parsed as a selector and never as a property with nested properties + /// beneath it. + // todo: potentially we read the property to a string already since properties + // are more common than selectors? this seems to be annihilating our performance pub(super) fn is_selector_or_style(&mut self) -> SassResult { if let Some(first_char) = self.toks.peek() { if first_char.kind == '#' { diff --git a/src/parse/value/parse.rs b/src/parse/value/parse.rs index 03f3789..7f84229 100644 --- a/src/parse/value/parse.rs +++ b/src/parse/value/parse.rs @@ -194,6 +194,7 @@ impl<'a> Parser<'a> { at_root: self.at_root, at_root_has_selector: self.at_root_has_selector, extender: self.extender, + in_keyframes: self.in_keyframes, } .parse_value() } diff --git a/src/utils/number.rs b/src/utils/number.rs index 55bf8aa..c84ac4f 100644 --- a/src/utils/number.rs +++ b/src/utils/number.rs @@ -117,7 +117,10 @@ pub(crate) fn eat_number>( }) } -fn eat_whole_number>(toks: &mut PeekMoreIterator, buf: &mut String) { +pub(crate) fn eat_whole_number>( + toks: &mut PeekMoreIterator, + buf: &mut String, +) { while let Some(c) = toks.peek() { if !c.kind.is_ascii_digit() { break; diff --git a/src/value/mod.rs b/src/value/mod.rs index 1c62baa..bc2ce1a 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -341,6 +341,7 @@ impl Value { at_root: parser.at_root, at_root_has_selector: parser.at_root_has_selector, extender: parser.extender, + in_keyframes: parser.in_keyframes, } .parse_selector(allows_parent, true, String::new()) } diff --git a/tests/keyframes.rs b/tests/keyframes.rs new file mode 100644 index 0000000..80b99ff --- /dev/null +++ b/tests/keyframes.rs @@ -0,0 +1,111 @@ +#![cfg(test)] + +#[macro_use] +mod macros; + +// @content inside keyframes +test!( + content_inside_keyframes, + "@mixin foo { + @keyframes { + @content; + } + } + a { + @include foo { + color: red; + }; + }", + "@keyframes {\n color: red;\n}\n" +); + +test!( + empty_keyframes_is_emitted_exact, + "@keyframes {}", + "@keyframes {}\n" +); +test!( + keyframes_is_at_root, + "a {\n @keyframes {}\n}\n", + "@keyframes {}\n" +); +test!( + keyframes_inside_ruleset_with_other_styles, + "a { + color: red; + @keyframes {} + color: green; + }", + "a {\n color: red;\n color: green;\n}\n@keyframes {}\n" +); +test!( + keyframes_lowercase_to, + "@keyframes {to {color: red;}}", + "@keyframes {\n to {\n color: red;\n }\n}\n" +); +test!( + keyframes_lowercase_from, + "@keyframes {from {color: red;}}", + "@keyframes {\n from {\n color: red;\n }\n}\n" +); +test!( + keyframes_uppercase_to, + "@keyframes {TO {color: red;}}", + "@keyframes {\n to {\n color: red;\n }\n}\n" +); +test!( + keyframes_uppercase_from, + "@keyframes {FROM {color: red;}}", + "@keyframes {\n from {\n color: red;\n }\n}\n" +); +error!( + keyframes_invalid_selector_beginning_with_f, + "@keyframes {foo {}}", "Error: Expected \"to\" or \"from\"." +); +error!( + keyframes_invalid_selector_beginning_with_t, + "@keyframes {too {}}", "Error: Expected \"to\" or \"from\"." +); +error!( + keyframes_invalid_selector_beginning_with_ascii_char, + "@keyframes {a {}}", "Error: Expected \"to\" or \"from\"." +); +error!( + keyframes_invalid_selector_number_missing_percent, + "@keyframes {10 {}}", "Error: expected \"%\"." +); +test!( + keyframes_simple_percent_selector, + "@keyframes {0% {color: red;}}", + "@keyframes {\n 0% {\n color: red;\n }\n}\n" +); +test!( + keyframes_comma_separated_percent_selectors, + "@keyframes {0%, 5%, 10%, 15% {color: red;}}", + "@keyframes {\n 0%, 5%, 10%, 15% {\n color: red;\n }\n}\n" +); +test!( + keyframes_empty_with_name, + "@keyframes foo {}", + "@keyframes foo {}\n" +); +test!( + keyframes_variable_in_name, + "@keyframes $foo {}", + "@keyframes $foo {}\n" +); +test!( + keyframes_arithmetic_in_name, + "@keyframes 1 + 2 {}", + "@keyframes 1 + 2 {}\n" +); +test!( + keyframes_interpolation_in_name, + "@keyframes #{1 + 2} {}", + "@keyframes 3 {}\n" +); +test!( + keyframes_contains_multiline_comment, + "@keyframes foo {/**/}", + "@keyframes foo {\n /**/\n}\n" +);