Skip to main content

oxc_css_parser/parser/at_rule/
keyframes.rs

1use super::Parser;
2use crate::{
3    Parse,
4    ast::*,
5    error::{Error, ErrorKind, PResult},
6    parser::state::ParserState,
7    pos::Span,
8    tokenizer::Token,
9    util,
10};
11
12// https://drafts.csswg.org/css-animations/#keyframes
13//
14// <keyframe-block> = <keyframe-selector># { <declaration-list> }
15impl<'a> Parse<'a> for KeyframeBlock<'a> {
16    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
17        let first_selector = input.parse::<KeyframeSelector>()?;
18        let start = first_selector.span().start;
19
20        let mut selectors = input.vec_with_capacity(2);
21        selectors.push(first_selector);
22        let mut comma_spans = input.vec();
23        while let Some((_, comma_span)) = input.cursor.eat_comma()? {
24            comma_spans.push(comma_span);
25            selectors.push(input.parse()?);
26        }
27        debug_assert_eq!(comma_spans.len() + 1, selectors.len());
28
29        let block = input
30            .with_state(ParserState { in_keyframes_at_rule: false, ..input.state.clone() })
31            .parse::<SimpleBlock>()?;
32
33        let span = Span { start, end: block.span.end };
34        Ok(KeyframeBlock { selectors, comma_spans, block, span })
35    }
36}
37
38// https://drafts.csswg.org/scroll-animations-1/#named-ranges
39fn is_timeline_range_name(name: &str) -> bool {
40    name.eq_ignore_ascii_case("cover")
41        || name.eq_ignore_ascii_case("contain")
42        || name.eq_ignore_ascii_case("entry")
43        || name.eq_ignore_ascii_case("exit")
44        || name.eq_ignore_ascii_case("entry-crossing")
45        || name.eq_ignore_ascii_case("exit-crossing")
46}
47
48// <keyframe-selector> = from | to | <percentage [0,100]>
49//                     | <timeline-range-name> <percentage [0,100]>
50impl<'a> Parse<'a> for KeyframeSelector<'a> {
51    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
52        match &input.cursor.peek()?.token {
53            Token::Percentage(..) => Ok(KeyframeSelector::Percentage(input.parse()?)),
54            _ => {
55                let ident = input.parse::<InterpolableIdent>()?;
56                // Scroll-driven animations attach keyframes to a named
57                // timeline range: `entry 0%`, `exit-crossing 100%`.
58                if let Token::Percentage(..) = &input.cursor.peek()?.token {
59                    if let InterpolableIdent::Literal(ident) = &ident
60                        && !is_timeline_range_name(ident.name)
61                    {
62                        input.recoverable_errors.push(Error {
63                            kind: ErrorKind::UnknownKeyframeSelectorIdent,
64                            span: ident.span,
65                        });
66                    }
67                    let percentage = input.parse::<Percentage>()?;
68                    let span = Span { start: ident.span().start, end: percentage.span.end };
69                    return Ok(KeyframeSelector::TimelineRange(KeyframeTimelineRange {
70                        name: ident,
71                        percentage,
72                        span,
73                    }));
74                }
75                if let InterpolableIdent::Literal(ident) = &ident
76                    && !ident.name.eq_ignore_ascii_case("from")
77                    && !ident.name.eq_ignore_ascii_case("to")
78                {
79                    input.recoverable_errors.push(Error {
80                        kind: ErrorKind::UnknownKeyframeSelectorIdent,
81                        span: ident.span,
82                    });
83                }
84                Ok(KeyframeSelector::Ident(ident))
85            }
86        }
87    }
88}
89
90// https://drafts.csswg.org/css-animations/#keyframes
91//
92// @keyframes <keyframes-name> { <rule-list> }
93// <keyframes-name> = <custom-ident> | <string>   (not a CSS-wide keyword)
94impl<'a> Parse<'a> for KeyframesName<'a> {
95    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
96        match &input.cursor.peek()?.token {
97            Token::Ident(..) | Token::HashLBrace(..) | Token::AtLBraceVar(..) => {
98                let ident = input.parse()?;
99                if let InterpolableIdent::Literal(ident) = &ident
100                    && (util::is_css_wide_keyword(ident.name)
101                        || ident.name.eq_ignore_ascii_case("default"))
102                {
103                    input.recoverable_errors.push(Error {
104                        kind: ErrorKind::CSSWideKeywordDisallowed,
105                        span: ident.span,
106                    });
107                }
108                Ok(KeyframesName::Ident(ident))
109            }
110            Token::AtKeyword(..) => input.parse().map(KeyframesName::LessVariable),
111            Token::Tilde(..) => input.parse().map(KeyframesName::LessEscapedStr),
112            _ => input.parse().map(KeyframesName::Str),
113        }
114    }
115}