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