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    error::{Error, ErrorKind, PResult},
6    pos::Span,
7    tokenizer::Token,
8};
9
10mod color_profile;
11mod container;
12mod counter_style;
13mod custom_media;
14mod custom_selector;
15mod document;
16mod font_feature_values;
17mod import;
18mod keyframes;
19mod layer;
20mod media;
21mod namespace;
22mod page;
23mod scope;
24mod supports;
25
26// https://drafts.csswg.org/css-syntax-3/#consume-at-rule
27//
28// <at-rule> = <at-keyword-token> <component-value>* [ <{}-block> | ; ]
29//
30// This dispatches on the at-keyword name to a typed prelude/block grammar; each
31// rule's grammar is documented in its own module (see the `mod` list above).
32// Unrecognized names fall back to raw component values (`parse_unknown_at_rule`).
33impl<'a> Parse<'a> for AtRule<'a> {
34    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
35        let (at_keyword, at_keyword_span) = input.cursor.expect_at_keyword()?;
36
37        let at_rule_name = at_keyword.ident.name();
38        let (prelude, block, end) = if at_rule_name.eq_ignore_ascii_case("media") {
39            // The typed grammar must account for the whole prelude; queries it
40            // can't express (`@media all #{$m}`) are kept as raw tokens.
41            let prelude = match input
42                .try_parse_full_prelude(|p| MediaQueryList::parse(p).map(AtRulePrelude::Media))
43            {
44                Ok(prelude) => Some(prelude),
45                Err(_)
46                    if matches!(
47                        input.cursor.peek()?.token,
48                        Token::LBrace(..) | Token::Indent(..)
49                    ) =>
50                {
51                    None
52                }
53                // Only interpolation justifies the raw form — dart-sass
54                // reparses such queries after resolving `#{...}`; plain
55                // malformed logic (`@media a and b or c`) must keep erroring.
56                Err(_) => {
57                    // Only substitution constructs justify the raw form —
58                    // `#{...}` in Sass (dart-sass reparses after resolving) and
59                    // variables in Less (`@media @all and @tv {`); plain
60                    // malformed logic must keep erroring.
61                    let raw = if input.syntax != Syntax::Css {
62                        input.try_parse(|p| {
63                            let raw = p.parse_raw_at_rule_prelude()?;
64                            let has_substitution = matches!(
65                                &raw,
66                                UnknownAtRulePrelude::TokenSeq(seq)
67                                    if seq.tokens.iter().any(|t| match &t.token {
68                                        Token::HashLBrace(..) | Token::StrTemplate(..) => {
69                                            matches!(p.syntax, Syntax::Scss | Syntax::Sass)
70                                        }
71                                        Token::AtKeyword(..) | Token::AtLBraceVar(..) => {
72                                            p.syntax == Syntax::Less
73                                        }
74                                        _ => false,
75                                    })
76                            );
77                            if has_substitution {
78                                Ok(raw)
79                            } else {
80                                let span = p.cursor.peek()?.span;
81                                Err(Error { kind: ErrorKind::TryParseError, span })
82                            }
83                        })
84                    } else {
85                        Err(Error {
86                            kind: ErrorKind::TryParseError,
87                            span: input.cursor.peek()?.span,
88                        })
89                    };
90                    match raw {
91                        Ok(raw) => Some(AtRulePrelude::Unknown(input.alloc(raw))),
92                        Err(_) => Some(AtRulePrelude::Media(input.parse()?)),
93                    }
94                }
95            };
96            let block = input.parse::<SimpleBlock>()?;
97            let end = block.span.end;
98            (prelude, Some(block), end)
99        } else if at_rule_name.eq_ignore_ascii_case("keyframes")
100            || at_rule_name.eq_ignore_ascii_case("-webkit-keyframes")
101            || at_rule_name.eq_ignore_ascii_case("-moz-keyframes")
102            || at_rule_name.eq_ignore_ascii_case("-ms-keyframes")
103            || at_rule_name.eq_ignore_ascii_case("-o-keyframes")
104        {
105            // A nameless `@keyframes {}` is invalid CSS, but Sass parses it
106            // (the name may be produced elsewhere, e.g. a keyframes mixin
107            // emitting vendor-prefixed blocks around `@content`).
108            let prelude = match &input.cursor.peek()?.token {
109                Token::LBrace(..) => None,
110                _ => {
111                    // A typed name normally ends the prelude; real-world code
112                    // also carries loose preludes (`@keyframes \$a`,
113                    // `@-moz-keyframes name /* c */ line 429`) — keep those as
114                    // raw tokens like an unknown at-rule's prelude.
115                    let typed = input.try_parse_full_prelude(KeyframesName::parse);
116                    match typed {
117                        Ok(name) => Some(AtRulePrelude::Keyframes(name)),
118                        Err(_) => input
119                            .parse_unknown_at_rule_prelude()?
120                            .map(|prelude| AtRulePrelude::Unknown(input.alloc(prelude))),
121                    }
122                }
123            };
124            let block = input
125                .with_state(ParserState { in_keyframes_at_rule: true, ..input.state.clone() })
126                .parse::<SimpleBlock>()?;
127            let end = block.span.end;
128            (prelude, Some(block), end)
129        } else if at_rule_name.eq_ignore_ascii_case("import") {
130            let (end, prelude) = match input.syntax {
131                Syntax::Css => {
132                    let prelude = input.parse::<ImportPrelude>()?;
133                    (prelude.span.end, AtRulePrelude::Import(input.alloc(prelude)))
134                }
135                Syntax::Scss | Syntax::Sass => {
136                    // A multi-path import (`@import 'a', 'b';`) is
137                    // Sass-specific and must win over `ImportPrelude`'s
138                    // lenient raw-modifiers tail so each path stays typed.
139                    // Claim it only on a full-prelude match with more than
140                    // one path: a single path keeps the standard
141                    // `ImportPrelude` shape, and any unparsed tail
142                    // (`@import 'a', 'b' screen;`) falls through to
143                    // `ImportPrelude`'s leniency.
144                    let multi_path = input.try_parse(|parser| {
145                        let prelude = parser.try_parse_full_prelude(SassImportPrelude::parse)?;
146                        if prelude.paths.len() > 1 {
147                            Ok(prelude)
148                        } else {
149                            Err(Error { kind: ErrorKind::TryParseError, span: prelude.span })
150                        }
151                    });
152                    if let Ok(prelude) = multi_path {
153                        (prelude.span.end, AtRulePrelude::SassImport(prelude))
154                    } else if let Ok(prelude) = input.try_parse(ImportPrelude::parse) {
155                        (prelude.span.end, AtRulePrelude::Import(input.alloc(prelude)))
156                    } else {
157                        let prelude = input.parse::<SassImportPrelude>()?;
158                        (prelude.span.end, AtRulePrelude::SassImport(prelude))
159                    }
160                }
161                Syntax::Less => {
162                    if let Ok(prelude) = input.try_parse(ImportPrelude::parse) {
163                        (prelude.span.end, AtRulePrelude::Import(input.alloc(prelude)))
164                    } else {
165                        let prelude = input.parse::<LessImportPrelude>()?;
166                        (prelude.span.end, AtRulePrelude::LessImport(input.alloc(prelude)))
167                    }
168                }
169            };
170            (Some(prelude), None, end)
171        } else if at_rule_name.eq_ignore_ascii_case("charset") {
172            // https://drafts.csswg.org/css2/#charset%E2%91%A0
173            // Less may interpolate into it: `@charset "UTF-@{Eight}";`
174            if input.syntax == Syntax::Less
175                && matches!(input.cursor.peek()?.token, Token::StrTemplate(..))
176            {
177                let prelude = input.parse::<InterpolableStr>()?;
178                let end = prelude.span().end;
179                (
180                    Some(AtRulePrelude::Unknown(input.alloc(
181                        UnknownAtRulePrelude::ComponentValue(ComponentValue::InterpolableStr(
182                            prelude,
183                        )),
184                    ))),
185                    None,
186                    end,
187                )
188            } else {
189                let prelude = input.parse::<Str>()?;
190                let end = prelude.span.end;
191                (Some(AtRulePrelude::Charset(prelude)), None, end)
192            }
193        } else if at_rule_name.eq_ignore_ascii_case("font-face") {
194            let block = input.parse::<SimpleBlock>()?;
195            let end = block.span.end;
196            (None, Some(block), end)
197        } else if at_rule_name.eq_ignore_ascii_case("supports") {
198            let prelude = Some(AtRulePrelude::Supports(input.parse()?));
199            let block = input.parse::<SimpleBlock>()?;
200            let end = block.span.end;
201            (prelude, Some(block), end)
202        } else if at_rule_name.eq_ignore_ascii_case("layer") {
203            let prelude = match input.try_parse(LayerNames::parse) {
204                Ok(names) => Some(AtRulePrelude::Layer(names)),
205                // a Less variable may stand for the name: `@layer @layer-name {`
206                Err(_)
207                    if input.syntax == Syntax::Less
208                        && matches!(input.cursor.peek()?.token, Token::AtKeyword(..)) =>
209                {
210                    let raw = input.parse_raw_at_rule_prelude()?;
211                    Some(AtRulePrelude::Unknown(input.alloc(raw)))
212                }
213                Err(_) => None,
214            };
215            let block =
216                if matches!(input.cursor.peek()?.token, Token::LBrace(..) | Token::Indent(..)) {
217                    Some(input.parse::<SimpleBlock>()?)
218                } else {
219                    None
220                };
221            if let Some(block) = &block
222                && matches!(&prelude, Some(AtRulePrelude::Layer(names)) if names.names.len() > 1)
223            {
224                input
225                    .recoverable_errors
226                    .push(Error { kind: ErrorKind::UnexpectedSimpleBlock, span: block.span });
227            }
228            let end = block
229                .as_ref()
230                .map(|block| block.span.end)
231                .or_else(|| prelude.as_ref().map(|prelude| prelude.span().end))
232                .unwrap_or(at_keyword_span.end);
233            (prelude, block, end)
234        } else if at_rule_name.eq_ignore_ascii_case("container") {
235            // less.js emits comma-separated query lists
236            // (`@container card (w > 400px), style(--x: y) {`); those — and
237            // only those — are kept as raw tokens. A malformed single query
238            // still errors.
239            let prelude =
240                match input.try_parse_full_prelude(|p| p.parse().map(AtRulePrelude::Container)) {
241                    Ok(prelude) => prelude,
242                    Err(error) => {
243                        let is_query_list = input
244                            .try_parse(|p| {
245                                p.parse::<ContainerPrelude>()?;
246                                if matches!(p.cursor.peek()?.token, Token::Comma(..)) {
247                                    Ok(())
248                                } else {
249                                    let span = p.cursor.peek()?.span;
250                                    Err(Error { kind: ErrorKind::TryParseError, span })
251                                }
252                            })
253                            .is_ok();
254                        // a Less variable may stand for the container name:
255                        // `@container @varfoo (min-width: @threshold) {`
256                        let less_variable_name = input.syntax == Syntax::Less
257                            && matches!(input.cursor.peek()?.token, Token::AtKeyword(..));
258                        if !is_query_list && !less_variable_name {
259                            return Err(error);
260                        }
261                        let prelude = input.parse_raw_at_rule_prelude()?;
262                        AtRulePrelude::Unknown(input.alloc(prelude))
263                    }
264                };
265            let block = input.parse::<SimpleBlock>()?;
266            let end = block.span.end;
267            (Some(prelude), Some(block), end)
268        } else if at_rule_name.eq_ignore_ascii_case("page") {
269            let prelude = input.try_parse(PageSelectorList::parse).map(AtRulePrelude::Page).ok();
270            let block = input.try_parse(SimpleBlock::parse).ok();
271            let end = block
272                .as_ref()
273                .map(|block| block.span.end)
274                .or_else(|| prelude.as_ref().map(|prelude| prelude.span().end))
275                .unwrap_or(at_keyword_span.end);
276            (prelude, block, end)
277        } else if at_rule_name.eq_ignore_ascii_case("namespace")
278            && input.syntax == Syntax::Less
279            && matches!(input.cursor.peek()?.token, Token::AtKeyword(..))
280        {
281            // `@namespace @ns "http://...";` — a Less variable prefix
282            let raw = input.parse_raw_at_rule_prelude()?;
283            let end = raw.span().end;
284            (Some(AtRulePrelude::Unknown(input.alloc(raw))), None, end)
285        } else if at_rule_name.eq_ignore_ascii_case("namespace") {
286            let namespace = input.parse::<NamespacePrelude>()?;
287            let end = namespace.span.end;
288            (Some(AtRulePrelude::Namespace(input.alloc(namespace))), None, end)
289        } else if at_rule_name.eq_ignore_ascii_case("color-profile") {
290            let prelude = Some(AtRulePrelude::ColorProfile(input.parse()?));
291            let block = input.parse::<SimpleBlock>()?;
292            let end = block.span.end;
293            (prelude, Some(block), end)
294        } else if at_rule_name.eq_ignore_ascii_case("font-feature-values") {
295            let prelude = Some(AtRulePrelude::FontFeatureValues(input.parse()?));
296            let block = input.parse::<SimpleBlock>()?;
297            let end = block.span.end;
298            (prelude, Some(block), end)
299        } else if at_rule_name.eq_ignore_ascii_case("font-palette-values") {
300            // https://drafts.csswg.org/css-fonts/Overview.bs
301            let prelude = Some(AtRulePrelude::FontPaletteValues(input.parse_dashed_ident()?));
302            let block = input.parse::<SimpleBlock>()?;
303            let end = block.span.end;
304            (prelude, Some(block), end)
305        } else if at_rule_name.eq_ignore_ascii_case("counter-style") {
306            let prelude = Some(AtRulePrelude::CounterStyle(input.parse_counter_style_prelude()?));
307            let block = input.parse::<SimpleBlock>()?;
308            let end = block.span.end;
309            (prelude, Some(block), end)
310        } else if at_rule_name.eq_ignore_ascii_case("custom-media") {
311            let custom_media = input.parse::<CustomMedia>()?;
312            let end = custom_media.span.end;
313            (Some(AtRulePrelude::CustomMedia(input.alloc(custom_media))), None, end)
314        } else if at_rule_name.eq_ignore_ascii_case("custom-selector") {
315            let custom_selector_prelude = input.parse::<CustomSelectorPrelude>()?;
316            let end = custom_selector_prelude.span.end;
317            (Some(AtRulePrelude::CustomSelector(input.alloc(custom_selector_prelude))), None, end)
318        } else if at_rule_name.eq_ignore_ascii_case("position-try") {
319            // https://drafts.csswg.org/css-anchor-position-1/#fallback-rule
320            let prelude = Some(AtRulePrelude::PositionTry(input.parse_dashed_ident()?));
321            let block = input.parse::<SimpleBlock>()?;
322            let end = block.span.end;
323            (prelude, Some(block), end)
324        } else if at_rule_name.eq_ignore_ascii_case("nest") {
325            // https://www.w3.org/TR/css-nesting-1/#at-nest
326            let prelude = Some(AtRulePrelude::Nest(input.parse()?));
327            let block = input.parse::<SimpleBlock>()?;
328            let end = block.span.end;
329            (prelude, Some(block), end)
330        } else if at_rule_name.eq_ignore_ascii_case("property") {
331            // https://drafts.css-houdini.org/css-properties-values-api/#at-property-rule
332            let prelude = Some(AtRulePrelude::Property(input.parse_dashed_ident()?));
333            let block = input.parse::<SimpleBlock>()?;
334            let end = block.span.end;
335            (prelude, Some(block), end)
336        } else if at_rule_name.eq_ignore_ascii_case("scope") {
337            let prelude = if let Token::LParen(..) | Token::Ident(..) = input.cursor.peek()?.token {
338                let scope = input.parse()?;
339                Some(AtRulePrelude::Scope(input.alloc(scope)))
340            } else {
341                None
342            };
343            let block = input.parse::<SimpleBlock>()?;
344            let end = block.span.end;
345            (prelude, Some(block), end)
346        } else if at_rule_name.eq_ignore_ascii_case("document")
347            || at_rule_name.eq_ignore_ascii_case("-moz-document")
348        {
349            let prelude = match input.try_parse(|p| p.parse().map(AtRulePrelude::Document)) {
350                Ok(prelude) => prelude,
351                // e.g. less.js's permissive `@-moz-document @fn("(x)")` — but
352                // an empty prelude (`@document {}`) stays an error.
353                Err(error) => {
354                    if matches!(
355                        input.cursor.peek()?.token,
356                        Token::LBrace(..) | Token::Indent(..) | Token::Semicolon(..)
357                    ) {
358                        return Err(error);
359                    }
360                    let prelude = input.parse_raw_at_rule_prelude()?;
361                    AtRulePrelude::Unknown(input.alloc(prelude))
362                }
363            };
364            // real-world code also writes a block-less `@document ...;`
365            let block =
366                if matches!(input.cursor.peek()?.token, Token::LBrace(..) | Token::Indent(..)) {
367                    Some(input.parse::<SimpleBlock>()?)
368                } else {
369                    None
370                };
371            let end = block.as_ref().map_or(prelude.span().end, |block| block.span.end);
372            (Some(prelude), block, end)
373        } else if at_rule_name.eq_ignore_ascii_case("stylistic")
374            || at_rule_name.eq_ignore_ascii_case("historical-forms")
375            || at_rule_name.eq_ignore_ascii_case("styleset")
376            || at_rule_name.eq_ignore_ascii_case("character-variant")
377            || at_rule_name.eq_ignore_ascii_case("swash")
378            || at_rule_name.eq_ignore_ascii_case("ornaments")
379            || at_rule_name.eq_ignore_ascii_case("annotation")
380            || at_rule_name.eq_ignore_ascii_case("top-left-corner")
381            || at_rule_name.eq_ignore_ascii_case("top-left")
382            || at_rule_name.eq_ignore_ascii_case("top-center")
383            || at_rule_name.eq_ignore_ascii_case("top-right")
384            || at_rule_name.eq_ignore_ascii_case("top-right-corner")
385            || at_rule_name.eq_ignore_ascii_case("bottom-left-corner")
386            || at_rule_name.eq_ignore_ascii_case("bottom-left")
387            || at_rule_name.eq_ignore_ascii_case("bottom-center")
388            || at_rule_name.eq_ignore_ascii_case("bottom-right")
389            || at_rule_name.eq_ignore_ascii_case("bottom-right-corner")
390            || at_rule_name.eq_ignore_ascii_case("left-top")
391            || at_rule_name.eq_ignore_ascii_case("left-middle")
392            || at_rule_name.eq_ignore_ascii_case("left-bottom")
393            || at_rule_name.eq_ignore_ascii_case("right-top")
394            || at_rule_name.eq_ignore_ascii_case("right-middle")
395            || at_rule_name.eq_ignore_ascii_case("right-bottom")
396            || at_rule_name.eq_ignore_ascii_case("viewport")
397            || at_rule_name.eq_ignore_ascii_case("try")
398            || at_rule_name.eq_ignore_ascii_case("starting-style")
399        {
400            let block = input.parse::<SimpleBlock>()?;
401            let end = block.span.end;
402            (None, Some(block), end)
403        } else if at_rule_name == "plugin" && input.syntax == Syntax::Less {
404            let prelude = input.parse::<LessPlugin>()?;
405            let end = prelude.span.end;
406            (Some(AtRulePrelude::LessPlugin(input.alloc(prelude))), None, end)
407        } else if at_rule_name.eq_ignore_ascii_case("function")
408            && input.cursor.peek()?.is_ident_raw_starts_with(input.source, "--")
409        {
410            // A CSS custom function (css-mixins spec): `@function --name(params)
411            // returns <type> { declarations }`. dart-sass parses this as plain
412            // CSS in every syntax (the `--` name marks it), so the prelude is
413            // kept as raw tokens and the body takes declarations with raw
414            // values.
415            let prelude = input.parse_raw_at_rule_prelude()?;
416            let block = input
417                .with_state(ParserState { in_css_function_body: true, ..input.state.clone() })
418                .parse::<SimpleBlock>()?;
419            let end = block.span.end;
420            (Some(AtRulePrelude::Unknown(input.alloc(prelude))), Some(block), end)
421        } else if matches!(input.syntax, Syntax::Scss | Syntax::Sass) {
422            use super::state::{
423                SASS_CTX_ALLOW_DIV, SASS_CTX_ALLOW_KEYFRAME_BLOCK, SASS_CTX_IN_FUNCTION,
424            };
425            match &*at_rule_name {
426                "each" => {
427                    let prelude = input.parse()?;
428                    let block = input.parse::<SimpleBlock>()?;
429                    let end = block.span.end;
430                    (Some(AtRulePrelude::SassEach(input.alloc(prelude))), Some(block), end)
431                }
432                "while" => {
433                    input.eat_sass_line_continuation()?;
434                    let prelude = input.parse()?;
435                    let block = input.parse::<SimpleBlock>()?;
436                    let end = block.span.end;
437                    (Some(AtRulePrelude::SassExpr(input.alloc(prelude))), Some(block), end)
438                }
439                "for" => {
440                    let prelude = input.parse()?;
441                    let block = input.parse::<SimpleBlock>()?;
442                    let end = block.span.end;
443                    (Some(AtRulePrelude::SassFor(input.alloc(prelude))), Some(block), end)
444                }
445                "mixin" => {
446                    let prelude = input.parse()?;
447                    let block = input
448                        .with_state(ParserState {
449                            sass_ctx: input.state.sass_ctx | SASS_CTX_ALLOW_KEYFRAME_BLOCK,
450                            ..input.state.clone()
451                        })
452                        .parse::<SimpleBlock>()?;
453                    let end = block.span.end;
454                    (Some(AtRulePrelude::SassMixin(input.alloc(prelude))), Some(block), end)
455                }
456                "include" => {
457                    let prelude = input.parse::<SassInclude>()?;
458                    let block = if matches!(
459                        input.cursor.peek()?.token,
460                        Token::LBrace(..) | Token::Indent(..)
461                    ) {
462                        Some(
463                            input
464                                .with_state(ParserState {
465                                    sass_ctx: input.state.sass_ctx | SASS_CTX_ALLOW_KEYFRAME_BLOCK,
466                                    ..input.state.clone()
467                                })
468                                .parse::<SimpleBlock>()?,
469                        )
470                    } else {
471                        None
472                    };
473                    let end =
474                        block.as_ref().map(|block| block.span.end).unwrap_or(prelude.span.end);
475                    (Some(AtRulePrelude::SassInclude(input.alloc(prelude))), block, end)
476                }
477                "content" => {
478                    if matches!(input.cursor.peek()?.token, Token::LParen(..)) {
479                        let prelude = input.parse::<SassContent>()?;
480                        let end = prelude.span.end;
481                        (Some(AtRulePrelude::SassContent(prelude)), None, end)
482                    } else {
483                        (None, None, input.cursor.tokenizer.current_offset())
484                    }
485                }
486                "use" => {
487                    let prelude = input.parse::<SassUse>()?;
488                    let end = prelude.span.end;
489                    (Some(AtRulePrelude::SassUse(input.alloc(prelude))), None, end)
490                }
491                "function" => {
492                    let prelude = input.parse::<SassFunction>()?;
493                    let block = input
494                        .with_state(ParserState {
495                            sass_ctx: input.state.sass_ctx | SASS_CTX_IN_FUNCTION,
496                            ..input.state.clone()
497                        })
498                        .parse::<SimpleBlock>()?;
499                    let end = block.span.end;
500                    (Some(AtRulePrelude::SassFunction(input.alloc(prelude))), Some(block), end)
501                }
502                "return" => {
503                    input.eat_sass_line_continuation()?;
504                    let expr = input
505                        .with_state(ParserState {
506                            sass_ctx: input.state.sass_ctx | SASS_CTX_ALLOW_DIV,
507                            ..input.state.clone()
508                        })
509                        .parse_maybe_sass_list(/* allow_comma */ true)?;
510                    let end = expr.span().end;
511                    if input.state.sass_ctx & SASS_CTX_IN_FUNCTION == 0 {
512                        input.recoverable_errors.push(Error {
513                            kind: ErrorKind::ReturnOutsideFunction,
514                            span: Span { start: at_keyword_span.start, end },
515                        });
516                    }
517                    (Some(AtRulePrelude::SassExpr(input.alloc(expr))), None, end)
518                }
519                "extend" => {
520                    let prelude = input.parse::<SassExtend>()?;
521                    let end = prelude.span.end;
522                    (Some(AtRulePrelude::SassExtend(input.alloc(prelude))), None, end)
523                }
524                "warn" | "error" | "debug" => {
525                    input.eat_sass_line_continuation()?;
526                    let expr = input.parse_maybe_sass_list(/* allow_comma */ true)?;
527                    let end = expr.span().end;
528                    (Some(AtRulePrelude::SassExpr(input.alloc(expr))), None, end)
529                }
530                "forward" => {
531                    let prelude = input.parse::<SassForward>()?;
532                    let end = prelude.span.end;
533                    (Some(AtRulePrelude::SassForward(input.alloc(prelude))), None, end)
534                }
535                "at-root" => {
536                    let prelude = if !matches!(
537                        input.cursor.peek()?.token,
538                        Token::LBrace(..)
539                            | Token::Indent(..)
540                            | Token::Linebreak(..)
541                            | Token::Dedent(..)
542                            | Token::Eof(..)
543                    ) {
544                        Some(AtRulePrelude::SassAtRoot(input.parse()?))
545                    } else {
546                        None
547                    };
548                    // `@at-root` escapes surrounding contexts, including a
549                    // `@keyframes` body — its block holds normal rules.
550                    let block = input
551                        .with_state(ParserState {
552                            in_keyframes_at_rule: false,
553                            ..input.state.clone()
554                        })
555                        .parse::<SimpleBlock>()?;
556                    let end = block.span.end;
557                    (prelude, Some(block), end)
558                }
559                _ => {
560                    let (prelude, block, end) = input.parse_unknown_at_rule()?;
561                    (
562                        prelude.map(|prelude| AtRulePrelude::Unknown(input.alloc(prelude))),
563                        block,
564                        end.unwrap_or(at_keyword_span.end),
565                    )
566                }
567            }
568        } else {
569            let (prelude, block, end) = input.parse_unknown_at_rule()?;
570            (
571                prelude.map(|prelude| AtRulePrelude::Unknown(input.alloc(prelude))),
572                block,
573                end.unwrap_or(at_keyword_span.end),
574            )
575        };
576
577        let span = Span { start: at_keyword_span.start, end };
578        Ok(AtRule {
579            name: input.ident(
580                at_keyword.ident,
581                Span { start: at_keyword_span.start + 1, end: at_keyword_span.end },
582            ),
583            prelude,
584            block,
585            span,
586        })
587    }
588}
589
590impl<'a> Parser<'a> {
591    /// `try_parse` a typed at-rule prelude that must account for everything
592    /// up to the end of the prelude (the block's opener or the statement
593    /// boundary) — otherwise the parse is rolled back so the caller can fall
594    /// back to a raw form.
595    fn try_parse_full_prelude<T>(&mut self, f: impl FnOnce(&mut Self) -> PResult<T>) -> PResult<T> {
596        self.try_parse(|p| {
597            let value = f(p)?;
598            match &p.cursor.peek()?.token {
599                Token::LBrace(..)
600                | Token::Indent(..)
601                | Token::Semicolon(..)
602                | Token::Dedent(..)
603                | Token::Linebreak(..)
604                | Token::Eof(..) => Ok(value),
605                _ => {
606                    let span = p.cursor.peek()?.span;
607                    Err(Error { kind: ErrorKind::TryParseError, span })
608                }
609            }
610        })
611    }
612
613    /// A raw at-rule prelude: everything up to the body's `{` (or the end of
614    /// the statement), balancing pairs so interpolations and parens pass
615    /// through — CSS custom function preludes (`--name(--arg) returns <type>`)
616    /// and media queries the typed grammar can't express.
617    fn parse_raw_at_rule_prelude(&mut self) -> PResult<UnknownAtRulePrelude<'a>> {
618        let start = self.cursor.tokenizer.current_offset();
619        let mut tokens = self.vec();
620        let mut pairs: Vec<crate::util::PairedToken> = Vec::new();
621        loop {
622            match &self.cursor.peek()?.token {
623                Token::Semicolon(..)
624                | Token::Dedent(..)
625                | Token::Linebreak(..)
626                | Token::Indent(..)
627                | Token::Eof(..) => break,
628                Token::LBrace(..) if pairs.is_empty() => break,
629                // Interpolated strings must be consumed structurally — the
630                // tokenizer resumes the string after each `#{...}` — but
631                // their pieces are still plain tokens.
632                Token::StrTemplate(..) => {
633                    self.consume_str_template_tokens_into(&mut tokens)?;
634                    continue;
635                }
636                token => {
637                    if !crate::util::track_paired_token(token, &mut pairs) {
638                        break;
639                    }
640                }
641            }
642            tokens.push(self.cursor.bump()?);
643        }
644        let span = Span {
645            start: tokens.first().map_or(start, |token| token.span.start),
646            end: tokens.last().map_or(start, |token| token.span.end),
647        };
648        Ok(UnknownAtRulePrelude::TokenSeq(TokenSeq { tokens, span }))
649    }
650
651    // The generic `<at-rule>` for a name with no typed grammar: an arbitrary
652    // prelude of component values, then an optional `{}`-block or a `;`.
653    // https://drafts.csswg.org/css-syntax-3/#consume-at-rule
654    pub(super) fn parse_unknown_at_rule(
655        &mut self,
656    ) -> PResult<(Option<UnknownAtRulePrelude<'a>>, Option<SimpleBlock<'a>>, Option<usize>)> {
657        let prelude = self.parse_unknown_at_rule_prelude()?;
658        let block = match &self.cursor.peek()?.token {
659            // An unknown at-rule's children parse generically; in Sass that
660            // includes keyframe-style `10% { ... }` blocks
661            // (`@keyfr#{"ames"} a {...}`). less.js keeps them an error.
662            Token::LBrace(..) | Token::Indent(..) => {
663                let sass_ctx = if matches!(self.syntax, Syntax::Scss | Syntax::Sass) {
664                    self.state.sass_ctx | super::state::SASS_CTX_ALLOW_KEYFRAME_BLOCK
665                } else {
666                    self.state.sass_ctx
667                };
668                Some(
669                    self.with_state(ParserState { sass_ctx, ..self.state.clone() })
670                        .parse::<SimpleBlock>()?,
671                )
672            }
673            _ => None,
674        };
675        let end = block
676            .as_ref()
677            .map(|block| block.span.end)
678            .or_else(|| prelude.as_ref().map(|prelude| prelude.span().end));
679        Ok((prelude, block, end))
680    }
681
682    // The prelude of an unknown at-rule: raw `<component-value>*` up to the body
683    // `{` or the statement boundary.
684    fn parse_unknown_at_rule_prelude(&mut self) -> PResult<Option<UnknownAtRulePrelude<'a>>> {
685        if let Ok(prelude) = self.try_parse(|parser| {
686            let mut tokens = parser.vec();
687            loop {
688                match &parser.cursor.peek()?.token {
689                    Token::LBrace(..)
690                    | Token::RBrace(..)
691                    | Token::Semicolon(..)
692                    | Token::Indent(..)
693                    | Token::Dedent(..)
694                    | Token::Linebreak(..)
695                    | Token::Eof(..) => break,
696                    Token::StrTemplate(..) | Token::HashLBrace(..) => {
697                        return Err(Error {
698                            kind: ErrorKind::TryParseError,
699                            span: parser.cursor.bump()?.span,
700                        });
701                    }
702                    _ => tokens.push(parser.cursor.bump()?),
703                }
704            }
705            if let Some((first, last)) = tokens.first().zip(tokens.last()) {
706                let span = Span { start: first.span.start, end: last.span.end };
707                Ok(Some(UnknownAtRulePrelude::TokenSeq(TokenSeq { tokens, span })))
708            } else {
709                Ok(None)
710            }
711        }) {
712            return Ok(prelude);
713        }
714
715        Ok(Some(UnknownAtRulePrelude::ComponentValue(match self.syntax {
716            Syntax::Css => self.parse()?,
717            Syntax::Scss | Syntax::Sass => {
718                self.parse_maybe_sass_list(/* allow_comma */ true)?
719            }
720            Syntax::Less => self.parse_maybe_less_list(/* allow_comma */ true)?,
721        })))
722    }
723}