diff --git a/src/lib.rs b/src/lib.rs index 5011c440..5dab7122 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5396,37 +5396,77 @@ mod tests { fn test_keyframes() { minify_test( r#" - @keyframes test { - from { - background: green; - } - - 50% { - background: red; - } - + @keyframes "test" { 100% { background: blue } } "#, - "@keyframes test{0%{background:green}50%{background:red}to{background:#00f}}", + "@keyframes test{to{background:#00f}}", ); minify_test( r#" @keyframes test { + 100% { + background: blue + } + } + "#, + "@keyframes test{to{background:#00f}}", + ); + + // CSS-wide keywords and `none` cannot remove quotes. + minify_test( + r#" + @keyframes "revert" { from { background: green; - background-color: red; } + } + "#, + "@keyframes \"revert\"{0%{background:green}}", + ); - 100% { - background: blue + minify_test( + r#" + @keyframes "none" { + from { + background: green; } } "#, - "@keyframes test{0%{background:red}to{background:#00f}}", + "@keyframes \"none\"{0%{background:green}}", + ); + + // CSS-wide keywords without quotes throws an error. + error_test( + r#" + @keyframes revert {} + "#, + ParserError::UnexpectedToken(Token::Ident("revert".into())), + ); + + error_test( + r#" + @keyframes revert-layer {} + "#, + ParserError::UnexpectedToken(Token::Ident("revert-layer".into())), + ); + + error_test( + r#" + @keyframes none {} + "#, + ParserError::UnexpectedToken(Token::Ident("none".into())), ); + + error_test( + r#" + @keyframes NONE {} + "#, + ParserError::UnexpectedToken(Token::Ident("NONE".into())), + ); + minify_test( r#" @-webkit-keyframes test { diff --git a/src/parser.rs b/src/parser.rs index 1c8c28aa..079b76b5 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -13,7 +13,7 @@ use crate::rules::{ document::MozDocumentRule, font_face::{FontFaceDeclarationParser, FontFaceRule}, import::ImportRule, - keyframes::{KeyframeListParser, KeyframesRule}, + keyframes::{KeyframeListParser, KeyframesName, KeyframesRule}, layer::LayerName, media::MediaRule, namespace::NamespaceRule, @@ -122,7 +122,7 @@ pub enum AtRulePrelude<'i> { /// A @viewport rule prelude. Viewport(VendorPrefix), /// A @keyframes rule, with its animation name and vendor prefix if exists. - Keyframes(CustomIdent<'i>, VendorPrefix), + Keyframes(KeyframesName<'i>, VendorPrefix), /// A @page rule prelude. Page(Vec>), /// A @-moz-document rule. @@ -425,14 +425,8 @@ impl<'a, 'o, 'b, 'i> AtRuleParser<'i> for NestedRuleParser<'a, 'o, 'i> { VendorPrefix::None }; - let location = input.current_source_location(); - let name = match *input.next()? { - Token::Ident(ref s) => s.into(), - Token::QuotedString(ref s) => s.into(), - ref t => return Err(location.new_unexpected_token_error(t.clone())), - }; - - Ok(AtRulePrelude::Keyframes(CustomIdent(name), prefix)) + let name = input.try_parse(KeyframesName::parse)?; + Ok(AtRulePrelude::Keyframes(name, prefix)) }, "page" => { let selectors = input.try_parse(|input| input.parse_comma_separated(PageSelector::parse)).unwrap_or_default(); diff --git a/src/rules/keyframes.rs b/src/rules/keyframes.rs index 22a10561..28d02ca3 100644 --- a/src/rules/keyframes.rs +++ b/src/rules/keyframes.rs @@ -15,6 +15,7 @@ use crate::traits::{Parse, ToCss}; use crate::values::color::ColorFallbackKind; use crate::values::ident::CustomIdent; use crate::values::percentage::Percentage; +use crate::values::string::CowArcStr; use crate::vendor_prefix::VendorPrefix; use cssparser::*; @@ -23,8 +24,9 @@ use cssparser::*; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct KeyframesRule<'i> { /// The animation name. + /// = | #[cfg_attr(feature = "serde", serde(borrow))] - pub name: CustomIdent<'i>, + pub name: KeyframesName<'i>, /// A list of keyframes in the animation. pub keyframes: Vec>, /// A vendor prefix for the rule, e.g. `@-webkit-keyframes`. @@ -33,6 +35,65 @@ pub struct KeyframesRule<'i> { pub loc: Location, } +/// KeyframesName +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum KeyframesName<'i> { + /// `` of a `@keyframes` name. + #[cfg_attr(feature = "serde", serde(borrow))] + Ident(CustomIdent<'i>), + + /// `` of a `@keyframes` name. + #[cfg_attr(feature = "serde", serde(borrow))] + Custom(CowArcStr<'i>), +} + +impl<'i> Parse<'i> for KeyframesName<'i> { + fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { + match input.next()?.clone() { + Token::Ident(ref s) => { + // CSS-wide keywords without quotes throws an error. + match_ignore_ascii_case! { &*s, + "none" | "initial" | "inherit" | "unset" | "default" | "revert" | "revert-layer" => { + Err(input.new_unexpected_token_error(Token::Ident(s.clone()))) + }, + _ => { + Ok(KeyframesName::Ident(CustomIdent(s.into()))) + } + } + } + + Token::QuotedString(ref s) => Ok(KeyframesName::Custom(s.into())), + t => return Err(input.new_unexpected_token_error(t.clone())), + } + } +} + +impl<'i> ToCss for KeyframesName<'i> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + match self { + KeyframesName::Ident(ident) => { + dest.write_ident(ident.0.as_ref())?; + } + KeyframesName::Custom(s) => { + // CSS-wide keywords and `none` cannot remove quotes. + match_ignore_ascii_case! { &*s, + "none" | "initial" | "inherit" | "unset" | "default" | "revert" | "revert-layer" => { + serialize_string(&s, dest)?; + }, + _ => { + dest.write_ident(s.as_ref())?; + } + } + } + } + Ok(()) + } +} + impl<'i> KeyframesRule<'i> { pub(crate) fn minify(&mut self, context: &mut MinifyContext<'_, 'i>) { context.handler_context.context = DeclarationContext::Keyframes; diff --git a/src/rules/mod.rs b/src/rules/mod.rs index eeab3354..39aec9a8 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -85,6 +85,7 @@ use nesting::NestingRule; use page::PageRule; use serde::Serialize; use std::collections::{HashMap, HashSet}; +use std::default::Default; use style::StyleRule; use supports::SupportsRule; use unknown::UnknownAtRule; @@ -259,7 +260,10 @@ impl<'i> CssRuleList<'i> { for mut rule in self.0.drain(..) { match &mut rule { CssRule::Keyframes(keyframes) => { - if context.unused_symbols.contains(keyframes.name.0.as_ref()) { + if context + .unused_symbols + .contains(&keyframes.name.to_css_string(Default::default()).unwrap()) + { continue; } keyframes.minify(context);