Skip to main content

oxc_css_parser/parser/at_rule/
mod.rs

1use super::{Parser, state::ParserState};
2use crate::{
3    Parse, Syntax, arena_box,
4    ast::*,
5    bump,
6    error::{Error, ErrorKind, PResult},
7    expect, peek,
8    pos::{Span, Spanned},
9    tokenizer::Token,
10};
11
12mod color_profile;
13mod container;
14mod counter_style;
15mod custom_media;
16mod custom_selector;
17mod document;
18mod font_feature_values;
19mod import;
20mod keyframes;
21mod layer;
22mod media;
23mod namespace;
24mod page;
25mod scope;
26mod supports;
27
28impl<'a> Parse<'a> for AtRule<'a> {
29    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
30        let (at_keyword, at_keyword_span) = expect!(input, AtKeyword);
31
32        let at_rule_name = at_keyword.ident.name();
33        let (prelude, block, end) = if at_rule_name.eq_ignore_ascii_case("media") {
34            let prelude = input.try_parse(MediaQueryList::parse).ok().map(AtRulePrelude::Media);
35            let block = input.parse::<SimpleBlock>()?;
36            let end = block.span.end;
37            (prelude, Some(block), end)
38        } else if at_rule_name.eq_ignore_ascii_case("keyframes")
39            || at_rule_name.eq_ignore_ascii_case("-webkit-keyframes")
40            || at_rule_name.eq_ignore_ascii_case("-moz-keyframes")
41            || at_rule_name.eq_ignore_ascii_case("-ms-keyframes")
42            || at_rule_name.eq_ignore_ascii_case("-o-keyframes")
43        {
44            // A nameless `@keyframes {}` is invalid CSS, but Sass parses it
45            // (the name may be produced elsewhere, e.g. a keyframes mixin
46            // emitting vendor-prefixed blocks around `@content`).
47            let prelude = match &peek!(input).token {
48                Token::LBrace(..) => None,
49                _ => Some(AtRulePrelude::Keyframes(input.parse()?)),
50            };
51            let block = input
52                .with_state(ParserState { in_keyframes_at_rule: true, ..input.state.clone() })
53                .parse::<SimpleBlock>()?;
54            let end = block.span.end;
55            (prelude, Some(block), end)
56        } else if at_rule_name.eq_ignore_ascii_case("import") {
57            let (end, prelude) = match input.syntax {
58                Syntax::Css => {
59                    let prelude = input.parse::<ImportPrelude>()?;
60                    (prelude.span.end, AtRulePrelude::Import(arena_box!(input, prelude)))
61                }
62                Syntax::Scss | Syntax::Sass => {
63                    if let Ok(prelude) = input.try_parse(ImportPrelude::parse) {
64                        (prelude.span.end, AtRulePrelude::Import(arena_box!(input, prelude)))
65                    } else {
66                        let prelude = input.parse::<SassImportPrelude>()?;
67                        (prelude.span.end, AtRulePrelude::SassImport(prelude))
68                    }
69                }
70                Syntax::Less => {
71                    if let Ok(prelude) = input.try_parse(ImportPrelude::parse) {
72                        (prelude.span.end, AtRulePrelude::Import(arena_box!(input, prelude)))
73                    } else {
74                        let prelude = input.parse::<LessImportPrelude>()?;
75                        (prelude.span.end, AtRulePrelude::LessImport(arena_box!(input, prelude)))
76                    }
77                }
78            };
79            (Some(prelude), None, end)
80        } else if at_rule_name.eq_ignore_ascii_case("charset") {
81            // https://drafts.csswg.org/css2/#charset%E2%91%A0
82            let prelude = input.parse::<Str>()?;
83            let end = prelude.span.end;
84            (Some(AtRulePrelude::Charset(prelude)), None, end)
85        } else if at_rule_name.eq_ignore_ascii_case("font-face") {
86            let block = input.parse::<SimpleBlock>()?;
87            let end = block.span.end;
88            (None, Some(block), end)
89        } else if at_rule_name.eq_ignore_ascii_case("supports") {
90            let prelude = Some(AtRulePrelude::Supports(input.parse()?));
91            let block = input.parse::<SimpleBlock>()?;
92            let end = block.span.end;
93            (prelude, Some(block), end)
94        } else if at_rule_name.eq_ignore_ascii_case("layer") {
95            let prelude = input.try_parse(LayerNames::parse).ok();
96            let block = if matches!(peek!(input).token, Token::LBrace(..) | Token::Indent(..)) {
97                Some(input.parse::<SimpleBlock>()?)
98            } else {
99                None
100            };
101            if let Some(block) = &block
102                && prelude.as_ref().is_some_and(|prelude| prelude.names.len() > 1)
103            {
104                input.recoverable_errors.push(Error {
105                    kind: ErrorKind::UnexpectedSimpleBlock,
106                    span: block.span.clone(),
107                });
108            }
109            let end = block
110                .as_ref()
111                .map(|block| block.span.end)
112                .or_else(|| prelude.as_ref().map(|prelude| prelude.span.end))
113                .unwrap_or(at_keyword_span.end);
114            (prelude.map(AtRulePrelude::Layer), block, end)
115        } else if at_rule_name.eq_ignore_ascii_case("container") {
116            let prelude = Some(AtRulePrelude::Container(input.parse()?));
117            let block = input.parse::<SimpleBlock>()?;
118            let end = block.span.end;
119            (prelude, Some(block), end)
120        } else if at_rule_name.eq_ignore_ascii_case("page") {
121            let prelude = input.try_parse(PageSelectorList::parse).map(AtRulePrelude::Page).ok();
122            let block = input.try_parse(SimpleBlock::parse).ok();
123            let end = block
124                .as_ref()
125                .map(|block| block.span.end)
126                .or_else(|| prelude.as_ref().map(|prelude| prelude.span().end))
127                .unwrap_or(at_keyword_span.end);
128            (prelude, block, end)
129        } else if at_rule_name.eq_ignore_ascii_case("namespace") {
130            let namespace = input.parse::<NamespacePrelude>()?;
131            let end = namespace.span.end;
132            (Some(AtRulePrelude::Namespace(arena_box!(input, namespace))), None, end)
133        } else if at_rule_name.eq_ignore_ascii_case("color-profile") {
134            let prelude = Some(AtRulePrelude::ColorProfile(input.parse()?));
135            let block = input.parse::<SimpleBlock>()?;
136            let end = block.span.end;
137            (prelude, Some(block), end)
138        } else if at_rule_name.eq_ignore_ascii_case("font-feature-values") {
139            let prelude = Some(AtRulePrelude::FontFeatureValues(input.parse()?));
140            let block = input.parse::<SimpleBlock>()?;
141            let end = block.span.end;
142            (prelude, Some(block), end)
143        } else if at_rule_name.eq_ignore_ascii_case("font-palette-values") {
144            // https://drafts.csswg.org/css-fonts/Overview.bs
145            let prelude = Some(AtRulePrelude::FontPaletteValues(input.parse_dashed_ident()?));
146            let block = input.parse::<SimpleBlock>()?;
147            let end = block.span.end;
148            (prelude, Some(block), end)
149        } else if at_rule_name.eq_ignore_ascii_case("counter-style") {
150            let prelude = Some(AtRulePrelude::CounterStyle(input.parse_counter_style_prelude()?));
151            let block = input.parse::<SimpleBlock>()?;
152            let end = block.span.end;
153            (prelude, Some(block), end)
154        } else if at_rule_name.eq_ignore_ascii_case("custom-media") {
155            let custom_media = input.parse::<CustomMedia>()?;
156            let end = custom_media.span.end;
157            (Some(AtRulePrelude::CustomMedia(arena_box!(input, custom_media))), None, end)
158        } else if at_rule_name.eq_ignore_ascii_case("custom-selector") {
159            let custom_selector_prelude = input.parse::<CustomSelectorPrelude>()?;
160            let end = custom_selector_prelude.span.end;
161            (
162                Some(AtRulePrelude::CustomSelector(arena_box!(input, custom_selector_prelude))),
163                None,
164                end,
165            )
166        } else if at_rule_name.eq_ignore_ascii_case("position-try") {
167            // https://drafts.csswg.org/css-anchor-position-1/#fallback-rule
168            let prelude = Some(AtRulePrelude::PositionTry(input.parse_dashed_ident()?));
169            let block = input.parse::<SimpleBlock>()?;
170            let end = block.span.end;
171            (prelude, Some(block), end)
172        } else if at_rule_name.eq_ignore_ascii_case("nest") {
173            // https://www.w3.org/TR/css-nesting-1/#at-nest
174            let prelude = Some(AtRulePrelude::Nest(input.parse()?));
175            let block = input.parse::<SimpleBlock>()?;
176            let end = block.span.end;
177            (prelude, Some(block), end)
178        } else if at_rule_name.eq_ignore_ascii_case("property") {
179            // https://drafts.css-houdini.org/css-properties-values-api/#at-property-rule
180            let prelude = Some(AtRulePrelude::Property(input.parse_dashed_ident()?));
181            let block = input.parse::<SimpleBlock>()?;
182            let end = block.span.end;
183            (prelude, Some(block), end)
184        } else if at_rule_name.eq_ignore_ascii_case("scope") {
185            let prelude = if let Token::LParen(..) | Token::Ident(..) = peek!(input).token {
186                Some(AtRulePrelude::Scope(arena_box!(input, input.parse()?)))
187            } else {
188                None
189            };
190            let block = input.parse::<SimpleBlock>()?;
191            let end = block.span.end;
192            (prelude, Some(block), end)
193        } else if at_rule_name.eq_ignore_ascii_case("document")
194            || at_rule_name.eq_ignore_ascii_case("-moz-document")
195        {
196            let prelude = Some(AtRulePrelude::Document(input.parse()?));
197            let block = input.parse::<SimpleBlock>()?;
198            let end = block.span.end;
199            (prelude, Some(block), end)
200        } else if at_rule_name.eq_ignore_ascii_case("stylistic")
201            || at_rule_name.eq_ignore_ascii_case("historical-forms")
202            || at_rule_name.eq_ignore_ascii_case("styleset")
203            || at_rule_name.eq_ignore_ascii_case("character-variant")
204            || at_rule_name.eq_ignore_ascii_case("swash")
205            || at_rule_name.eq_ignore_ascii_case("ornaments")
206            || at_rule_name.eq_ignore_ascii_case("annotation")
207            || at_rule_name.eq_ignore_ascii_case("top-left-corner")
208            || at_rule_name.eq_ignore_ascii_case("top-left")
209            || at_rule_name.eq_ignore_ascii_case("top-center")
210            || at_rule_name.eq_ignore_ascii_case("top-right")
211            || at_rule_name.eq_ignore_ascii_case("top-right-corner")
212            || at_rule_name.eq_ignore_ascii_case("bottom-left-corner")
213            || at_rule_name.eq_ignore_ascii_case("bottom-left")
214            || at_rule_name.eq_ignore_ascii_case("bottom-center")
215            || at_rule_name.eq_ignore_ascii_case("bottom-right")
216            || at_rule_name.eq_ignore_ascii_case("bottom-right-corner")
217            || at_rule_name.eq_ignore_ascii_case("left-top")
218            || at_rule_name.eq_ignore_ascii_case("left-middle")
219            || at_rule_name.eq_ignore_ascii_case("left-bottom")
220            || at_rule_name.eq_ignore_ascii_case("right-top")
221            || at_rule_name.eq_ignore_ascii_case("right-middle")
222            || at_rule_name.eq_ignore_ascii_case("right-bottom")
223            || at_rule_name.eq_ignore_ascii_case("viewport")
224            || at_rule_name.eq_ignore_ascii_case("try")
225            || at_rule_name.eq_ignore_ascii_case("starting-style")
226        {
227            let block = input.parse::<SimpleBlock>()?;
228            let end = block.span.end;
229            (None, Some(block), end)
230        } else if at_rule_name == "plugin" && input.syntax == Syntax::Less {
231            let prelude = input.parse::<LessPlugin>()?;
232            let end = prelude.span.end;
233            (Some(AtRulePrelude::LessPlugin(arena_box!(input, prelude))), None, end)
234        } else if matches!(input.syntax, Syntax::Scss | Syntax::Sass) {
235            use super::state::{
236                SASS_CTX_ALLOW_DIV, SASS_CTX_ALLOW_KEYFRAME_BLOCK, SASS_CTX_IN_FUNCTION,
237            };
238            match &*at_rule_name {
239                "each" => {
240                    let prelude = input.parse()?;
241                    let block = input.parse::<SimpleBlock>()?;
242                    let end = block.span.end;
243                    (Some(AtRulePrelude::SassEach(arena_box!(input, prelude))), Some(block), end)
244                }
245                "while" => {
246                    let prelude = input.parse()?;
247                    let block = input.parse::<SimpleBlock>()?;
248                    let end = block.span.end;
249                    (Some(AtRulePrelude::SassExpr(arena_box!(input, prelude))), Some(block), end)
250                }
251                "for" => {
252                    let prelude = input.parse()?;
253                    let block = input.parse::<SimpleBlock>()?;
254                    let end = block.span.end;
255                    (Some(AtRulePrelude::SassFor(arena_box!(input, prelude))), Some(block), end)
256                }
257                "mixin" => {
258                    let prelude = input.parse()?;
259                    let block = input
260                        .with_state(ParserState {
261                            sass_ctx: input.state.sass_ctx | SASS_CTX_ALLOW_KEYFRAME_BLOCK,
262                            ..input.state.clone()
263                        })
264                        .parse::<SimpleBlock>()?;
265                    let end = block.span.end;
266                    (Some(AtRulePrelude::SassMixin(arena_box!(input, prelude))), Some(block), end)
267                }
268                "include" => {
269                    let prelude = input.parse::<SassInclude>()?;
270                    let block =
271                        if matches!(peek!(input).token, Token::LBrace(..) | Token::Indent(..)) {
272                            Some(
273                                input
274                                    .with_state(ParserState {
275                                        sass_ctx: input.state.sass_ctx
276                                            | SASS_CTX_ALLOW_KEYFRAME_BLOCK,
277                                        ..input.state.clone()
278                                    })
279                                    .parse::<SimpleBlock>()?,
280                            )
281                        } else {
282                            None
283                        };
284                    let end =
285                        block.as_ref().map(|block| block.span.end).unwrap_or(prelude.span.end);
286                    (Some(AtRulePrelude::SassInclude(arena_box!(input, prelude))), block, end)
287                }
288                "content" => {
289                    if matches!(peek!(input).token, Token::LParen(..)) {
290                        let prelude = input.parse::<SassContent>()?;
291                        let end = prelude.span.end;
292                        (Some(AtRulePrelude::SassContent(prelude)), None, end)
293                    } else {
294                        (None, None, input.tokenizer.current_offset())
295                    }
296                }
297                "use" => {
298                    let prelude = input.parse::<SassUse>()?;
299                    let end = prelude.span.end;
300                    (Some(AtRulePrelude::SassUse(arena_box!(input, prelude))), None, end)
301                }
302                "function" => {
303                    let prelude = input.parse::<SassFunction>()?;
304                    let block = input
305                        .with_state(ParserState {
306                            sass_ctx: input.state.sass_ctx | SASS_CTX_IN_FUNCTION,
307                            ..input.state.clone()
308                        })
309                        .parse::<SimpleBlock>()?;
310                    let end = block.span.end;
311                    (
312                        Some(AtRulePrelude::SassFunction(arena_box!(input, prelude))),
313                        Some(block),
314                        end,
315                    )
316                }
317                "return" => {
318                    let expr = input
319                        .with_state(ParserState {
320                            sass_ctx: input.state.sass_ctx | SASS_CTX_ALLOW_DIV,
321                            ..input.state.clone()
322                        })
323                        .parse_maybe_sass_list(/* allow_comma */ true)?;
324                    let end = expr.span().end;
325                    if input.state.sass_ctx & SASS_CTX_IN_FUNCTION == 0 {
326                        input.recoverable_errors.push(Error {
327                            kind: ErrorKind::ReturnOutsideFunction,
328                            span: Span { start: at_keyword_span.start, end },
329                        });
330                    }
331                    (Some(AtRulePrelude::SassExpr(arena_box!(input, expr))), None, end)
332                }
333                "extend" => {
334                    let prelude = input.parse::<SassExtend>()?;
335                    let end = prelude.span.end;
336                    (Some(AtRulePrelude::SassExtend(arena_box!(input, prelude))), None, end)
337                }
338                "warn" | "error" | "debug" => {
339                    let expr = input.parse_maybe_sass_list(/* allow_comma */ true)?;
340                    let end = expr.span().end;
341                    (Some(AtRulePrelude::SassExpr(arena_box!(input, expr))), None, end)
342                }
343                "forward" => {
344                    let prelude = input.parse::<SassForward>()?;
345                    let end = prelude.span.end;
346                    (Some(AtRulePrelude::SassForward(arena_box!(input, prelude))), None, end)
347                }
348                "at-root" => {
349                    let prelude =
350                        if !matches!(peek!(input).token, Token::LBrace(..) | Token::Indent(..)) {
351                            Some(AtRulePrelude::SassAtRoot(input.parse()?))
352                        } else {
353                            None
354                        };
355                    let block = input.parse::<SimpleBlock>()?;
356                    let end = block.span.end;
357                    (prelude, Some(block), end)
358                }
359                _ => {
360                    let (prelude, block, end) = input.parse_unknown_at_rule()?;
361                    (
362                        prelude.map(|prelude| AtRulePrelude::Unknown(arena_box!(input, prelude))),
363                        block,
364                        end.unwrap_or(at_keyword_span.end),
365                    )
366                }
367            }
368        } else {
369            let (prelude, block, end) = input.parse_unknown_at_rule()?;
370            (
371                prelude.map(|prelude| AtRulePrelude::Unknown(arena_box!(input, prelude))),
372                block,
373                end.unwrap_or(at_keyword_span.end),
374            )
375        };
376
377        let span = Span { start: at_keyword_span.start, end };
378        Ok(AtRule {
379            name: input.ident(
380                at_keyword.ident,
381                Span { start: at_keyword_span.start + 1, end: at_keyword_span.end },
382            ),
383            prelude,
384            block,
385            span,
386        })
387    }
388}
389
390impl<'a> Parser<'a> {
391    pub(super) fn parse_unknown_at_rule(
392        &mut self,
393    ) -> PResult<(Option<UnknownAtRulePrelude<'a>>, Option<SimpleBlock<'a>>, Option<usize>)> {
394        let prelude = self.parse_unknown_at_rule_prelude()?;
395        let block = match &peek!(self).token {
396            Token::LBrace(..) | Token::Indent(..) => Some(self.parse::<SimpleBlock>()?),
397            _ => None,
398        };
399        let end = block
400            .as_ref()
401            .map(|block| block.span.end)
402            .or_else(|| prelude.as_ref().map(|prelude| prelude.span().end));
403        Ok((prelude, block, end))
404    }
405
406    fn parse_unknown_at_rule_prelude(&mut self) -> PResult<Option<UnknownAtRulePrelude<'a>>> {
407        if let Ok(prelude) = self.try_parse(|parser| {
408            let mut tokens = parser.vec();
409            loop {
410                match &peek!(parser).token {
411                    Token::LBrace(..)
412                    | Token::RBrace(..)
413                    | Token::Semicolon(..)
414                    | Token::Indent(..)
415                    | Token::Dedent(..)
416                    | Token::Linebreak(..)
417                    | Token::Eof(..) => break,
418                    Token::StrTemplate(..) | Token::HashLBrace(..) => {
419                        return Err(Error {
420                            kind: ErrorKind::TryParseError,
421                            span: bump!(parser).span,
422                        });
423                    }
424                    _ => tokens.push(bump!(parser)),
425                }
426            }
427            if let Some((first, last)) = tokens.first().zip(tokens.last()) {
428                let span = Span { start: first.span().start, end: last.span().end };
429                Ok(Some(UnknownAtRulePrelude::TokenSeq(TokenSeq { tokens, span })))
430            } else {
431                Ok(None)
432            }
433        }) {
434            return Ok(prelude);
435        }
436
437        Ok(Some(UnknownAtRulePrelude::ComponentValue(match self.syntax {
438            Syntax::Css => self.parse()?,
439            Syntax::Scss | Syntax::Sass => {
440                self.parse_maybe_sass_list(/* allow_comma */ true)?
441            }
442            Syntax::Less => self.parse_maybe_less_list(/* allow_comma */ true)?,
443        })))
444    }
445}