initial implementation of @keyframes

This commit is contained in:
Connor Skees 2020-07-04 20:50:53 -04:00
parent 65f93ad6d5
commit 827225a143
15 changed files with 583 additions and 84 deletions

View File

@ -1,10 +1,14 @@
# 0.10.0
- implement `@keyframes`
# 0.9.3 # 0.9.3
- fix parsing bugs for empty bracketed lists - fix parsing bugs for empty bracketed lists
- partially implement inverse units - partially implement inverse units
- remove all remaining `todo!()`s from binary and unary ops - remove all remaining `todo!()`s from binary and unary ops
- parse keywords case sensitively - parse keywords case sensitively
- various optimizations that make bulma about *6x faster* to compile - various optimizations that make bulma about _6x faster_ to compile
# 0.9.2 # 0.9.2

20
src/atrule/keyframes.rs Normal file
View File

@ -0,0 +1,20 @@
use crate::parse::Stmt;
#[derive(Debug, Clone)]
pub(crate) struct Keyframes {
pub name: String,
pub body: Vec<Stmt>,
}
#[derive(Debug, Clone)]
pub(crate) struct KeyframesRuleSet {
pub selector: Vec<KeyframesSelector>,
pub body: Vec<Stmt>,
}
#[derive(Debug, Clone)]
pub(crate) enum KeyframesSelector {
To,
From,
Percent(Box<str>),
}

View File

@ -5,6 +5,7 @@ pub(crate) use supports::SupportsRule;
pub(crate) use unknown::UnknownAtRule; pub(crate) use unknown::UnknownAtRule;
mod function; mod function;
pub mod keyframes;
mod kind; mod kind;
pub mod media; pub mod media;
mod mixin; mod mixin;

View File

@ -155,6 +155,7 @@ pub fn from_path(p: &str) -> Result<String> {
at_root: true, at_root: true,
at_root_has_selector: false, at_root_has_selector: false,
extender: &mut Extender::new(empty_span), extender: &mut Extender::new(empty_span),
in_keyframes: false,
} }
.parse() .parse()
.map_err(|e| raw_to_parse_error(&map, *e))?; .map_err(|e| raw_to_parse_error(&map, *e))?;
@ -199,6 +200,7 @@ pub fn from_string(p: String) -> Result<String> {
at_root: true, at_root: true,
at_root_has_selector: false, at_root_has_selector: false,
extender: &mut Extender::new(empty_span), extender: &mut Extender::new(empty_span),
in_keyframes: false,
} }
.parse() .parse()
.map_err(|e| raw_to_parse_error(&map, *e))?; .map_err(|e| raw_to_parse_error(&map, *e))?;
@ -234,6 +236,7 @@ pub fn from_string(p: String) -> std::result::Result<String, JsValue> {
at_root: true, at_root: true,
at_root_has_selector: false, at_root_has_selector: false,
extender: &mut Extender::new(empty_span), extender: &mut Extender::new(empty_span),
in_keyframes: false,
} }
.parse() .parse()
.map_err(|e| raw_to_parse_error(&map, *e).to_string())?; .map_err(|e| raw_to_parse_error(&map, *e).to_string())?;

View File

@ -4,7 +4,11 @@ use std::io::Write;
use codemap::CodeMap; use codemap::CodeMap;
use crate::{ use crate::{
atrule::{media::MediaRule, SupportsRule, UnknownAtRule}, atrule::{
keyframes::{Keyframes, KeyframesRuleSet, KeyframesSelector},
media::MediaRule,
SupportsRule, UnknownAtRule,
},
error::SassResult, error::SassResult,
parse::Stmt, parse::Stmt,
selector::Selector, selector::Selector,
@ -23,6 +27,8 @@ enum Toplevel {
RuleSet(Selector, Vec<BlockEntry>), RuleSet(Selector, Vec<BlockEntry>),
MultilineComment(String), MultilineComment(String),
UnknownAtRule(Box<ToplevelUnknownAtRule>), UnknownAtRule(Box<ToplevelUnknownAtRule>),
Keyframes(Box<Keyframes>),
KeyframesRuleSet(Vec<KeyframesSelector>, Vec<BlockEntry>),
Media { query: String, body: Vec<Stmt> }, Media { query: String, body: Vec<Stmt> },
Supports { params: String, body: Vec<Stmt> }, Supports { params: String, body: Vec<Stmt> },
Newline, Newline,
@ -49,18 +55,26 @@ impl Toplevel {
Toplevel::RuleSet(selector, Vec::new()) Toplevel::RuleSet(selector, Vec::new())
} }
fn new_keyframes_rule(selector: Vec<KeyframesSelector>) -> Self {
Toplevel::KeyframesRuleSet(selector, Vec::new())
}
fn push_style(&mut self, s: Style) { fn push_style(&mut self, s: Style) {
if s.value.is_null() { if s.value.is_null() {
return; return;
} }
if let Toplevel::RuleSet(_, entries) = self { if let Toplevel::RuleSet(_, entries) | Toplevel::KeyframesRuleSet(_, entries) = self {
entries.push(BlockEntry::Style(Box::new(s))); entries.push(BlockEntry::Style(Box::new(s)));
} else {
panic!()
} }
} }
fn push_comment(&mut self, s: String) { 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)); entries.push(BlockEntry::MultilineComment(s));
} else {
panic!()
} }
} }
} }
@ -120,6 +134,13 @@ impl Css {
Ok(()) 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 vals
@ -146,6 +167,22 @@ impl Css {
} }
Stmt::Return(..) => unreachable!("@return: {:?}", stmt), Stmt::Return(..) => unreachable!("@return: {:?}", stmt),
Stmt::AtRoot { .. } => unreachable!("@at-root: {:?}", 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)?; 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::<Vec<String>>()
.join(", ")
)?;
for style in body {
writeln!(buf, "{} {}", padding, style.to_string()?)?;
}
writeln!(buf, "{}}}", padding)?;
}
Toplevel::MultilineComment(s) => { Toplevel::MultilineComment(s) => {
has_written = true; has_written = true;
writeln!(buf, "{}/*{}*/", padding, s)?; writeln!(buf, "{}/*{}*/", padding, s)?;
@ -232,6 +293,29 @@ impl Css {
Css::from_stmts(body)?._inner_pretty_print(buf, map, nesting + 1)?; Css::from_stmts(body)?._inner_pretty_print(buf, map, nesting + 1)?;
writeln!(buf, "{}}}", padding)?; 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 } => { Toplevel::Supports { params, body } => {
if should_emit_newline { if should_emit_newline {
should_emit_newline = false; should_emit_newline = false;

View File

@ -94,6 +94,7 @@ impl<'a> Parser<'a> {
at_root: false, at_root: false,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender, extender: self.extender,
in_keyframes: self.in_keyframes,
} }
.parse()?; .parse()?;

View File

@ -89,6 +89,7 @@ impl<'a> Parser<'a> {
at_root: self.at_root, at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender, extender: self.extender,
in_keyframes: self.in_keyframes,
} }
.parse(); .parse();
} }

223
src/parse/keyframes.rs Normal file
View File

@ -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<Vec<KeyframesSelector>> {
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<String> {
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<Vec<KeyframesSelector>> {
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<Token> = 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<Stmt> {
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 })))
}
}

View File

@ -134,6 +134,7 @@ impl<'a> Parser<'a> {
at_root: false, at_root: false,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender, extender: self.extender,
in_keyframes: self.in_keyframes,
} }
.parse()?; .parse()?;
@ -181,6 +182,7 @@ impl<'a> Parser<'a> {
at_root: self.at_root, at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender, extender: self.extender,
in_keyframes: self.in_keyframes,
} }
.parse()? .parse()?
} else { } else {

View File

@ -5,7 +5,11 @@ use num_traits::cast::ToPrimitive;
use peekmore::{PeekMore, PeekMoreIterator}; use peekmore::{PeekMore, PeekMoreIterator};
use crate::{ use crate::{
atrule::{media::MediaRule, AtRuleKind, Content, SupportsRule, UnknownAtRule}, atrule::{
keyframes::{Keyframes, KeyframesRuleSet},
media::MediaRule,
AtRuleKind, Content, SupportsRule, UnknownAtRule,
},
common::{Brackets, ListSeparator}, common::{Brackets, ListSeparator},
error::SassResult, error::SassResult,
scope::Scope, scope::Scope,
@ -31,6 +35,7 @@ pub mod common;
mod function; mod function;
mod ident; mod ident;
mod import; mod import;
mod keyframes;
mod media; mod media;
mod mixin; mod mixin;
mod style; mod style;
@ -57,6 +62,8 @@ pub(crate) enum Stmt {
}, },
Comment(String), Comment(String),
Return(Box<Value>), Return(Box<Value>),
Keyframes(Box<Keyframes>),
KeyframesRuleSet(Box<KeyframesRuleSet>),
} }
/// We could use a generic for the toks, but it makes the API /// 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_mixin: bool,
pub in_function: bool, pub in_function: bool,
pub in_control_flow: bool, pub in_control_flow: bool,
pub in_keyframes: bool,
/// Whether this parser is at the root of the document /// Whether this parser is at the root of the document
/// E.g. not inside a style, mixin, or function /// E.g. not inside a style, mixin, or function
pub at_root: bool, pub at_root: bool,
@ -193,14 +201,14 @@ impl<'a> Parser<'a> {
continue; continue;
} }
AtRuleKind::Media => stmts.push(self.parse_media()?), AtRuleKind::Media => stmts.push(self.parse_media()?),
AtRuleKind::Unknown(_) | AtRuleKind::Keyframes => { AtRuleKind::Unknown(_) => {
stmts.push(self.parse_unknown_at_rule(kind_string.node)?) stmts.push(self.parse_unknown_at_rule(kind_string.node)?)
} }
AtRuleKind::Use => todo!("@use not yet implemented"), AtRuleKind::Use => todo!("@use not yet implemented"),
AtRuleKind::Forward => todo!("@forward not yet implemented"), AtRuleKind::Forward => todo!("@forward not yet implemented"),
AtRuleKind::Extend => self.parse_extend()?, AtRuleKind::Extend => self.parse_extend()?,
AtRuleKind::Supports => stmts.push(self.parse_supports()?), 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()?, '$' => self.parse_variable_declaration()?,
@ -225,42 +233,72 @@ impl<'a> Parser<'a> {
} }
// dart-sass seems to special-case the error message here? // dart-sass seems to special-case the error message here?
'!' | '{' => return Err(("expected \"}\".", *pos).into()), '!' | '{' => return Err(("expected \"}\".", *pos).into()),
_ => match self.is_selector_or_style()? { _ => {
SelectorOrStyle::Style(property, value) => { if self.in_keyframes {
if let Some(value) = value { match self.is_selector_or_style()? {
stmts.push(Stmt::Style(Style { property, value })); SelectorOrStyle::Style(property, value) => {
} else { if let Some(value) = value {
stmts.extend( stmts.push(Stmt::Style(Style { property, value }));
self.parse_style_group(property)? } else {
.into_iter() stmts.extend(
.map(Stmt::Style), 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) Ok(stmts)
@ -343,6 +381,7 @@ impl<'a> Parser<'a> {
at_root: self.at_root, at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender, extender: self.extender,
in_keyframes: self.in_keyframes,
}, },
allows_parent, allows_parent,
true, true,
@ -350,8 +389,6 @@ impl<'a> Parser<'a> {
) )
.parse()?; .parse()?;
// todo: we should be registering the selector here, but that would require being given
// an `Rc<RefCell<Selector>>`, which we haven't implemented yet.
Ok(Selector(selector)) Ok(Selector(selector))
} }
@ -575,6 +612,7 @@ impl<'a> Parser<'a> {
at_root: self.at_root, at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender, extender: self.extender,
in_keyframes: self.in_keyframes,
} }
.parse(); .parse();
} }
@ -597,6 +635,7 @@ impl<'a> Parser<'a> {
at_root: self.at_root, at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender, extender: self.extender,
in_keyframes: self.in_keyframes,
} }
.parse() .parse()
} }
@ -740,6 +779,7 @@ impl<'a> Parser<'a> {
at_root: self.at_root, at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender, extender: self.extender,
in_keyframes: self.in_keyframes,
} }
.parse()?; .parse()?;
if !these_stmts.is_empty() { if !these_stmts.is_empty() {
@ -762,6 +802,7 @@ impl<'a> Parser<'a> {
at_root: self.at_root, at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender, extender: self.extender,
in_keyframes: self.in_keyframes,
} }
.parse()?, .parse()?,
); );
@ -812,6 +853,7 @@ impl<'a> Parser<'a> {
at_root: self.at_root, at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender, extender: self.extender,
in_keyframes: self.in_keyframes,
} }
.parse()?; .parse()?;
if !these_stmts.is_empty() { if !these_stmts.is_empty() {
@ -834,6 +876,7 @@ impl<'a> Parser<'a> {
at_root: self.at_root, at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender, extender: self.extender,
in_keyframes: self.in_keyframes,
} }
.parse()?, .parse()?,
); );
@ -944,6 +987,7 @@ impl<'a> Parser<'a> {
at_root: self.at_root, at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender, extender: self.extender,
in_keyframes: self.in_keyframes,
} }
.parse()?; .parse()?;
if !these_stmts.is_empty() { if !these_stmts.is_empty() {
@ -966,6 +1010,7 @@ impl<'a> Parser<'a> {
at_root: self.at_root, at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender, extender: self.extender,
in_keyframes: self.in_keyframes,
} }
.parse()?, .parse()?,
); );
@ -1062,6 +1107,7 @@ impl<'a> Parser<'a> {
at_root: false, at_root: false,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender, extender: self.extender,
in_keyframes: self.in_keyframes,
} }
.parse_stmt()?; .parse_stmt()?;
@ -1130,6 +1176,7 @@ impl<'a> Parser<'a> {
at_root: true, at_root: true,
at_root_has_selector, at_root_has_selector,
extender: self.extender, extender: self.extender,
in_keyframes: self.in_keyframes,
} }
.parse()? .parse()?
.into_iter() .into_iter()
@ -1171,6 +1218,7 @@ impl<'a> Parser<'a> {
at_root: self.at_root, at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender, extender: self.extender,
in_keyframes: self.in_keyframes,
} }
.parse_selector(false, true, String::new())?; .parse_selector(false, true, String::new())?;
@ -1249,6 +1297,7 @@ impl<'a> Parser<'a> {
at_root: false, at_root: false,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender, extender: self.extender,
in_keyframes: self.in_keyframes,
} }
.parse()?; .parse()?;
@ -1277,11 +1326,6 @@ impl<'a> Parser<'a> {
}))) })))
} }
#[allow(dead_code, clippy::unused_self)]
fn parse_keyframes(&mut self) -> SassResult<Stmt> {
todo!("@keyframes not yet implemented")
}
// todo: we should use a specialized struct to represent these // todo: we should use a specialized struct to represent these
fn parse_media_args(&mut self) -> SassResult<String> { fn parse_media_args(&mut self) -> SassResult<String> {
let mut params = String::new(); let mut params = String::new();

View File

@ -13,37 +13,6 @@ use super::common::SelectorOrStyle;
use super::Parser; use super::Parser;
impl<'a> Parser<'a> { 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<Vec<Token>> { fn parse_style_value_when_no_space_after_semicolon(&mut self) -> Option<Vec<Token>> {
let mut toks = Vec::new(); let mut toks = Vec::new();
while let Some(tok) = self.toks.peek() { while let Some(tok) = self.toks.peek() {
@ -94,6 +63,37 @@ impl<'a> Parser<'a> {
Some(toks) 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<SelectorOrStyle> { pub(super) fn is_selector_or_style(&mut self) -> SassResult<SelectorOrStyle> {
if let Some(first_char) = self.toks.peek() { if let Some(first_char) = self.toks.peek() {
if first_char.kind == '#' { if first_char.kind == '#' {

View File

@ -194,6 +194,7 @@ impl<'a> Parser<'a> {
at_root: self.at_root, at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender, extender: self.extender,
in_keyframes: self.in_keyframes,
} }
.parse_value() .parse_value()
} }

View File

@ -117,7 +117,10 @@ pub(crate) fn eat_number<I: Iterator<Item = Token>>(
}) })
} }
fn eat_whole_number<I: Iterator<Item = Token>>(toks: &mut PeekMoreIterator<I>, buf: &mut String) { pub(crate) fn eat_whole_number<I: Iterator<Item = Token>>(
toks: &mut PeekMoreIterator<I>,
buf: &mut String,
) {
while let Some(c) = toks.peek() { while let Some(c) = toks.peek() {
if !c.kind.is_ascii_digit() { if !c.kind.is_ascii_digit() {
break; break;

View File

@ -341,6 +341,7 @@ impl Value {
at_root: parser.at_root, at_root: parser.at_root,
at_root_has_selector: parser.at_root_has_selector, at_root_has_selector: parser.at_root_has_selector,
extender: parser.extender, extender: parser.extender,
in_keyframes: parser.in_keyframes,
} }
.parse_selector(allows_parent, true, String::new()) .parse_selector(allows_parent, true, String::new())
} }

111
tests/keyframes.rs Normal file
View File

@ -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"
);