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
- 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

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;
mod function;
pub mod keyframes;
mod kind;
pub mod media;
mod mixin;

View File

@ -155,6 +155,7 @@ pub fn from_path(p: &str) -> Result<String> {
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<String> {
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<String, JsValue> {
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())?;

View File

@ -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<BlockEntry>),
MultilineComment(String),
UnknownAtRule(Box<ToplevelUnknownAtRule>),
Keyframes(Box<Keyframes>),
KeyframesRuleSet(Vec<KeyframesSelector>, Vec<BlockEntry>),
Media { query: String, body: Vec<Stmt> },
Supports { params: String, body: Vec<Stmt> },
Newline,
@ -49,18 +55,26 @@ impl Toplevel {
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) {
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::<Vec<String>>()
.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;

View File

@ -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()?;

View File

@ -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();
}

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_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 {

View File

@ -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<Value>),
Keyframes(Box<Keyframes>),
KeyframesRuleSet(Box<KeyframesRuleSet>),
}
/// 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<RefCell<Selector>>`, 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<Stmt> {
todo!("@keyframes not yet implemented")
}
// todo: we should use a specialized struct to represent these
fn parse_media_args(&mut self) -> SassResult<String> {
let mut params = String::new();

View File

@ -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<Vec<Token>> {
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<SelectorOrStyle> {
if let Some(first_char) = self.toks.peek() {
if first_char.kind == '#' {

View File

@ -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()
}

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() {
if !c.kind.is_ascii_digit() {
break;

View File

@ -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())
}

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