initial implementation of @keyframes
This commit is contained in:
parent
65f93ad6d5
commit
827225a143
14
CHANGELOG.md
14
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
|
||||
|
||||
|
20
src/atrule/keyframes.rs
Normal file
20
src/atrule/keyframes.rs
Normal 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>),
|
||||
}
|
@ -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;
|
||||
|
@ -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())?;
|
||||
|
@ -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;
|
||||
|
@ -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()?;
|
||||
|
||||
|
@ -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
223
src/parse/keyframes.rs
Normal 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 })))
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
132
src/parse/mod.rs
132
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<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();
|
||||
|
@ -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 == '#' {
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
111
tests/keyframes.rs
Normal 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"
|
||||
);
|
Loading…
x
Reference in New Issue
Block a user