Skip to main content

oxc_css_parser/parser/at_rule/
mod.rs

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