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
|
# 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
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;
|
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;
|
||||||
|
@ -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())?;
|
||||||
|
@ -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;
|
||||||
|
@ -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()?;
|
||||||
|
|
||||||
|
@ -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
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: 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 {
|
||||||
|
132
src/parse/mod.rs
132
src/parse/mod.rs
@ -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();
|
||||||
|
@ -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 == '#' {
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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
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