lightningcss/rules/
keyframes.rs

1//! The `@keyframes` rule.
2
3use super::supports::SupportsRule;
4use super::MinifyContext;
5use super::{CssRule, CssRuleList, Location};
6use crate::context::DeclarationContext;
7use crate::declaration::DeclarationBlock;
8use crate::error::{ParserError, PrinterError};
9use crate::parser::ParserOptions;
10use crate::printer::Printer;
11use crate::properties::animation::TimelineRangeName;
12use crate::properties::custom::{CustomProperty, UnparsedProperty};
13use crate::properties::Property;
14use crate::targets::Targets;
15use crate::traits::{Parse, ToCss};
16use crate::values::color::ColorFallbackKind;
17use crate::values::ident::CustomIdent;
18use crate::values::percentage::Percentage;
19use crate::values::string::CowArcStr;
20use crate::vendor_prefix::VendorPrefix;
21#[cfg(feature = "visitor")]
22use crate::visitor::Visit;
23use cssparser::*;
24
25/// A [@keyframes](https://drafts.csswg.org/css-animations/#keyframes) rule.
26#[derive(Debug, PartialEq, Clone)]
27#[cfg_attr(feature = "visitor", derive(Visit))]
28#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
29#[cfg_attr(
30  feature = "serde",
31  derive(serde::Serialize, serde::Deserialize),
32  serde(rename_all = "camelCase")
33)]
34#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
35pub struct KeyframesRule<'i> {
36  /// The animation name.
37  /// <keyframes-name> = <custom-ident> | <string>
38  #[cfg_attr(feature = "serde", serde(borrow))]
39  pub name: KeyframesName<'i>,
40  /// A list of keyframes in the animation.
41  pub keyframes: Vec<Keyframe<'i>>,
42  /// A vendor prefix for the rule, e.g. `@-webkit-keyframes`.
43  #[cfg_attr(feature = "visitor", skip_visit)]
44  pub vendor_prefix: VendorPrefix,
45  /// The location of the rule in the source file.
46  #[cfg_attr(feature = "visitor", skip_visit)]
47  pub loc: Location,
48}
49
50/// KeyframesName
51#[derive(Debug, Clone, PartialEq, Eq, Hash)]
52#[cfg_attr(feature = "visitor", derive(Visit))]
53#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
54#[cfg_attr(
55  feature = "serde",
56  derive(serde::Serialize, serde::Deserialize),
57  serde(tag = "type", content = "value", rename_all = "kebab-case")
58)]
59#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
60pub enum KeyframesName<'i> {
61  /// `<custom-ident>` of a `@keyframes` name.
62  #[cfg_attr(feature = "serde", serde(borrow))]
63  Ident(CustomIdent<'i>),
64
65  /// `<string>` of a `@keyframes` name.
66  #[cfg_attr(feature = "serde", serde(borrow))]
67  Custom(CowArcStr<'i>),
68}
69
70impl<'i> Parse<'i> for KeyframesName<'i> {
71  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
72    match input.next()?.clone() {
73      Token::Ident(ref s) => {
74        // CSS-wide keywords without quotes throws an error.
75        match_ignore_ascii_case! { &*s,
76          "none" | "initial" | "inherit" | "unset" | "default" | "revert" | "revert-layer" => {
77            Err(input.new_unexpected_token_error(Token::Ident(s.clone())))
78          },
79          _ => {
80            Ok(KeyframesName::Ident(CustomIdent(s.into())))
81          }
82        }
83      }
84
85      Token::QuotedString(ref s) => Ok(KeyframesName::Custom(s.into())),
86      t => return Err(input.new_unexpected_token_error(t.clone())),
87    }
88  }
89}
90
91impl<'i> ToCss for KeyframesName<'i> {
92  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
93  where
94    W: std::fmt::Write,
95  {
96    let css_module_animation_enabled =
97      dest.css_module.as_ref().map_or(false, |css_module| css_module.config.animation);
98
99    match self {
100      KeyframesName::Ident(ident) => {
101        dest.write_ident(ident.0.as_ref(), css_module_animation_enabled)?;
102      }
103      KeyframesName::Custom(s) => {
104        // CSS-wide keywords and `none` cannot remove quotes.
105        match_ignore_ascii_case! { &*s,
106          "none" | "initial" | "inherit" | "unset" | "default" | "revert" | "revert-layer" => {
107            serialize_string(&s, dest)?;
108          },
109          _ => {
110            dest.write_ident(s.as_ref(), css_module_animation_enabled)?;
111          }
112        }
113      }
114    }
115    Ok(())
116  }
117}
118
119impl<'i> KeyframesRule<'i> {
120  pub(crate) fn minify(&mut self, context: &mut MinifyContext<'_, 'i>) {
121    context.handler_context.context = DeclarationContext::Keyframes;
122
123    for keyframe in &mut self.keyframes {
124      keyframe
125        .declarations
126        .minify(context.handler, context.important_handler, &mut context.handler_context)
127    }
128
129    context.handler_context.context = DeclarationContext::None;
130  }
131
132  pub(crate) fn get_fallbacks<T>(&mut self, targets: &Targets) -> Vec<CssRule<'i, T>> {
133    let mut fallbacks = ColorFallbackKind::empty();
134    for keyframe in &self.keyframes {
135      for property in &keyframe.declarations.declarations {
136        match property {
137          Property::Custom(CustomProperty { value, .. }) | Property::Unparsed(UnparsedProperty { value, .. }) => {
138            fallbacks |= value.get_necessary_fallbacks(*targets);
139          }
140          _ => {}
141        }
142      }
143    }
144
145    let mut res = Vec::new();
146    let lowest_fallback = fallbacks.lowest();
147    fallbacks.remove(lowest_fallback);
148
149    if fallbacks.contains(ColorFallbackKind::P3) {
150      res.push(self.get_fallback(ColorFallbackKind::P3));
151    }
152
153    if fallbacks.contains(ColorFallbackKind::LAB)
154      || (!lowest_fallback.is_empty() && lowest_fallback != ColorFallbackKind::LAB)
155    {
156      res.push(self.get_fallback(ColorFallbackKind::LAB));
157    }
158
159    if !lowest_fallback.is_empty() {
160      for keyframe in &mut self.keyframes {
161        for property in &mut keyframe.declarations.declarations {
162          match property {
163            Property::Custom(CustomProperty { value, .. })
164            | Property::Unparsed(UnparsedProperty { value, .. }) => {
165              *value = value.get_fallback(lowest_fallback);
166            }
167            _ => {}
168          }
169        }
170      }
171    }
172
173    res
174  }
175
176  fn get_fallback<T>(&self, kind: ColorFallbackKind) -> CssRule<'i, T> {
177    let keyframes = self
178      .keyframes
179      .iter()
180      .map(|keyframe| Keyframe {
181        selectors: keyframe.selectors.clone(),
182        declarations: DeclarationBlock {
183          important_declarations: vec![],
184          declarations: keyframe
185            .declarations
186            .declarations
187            .iter()
188            .map(|property| match property {
189              Property::Custom(custom) => Property::Custom(CustomProperty {
190                name: custom.name.clone(),
191                value: custom.value.get_fallback(kind),
192              }),
193              Property::Unparsed(unparsed) => Property::Unparsed(UnparsedProperty {
194                property_id: unparsed.property_id.clone(),
195                value: unparsed.value.get_fallback(kind),
196              }),
197              _ => property.clone(),
198            })
199            .collect(),
200        },
201      })
202      .collect();
203
204    CssRule::Supports(SupportsRule {
205      condition: kind.supports_condition(),
206      rules: CssRuleList(vec![CssRule::Keyframes(KeyframesRule {
207        name: self.name.clone(),
208        keyframes,
209        vendor_prefix: self.vendor_prefix,
210        loc: self.loc.clone(),
211      })]),
212      loc: self.loc.clone(),
213    })
214  }
215}
216
217impl<'i> ToCss for KeyframesRule<'i> {
218  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
219  where
220    W: std::fmt::Write,
221  {
222    #[cfg(feature = "sourcemap")]
223    dest.add_mapping(self.loc);
224    let mut first_rule = true;
225    macro_rules! write_prefix {
226      ($prefix: ident) => {
227        if self.vendor_prefix.contains(VendorPrefix::$prefix) {
228          #[allow(unused_assignments)]
229          if first_rule {
230            first_rule = false;
231          } else {
232            if !dest.minify {
233              dest.write_char('\n')?; // no indent
234            }
235            dest.newline()?;
236          }
237          dest.write_char('@')?;
238          VendorPrefix::$prefix.to_css(dest)?;
239          dest.write_str("keyframes ")?;
240          self.name.to_css(dest)?;
241          dest.whitespace()?;
242          dest.write_char('{')?;
243          dest.indent();
244          let mut first = true;
245          for keyframe in &self.keyframes {
246            if first {
247              first = false;
248            } else if !dest.minify {
249              dest.write_char('\n')?; // no indent
250            }
251            dest.newline()?;
252            keyframe.to_css(dest)?;
253          }
254          dest.dedent();
255          dest.newline()?;
256          dest.write_char('}')?;
257        }
258      };
259    }
260
261    write_prefix!(WebKit);
262    write_prefix!(Moz);
263    write_prefix!(O);
264    write_prefix!(None);
265    Ok(())
266  }
267}
268
269/// A percentage of a given timeline range
270#[derive(Debug, PartialEq, Clone)]
271#[cfg_attr(feature = "visitor", derive(Visit))]
272#[cfg_attr(
273  feature = "serde",
274  derive(serde::Serialize, serde::Deserialize),
275  serde(tag = "type", rename_all = "camelCase")
276)]
277#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
278#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
279pub struct TimelineRangePercentage {
280  /// The name of the timeline range.
281  name: TimelineRangeName,
282  /// The percentage progress between the start and end of the range.
283  percentage: Percentage,
284}
285
286impl<'i> Parse<'i> for TimelineRangePercentage {
287  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
288    let name = TimelineRangeName::parse(input)?;
289    let percentage = Percentage::parse(input)?;
290    Ok(TimelineRangePercentage { name, percentage })
291  }
292}
293
294/// A [keyframe selector](https://drafts.csswg.org/css-animations/#typedef-keyframe-selector)
295/// within an `@keyframes` rule.
296#[derive(Debug, PartialEq, Clone, Parse)]
297#[cfg_attr(feature = "visitor", derive(Visit))]
298#[cfg_attr(
299  feature = "serde",
300  derive(serde::Serialize, serde::Deserialize),
301  serde(tag = "type", content = "value", rename_all = "kebab-case")
302)]
303#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
304#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
305pub enum KeyframeSelector {
306  /// An explicit percentage.
307  Percentage(Percentage),
308  /// The `from` keyword. Equivalent to 0%.
309  From,
310  /// The `to` keyword. Equivalent to 100%.
311  To,
312  /// A [named timeline range selector](https://drafts.csswg.org/scroll-animations-1/#named-range-keyframes)
313  TimelineRangePercentage(TimelineRangePercentage),
314}
315
316impl ToCss for KeyframeSelector {
317  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
318  where
319    W: std::fmt::Write,
320  {
321    match self {
322      KeyframeSelector::Percentage(p) => {
323        if dest.minify && *p == Percentage(1.0) {
324          dest.write_str("to")
325        } else {
326          p.to_css(dest)
327        }
328      }
329      KeyframeSelector::From => {
330        if dest.minify {
331          dest.write_str("0%")
332        } else {
333          dest.write_str("from")
334        }
335      }
336      KeyframeSelector::To => dest.write_str("to"),
337      KeyframeSelector::TimelineRangePercentage(TimelineRangePercentage {
338        name: timeline_range_name,
339        percentage,
340      }) => {
341        timeline_range_name.to_css(dest)?;
342        dest.write_char(' ')?;
343        percentage.to_css(dest)
344      }
345    }
346  }
347}
348
349/// An individual keyframe within an `@keyframes` rule.
350///
351/// See [KeyframesRule](KeyframesRule).
352#[derive(Debug, PartialEq, Clone)]
353#[cfg_attr(feature = "visitor", derive(Visit))]
354#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
355#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
356#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
357pub struct Keyframe<'i> {
358  /// A list of keyframe selectors to associate with the declarations in this keyframe.
359  pub selectors: Vec<KeyframeSelector>,
360  /// The declarations for this keyframe.
361  #[cfg_attr(feature = "serde", serde(borrow))]
362  pub declarations: DeclarationBlock<'i>,
363}
364
365impl<'i> ToCss for Keyframe<'i> {
366  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
367  where
368    W: std::fmt::Write,
369  {
370    let mut first = true;
371    for selector in &self.selectors {
372      if !first {
373        dest.delim(',', false)?;
374      }
375      first = false;
376      selector.to_css(dest)?;
377    }
378
379    self.declarations.to_css_block(dest)
380  }
381}
382
383pub(crate) struct KeyframeListParser;
384
385impl<'a, 'i> AtRuleParser<'i> for KeyframeListParser {
386  type Prelude = ();
387  type AtRule = Keyframe<'i>;
388  type Error = ParserError<'i>;
389}
390
391impl<'a, 'i> QualifiedRuleParser<'i> for KeyframeListParser {
392  type Prelude = Vec<KeyframeSelector>;
393  type QualifiedRule = Keyframe<'i>;
394  type Error = ParserError<'i>;
395
396  fn parse_prelude<'t>(
397    &mut self,
398    input: &mut Parser<'i, 't>,
399  ) -> Result<Self::Prelude, ParseError<'i, ParserError<'i>>> {
400    input.parse_comma_separated(KeyframeSelector::parse)
401  }
402
403  fn parse_block<'t>(
404    &mut self,
405    selectors: Self::Prelude,
406    _: &ParserState,
407    input: &mut Parser<'i, 't>,
408  ) -> Result<Self::QualifiedRule, ParseError<'i, ParserError<'i>>> {
409    // For now there are no options that apply within @keyframes
410    let options = ParserOptions::default();
411    Ok(Keyframe {
412      selectors,
413      declarations: DeclarationBlock::parse(input, &options)?,
414    })
415  }
416}
417
418impl<'i> DeclarationParser<'i> for KeyframeListParser {
419  type Declaration = Keyframe<'i>;
420  type Error = ParserError<'i>;
421}
422
423impl<'i> RuleBodyItemParser<'i, Keyframe<'i>, ParserError<'i>> for KeyframeListParser {
424  fn parse_qualified(&self) -> bool {
425    true
426  }
427
428  fn parse_declarations(&self) -> bool {
429    false
430  }
431}