Skip to main content

oxc_css_parser/parser/
stmt.rs

1use super::{
2    Parser,
3    state::{ParserState, QualifiedRuleContext},
4};
5use crate::{
6    Parse, Syntax,
7    ast::*,
8    error::{Error, ErrorKind, PResult},
9    pos::Span,
10    tokenizer::{Token, TokenWithSpan},
11};
12
13// https://drafts.csswg.org/css-syntax-3/#consume-declaration
14//
15// <declaration> = <ident-token> : <declaration-value>? [ '!' important ]?
16impl<'a> Parse<'a> for Declaration<'a> {
17    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
18        // Css: a postcss property name (README "Acceptance").
19        // Statement position only: a feature query keeps the `<ident-token>`
20        // grammar, and one the typed grammar rejects (`@supports (*zoom: 1)`)
21        // is kept anyway by the `<general-enclosed>` raw fallback.
22        let postcss_name = if input.syntax == Syntax::Css
23            && input.state.in_statement
24            && !input.at_plain_ident_property_name()?
25        {
26            input.try_parse(Parser::parse_postcss_property_name).ok()
27        } else {
28            None
29        };
30        // Scss / Less: the IE `*color` hack (dart-sass and less.js accept it),
31        // kept as a property-name prefix, but only when glued: `* color`
32        // (whitespace or a comment after the sigil) is not the hack, so leave
33        // the token for the normal (failing) parse.
34        let name_prefix_start = if input.syntax != Syntax::Css
35            && input.state.in_statement
36            && let TokenWithSpan { token: Token::Asterisk(..), span } = input.cursor.peek()?
37            && input
38                .source
39                .as_bytes()
40                .get(span.end)
41                .is_some_and(|b| !b.is_ascii_whitespace() && *b != b'/')
42        {
43            let start = span.start;
44            input.cursor.bump()?;
45            Some(start)
46        } else {
47            None
48        };
49        // A css-in-js `${}` placeholder may stand in for the property name
50        // (`${foo}: ${bar}`); it is not a real ident, so accept it directly.
51        let name = if let Some(name) = postcss_name {
52            name
53        } else if let Token::Placeholder(..) = input.cursor.peek()?.token {
54            let (placeholder, span) = input.cursor.expect_placeholder()?;
55            InterpolableIdent::Placeholder((placeholder, span).into())
56        } else if input.state.in_statement
57            && input.syntax == Syntax::Less
58            && let token = input.cursor.peek()?
59            && let Some(raw) = token.number_raw(input.source)
60            && raw.bytes().all(|b| b.is_ascii_digit())
61        {
62            let span = token.span;
63            input.cursor.bump()?;
64            InterpolableIdent::Literal(Ident { name: raw, raw, span })
65        } else {
66            input
67                .with_state(ParserState {
68                    qualified_rule_ctx: Some(QualifiedRuleContext::DeclarationName),
69                    ..input.state
70                })
71                .parse::<InterpolableIdent>()?
72        };
73
74        // https://tailwindcss.com/docs/theme#overriding-the-default-theme
75        let name_suffix = if let TokenWithSpan { token: Token::Asterisk(..), span } =
76            input.cursor.peek()?
77            && name.span().end == span.start
78        {
79            input.cursor.bump()?;
80            Some('*')
81        } else {
82            None
83        };
84
85        // Less property merge (`prop+: v`, `prop+_: v`). In Css the `+` is
86        // part of the postcss property name above.
87        let less_property_merge = if input.syntax == Syntax::Less { input.parse()? } else { None };
88
89        let (_, colon_span) = input.cursor.expect_colon()?;
90        let is_custom_property = name.is_custom_property();
91        let (mut value, mut important, value_is_raw) = {
92            let mut parser = input.with_state(ParserState {
93                qualified_rule_ctx: Some(QualifiedRuleContext::DeclarationValue),
94                ..input.state
95            });
96            // For IE-compatibility, regardless of the property name (`filter`,
97            // `-ms-filter`, vendor variants...): `filter: progid:...`.
98            // Not peeked for a custom property: its text rescan below must
99            // start from the colon with nothing cached.
100            let source = parser.source;
101            let starts_with_progid = !is_custom_property
102                && matches!(&name, InterpolableIdent::Literal(..))
103                && parser.cursor.peek()?.is_ident_name_eq_ignore_ascii_case(source, "progid");
104            if is_custom_property && matches!(parser.syntax, Syntax::Scss | Syntax::Sass) {
105                // dart-sass reads a custom property as text,
106                // so `//` outside `#{...}` is part of the value
107                // even when the typed grammar can otherwise reach the terminator.
108                parser.parse_sass_custom_property_value()?
109            } else if is_custom_property || starts_with_progid {
110                // The value is everything up to the top-level `;` (§5.5.6).
111                // The typed parse may stop earlier (Scss ends a value at a nesting block),
112                // which would turn `--x: { a: b } --y: c;` into two declarations.
113                // So take the typed value only when it reaches the terminator.
114                if let Ok((values, important)) =
115                    parser.try_parse(Parser::parse_whole_declaration_value)
116                {
117                    (values, important, false)
118                } else {
119                    (parser.parse_declaration_value_tokens(false)?, None, true)
120                }
121            } else if parser.syntax == Syntax::Css
122                || (parser.state.in_css_function_body
123                    && matches!(&name, InterpolableIdent::Literal(..)))
124            {
125                // Scss/Sass/Less keep the strict grammar:
126                // their dialects assign meaning to these tokens
127                // and are expected to reject exactly what their reference compilers reject.
128                parser.parse_css_any_value()?
129            } else {
130                (parser.parse_declaration_value()?, None, false)
131            }
132        };
133
134        // CSS Syntax removes a trailing top-level `!important` from every declaration value,
135        // including one preserved as raw tokens because the typed grammar could not read it.
136        // Do this before looking at the cursor:
137        // the raw scan has already advanced to the declaration terminator.
138        if important.is_none() && value_is_raw {
139            important = take_raw_important(input, &mut value);
140        }
141        if important.is_none()
142            && let Token::Exclamation(..) = &input.cursor.peek()?.token
143        {
144            important = Some(input.parse::<ImportantAnnotation>()?);
145        }
146        // dart-sass allows `!important` mid-value (`fludge: foo bar
147        // !important hux;`): when more value follows, the annotation is just
148        // another component, and only a trailing one is structural.
149        while matches!(input.syntax, Syntax::Scss | Syntax::Sass)
150            && important.is_some()
151            && !at_declaration_value_end(&input.cursor.peek()?.token)
152        {
153            if let Some(annotation) = important.take() {
154                value.push(ComponentValue::ImportantAnnotation(annotation));
155            }
156            let more = input
157                .with_state(ParserState {
158                    qualified_rule_ctx: Some(QualifiedRuleContext::DeclarationValue),
159                    ..input.state
160                })
161                .parse_declaration_value()?;
162            for component in more {
163                value.push(component);
164            }
165            if let Token::Exclamation(..) = &input.cursor.peek()?.token {
166                important = Some(input.parse::<ImportantAnnotation>()?);
167            }
168        }
169
170        let span = Span {
171            start: name_prefix_start.unwrap_or(name.span().start),
172            end: if let Some(important) = &important {
173                important.span.end
174            } else if let Some(last) = value.last() {
175                last.span().end
176            } else {
177                colon_span.end
178            },
179        };
180        Ok(Declaration {
181            name,
182            name_prefix: name_prefix_start.map(|_| '*'),
183            name_suffix,
184            colon_span,
185            value,
186            value_is_raw,
187            important,
188            less_property_merge,
189            span,
190        })
191    }
192}
193
194/// Remove a trailing top-level `!important` from a preserved raw value.
195///
196/// Raw values flatten paired blocks into delimiter tokens, so first replay the
197/// pair stack up to the `!`: at EOF an unclosed `(... !important` must keep the
198/// annotation inside the value rather than promote it to declaration priority.
199fn take_raw_important<'a>(
200    input: &Parser<'a>,
201    value: &mut oxc_allocator::Vec<'a, ComponentValue<'a>>,
202) -> Option<ImportantAnnotation<'a>> {
203    let len = value.len();
204    if len < 2 {
205        return None;
206    }
207    let (bang_start, important) = match (&value[len - 2], &value[len - 1]) {
208        (
209            ComponentValue::TokenWithSpan(TokenWithSpan { token: Token::Exclamation(..), span }),
210            ComponentValue::TokenWithSpan(important),
211        ) if important.is_ident_name_eq_ignore_ascii_case(input.source, "important") => {
212            (span.start, *important)
213        }
214        _ => return None,
215    };
216
217    let mut pairs = Vec::with_capacity(1);
218    for component in &value[..len - 2] {
219        if let ComponentValue::TokenWithSpan(token) = component
220            && !crate::util::track_paired_token(&token.token, &mut pairs)
221        {
222            return None;
223        }
224    }
225    if !pairs.is_empty() {
226        return None;
227    }
228
229    let ident = input.ident(important.ident(input.source)?, important.span);
230    value.truncate(len - 2);
231    Some(ImportantAnnotation { span: Span { start: bang_start, end: important.span.end }, ident })
232}
233
234impl<'a> Parser<'a> {
235    /// `)` ends a feature-query declaration (`@supports (a: b)`), never a
236    /// statement one: there it is a stray closer the caller must deal with.
237    fn at_statement_value_end(in_statement: bool, token: &Token) -> bool {
238        at_declaration_value_end(token) && !(in_statement && matches!(token, Token::RParen(..)))
239    }
240
241    /// A Scss / Sass custom property value as dart-sass reads it: text where
242    /// `//` starts no comment outside `#{...}` (whose contents are SassScript).
243    /// The typed grammar wins for formatter layout only when it reaches the
244    /// terminator without meeting a line comment; otherwise the value is the
245    /// raw text run.
246    fn parse_sass_custom_property_value(
247        &mut self,
248    ) -> PResult<(oxc_allocator::Vec<'a, ComponentValue<'a>>, Option<ImportantAnnotation<'a>>, bool)>
249    {
250        debug_assert!(self.cursor.cached_token.is_none());
251        let line_comments_seen = self.cursor.tokenizer.state.line_comments_seen;
252        let typed = self.try_parse(|parser| {
253            let value = parser.parse_whole_declaration_value()?;
254            // A `//` the typed grammar read as a comment is value text (or, inside
255            // `#{...}`, a comment the typed stream cannot place): rescan as text.
256            if parser.cursor.tokenizer.state.line_comments_seen != line_comments_seen {
257                let span = parser.cursor.peek()?.span;
258                return Err(Error { kind: ErrorKind::TryParseError, span });
259            }
260            Ok(value)
261        });
262        if let Ok((values, important)) = typed {
263            return Ok((values, important, false));
264        }
265
266        // The failed `try_parse` restored the cursor to the colon; rescan the
267        // text with line comments disabled outside interpolation.
268        let line_comments = self.cursor.tokenizer.state.line_comments;
269        self.cursor.tokenizer.state.line_comments = false;
270        let values = self.parse_declaration_value_tokens(false);
271        self.cursor.tokenizer.state.line_comments = line_comments;
272        Ok((values?, None, true))
273    }
274
275    /// The Css `<any-value>` declaration value (CSS Syntax §5): serialized
276    /// selectors (`b: .c > d`), map-like blocks (`b: (3: 4)`) or stray delimiters
277    /// are all valid preserved tokens even though the typed grammar has no node
278    /// for them. The typed grammar wins when it accounts for everything up to
279    /// the terminator; otherwise the whole value is the raw token run.
280    pub(super) fn parse_css_any_value(
281        &mut self,
282    ) -> PResult<(oxc_allocator::Vec<'a, ComponentValue<'a>>, Option<ImportantAnnotation<'a>>, bool)>
283    {
284        if let Ok((values, important)) = self.try_parse(Parser::parse_whole_declaration_value) {
285            return Ok((values, important, false));
286        }
287        // A CSS custom function body holds declarations only, so a top-level
288        // `{}` there is part of the value; elsewhere it means this construct is
289        // really a qualified rule (CSS Nesting disambiguation) and the
290        // declaration is rejected.
291        let in_fn_body = self.state.in_css_function_body;
292        let values = self.parse_declaration_value_tokens(!in_fn_body)?;
293        let next = self.cursor.peek()?;
294        if !in_fn_body && let Token::LBrace(..) = next.token {
295            return Err(Error { kind: ErrorKind::BlockInDeclarationValue, span: next.span });
296        }
297        Ok((values, None, true))
298    }
299
300    /// The common `color: red`: an `<ident-token>` followed by whitespace or a
301    /// terminator can only be the single-ident run `parse_postcss_property_name`
302    /// rejects, so skip its snapshot and rescan.
303    fn at_plain_ident_property_name(&mut self) -> PResult<bool> {
304        let TokenWithSpan { token, span } = self.cursor.peek()?;
305        Ok(matches!(token, Token::Ident(..))
306            && self
307                .source
308                .as_bytes()
309                .get(span.end)
310                .is_none_or(|b| b.is_ascii_whitespace() || matches!(b, b':' | b';' | b'{' | b'}')))
311    }
312
313    /// postcss's property name (Css only): the glued token run up to the first
314    /// top-level `:`, whitespace or comment. A leading `:` is part of the run
315    /// (`:x: y`, an IE hack). Errors when the run is a single `<ident-token>`
316    /// (the typed grammar owns it), empty, or unbalanced, so `try_parse`
317    /// restores the cursor.
318    fn parse_postcss_property_name(&mut self) -> PResult<InterpolableIdent<'a>> {
319        let TokenWithSpan { token, span } = self.cursor.peek()?;
320        let start = span.start;
321        let ident_only_end = matches!(token, Token::Ident(..)).then_some(span.end);
322        let mut end = start;
323        let mut pairs: Vec<crate::util::PairedToken> = Vec::new();
324        loop {
325            let TokenWithSpan { token, span } = self.cursor.peek()?;
326            if end != start && span.start != end {
327                break;
328            }
329            match token {
330                Token::Colon(..) if pairs.is_empty() && end != start => break,
331                Token::Semicolon(..) | Token::LBrace(..) | Token::RBrace(..)
332                    if pairs.is_empty() =>
333                {
334                    break;
335                }
336                // Never name bytes; `#{}` pieces never appear in a Css property name.
337                Token::Eof(..)
338                | Token::Dedent(..)
339                | Token::Linebreak(..)
340                | Token::StrTemplate(..)
341                | Token::Placeholder(..) => break,
342                token => {
343                    if !crate::util::track_paired_token(token, &mut pairs) {
344                        break;
345                    }
346                }
347            }
348            end = span.end;
349            self.cursor.bump()?;
350        }
351        if end == start || Some(end) == ident_only_end || !pairs.is_empty() {
352            return Err(Error { kind: ErrorKind::ExpectRule, span: Span { start, end } });
353        }
354        let raw = &self.source[start..end];
355        Ok(InterpolableIdent::Literal(Ident { name: raw, raw, span: Span { start, end } }))
356    }
357
358    /// The typed `<declaration-value>` and its `!important`.
359    /// Fails unless they reach the declaration terminator,
360    /// so the caller can fall back to raw tokens for whatever the typed grammar missed.
361    pub(super) fn parse_whole_declaration_value(
362        &mut self,
363    ) -> PResult<(oxc_allocator::Vec<'a, ComponentValue<'a>>, Option<ImportantAnnotation<'a>>)>
364    {
365        let values = self.parse_declaration_value()?;
366        let important = match &self.cursor.peek()?.token {
367            Token::Exclamation(..) => Some(self.parse::<ImportantAnnotation>()?),
368            _ => None,
369        };
370        let in_statement = self.state.in_statement;
371        let next = self.cursor.peek()?;
372        if Self::at_statement_value_end(in_statement, &next.token) {
373            Ok((values, important))
374        } else {
375            Err(Error { kind: ErrorKind::ExpectComponentValue, span: next.span })
376        }
377    }
378}
379
380/// End of a declaration's value: the declaration terminator tokens.
381fn at_declaration_value_end(token: &Token) -> bool {
382    matches!(
383        token,
384        Token::Semicolon(..)
385            | Token::RBrace(..)
386            | Token::RParen(..)
387            | Token::Dedent(..)
388            | Token::Linebreak(..)
389            | Token::Eof(..)
390    )
391}
392
393// <important> = '!' important
394impl<'a> Parse<'a> for ImportantAnnotation<'a> {
395    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
396        let (_, span) = input.cursor.expect_exclamation()?;
397        input.eat_sass_line_continuation()?;
398        let ident: Ident = input.parse::<Ident>()?;
399        let span = Span { start: span.start, end: ident.span.end };
400        if ident.name.eq_ignore_ascii_case("important") {
401            Ok(ImportantAnnotation { ident, span })
402        } else {
403            Err(Error { kind: ErrorKind::ExpectImportantAnnotation, span })
404        }
405    }
406}
407
408// https://drafts.csswg.org/css-syntax-3/#consume-qualified-rule
409//
410// <qualified-rule> = <prelude> <{}-block>
411// In a style context the prelude is a selector list:
412//   <style-rule> = <selector-list> { <style-block> }
413impl<'a> Parse<'a> for QualifiedRule<'a> {
414    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
415        let selector_list = input
416            .with_state(ParserState {
417                qualified_rule_ctx: Some(QualifiedRuleContext::Selector),
418                ..input.state
419            })
420            .parse::<SelectorList>()?;
421        let block = input.parse::<SimpleBlock>()?;
422        let span = Span { start: selector_list.span.start, end: block.span.end };
423        Ok(QualifiedRule { selector: selector_list, block, span })
424    }
425}
426
427// https://drafts.csswg.org/css-syntax-3/#consume-block-contents
428//
429// <unknown-qualified-rule> = <ident-token> ':' <any-value> <{}-block>
430//
431// Two shapes end up here in CSS (section numbers: 2026-07 ED):
432// - a declaration §5.5.6 rejects because a `{}` block is mixed with other values
433//   (`BlockInDeclarationValue`); §5.5.5 then re-consumes it as a qualified rule
434// - a statement led by a token that can start neither a declaration nor an at-rule
435//   (`50% { }`); §5.5.5 consumes it as a qualified rule directly
436// Either way the prelude is no selector, so it is kept as raw tokens.
437// postcss keeps such rules too (postcss-nested-style dialects use the shape for nested config blocks),
438// and Prettier prints the prelude verbatim.
439impl<'a> Parse<'a> for UnknownQualifiedRule<'a> {
440    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
441        debug_assert!(input.syntax == Syntax::Css);
442        let prelude = input.parse_raw_prelude_tokens()?;
443        // The scan also stops at `;` / statement boundaries;
444        // `SimpleBlock`'s `expect_l_brace` rejects those, so only a block opener makes this shape.
445        let block = input.parse::<SimpleBlock>()?;
446        let span = Span { start: prelude.span.start, end: block.span.end };
447        Ok(UnknownQualifiedRule { prelude, block, span })
448    }
449}
450
451// https://drafts.csswg.org/css-syntax-3/#consume-simple-block
452//
453// <simple-block> = '{' <block-contents> '}'
454// (Sass indented syntax substitutes Indent/Dedent for the braces.)
455impl<'a> Parse<'a> for SimpleBlock<'a> {
456    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
457        let is_sass = input.syntax == Syntax::Sass;
458        let start = if is_sass {
459            // A continuation line deeper than this block's own level leaves a
460            // pending indent whose `Dedent` arrives before the block opens
461            // (`a,\n    b\n  c: d`); cancel those out first.
462            let drained = input.drain_sass_pending_dedents()?;
463            if let Some((_, span)) = input.cursor.eat_indent()? {
464                span.end
465            } else if drained
466                && input.sass_pending_indents == 0
467                && input.cursor.tokenizer.reopen_indent_level()
468            {
469                // The block's level sat between two known indents, so its
470                // `Indent` was never emitted; re-open it directly.
471                input.cursor.peek()?.span.start
472            } else if input.sass_pending_indents > 0 {
473                // The statement's clause consumed this block's `Indent` as a
474                // line continuation (`@each $a in\n  b, c\n  .x\n    ...`);
475                // enter the block "virtually" at that depth.
476                input.sass_pending_indents -= 1;
477                input.cursor.peek()?.span.start
478            } else {
479                let offset = input.cursor.peek()?.span.start;
480                return Ok(SimpleBlock {
481                    statements: input.vec(),
482                    span: Span { start: offset, end: offset },
483                });
484            }
485        } else {
486            input.cursor.expect_l_brace()?.1.start
487        };
488
489        let statements = input.parse_statements(/* is_top_level */ false)?;
490
491        // CSS Syntax: EOF closes all open constructs (a parse error, but the
492        // tree is valid — browsers accept unclosed blocks at EOF). The
493        // dialects' reference compilers reject them. Recovery is unchanged; the
494        // parse error is surfaced via `recoverable_errors` so downstream
495        // consumers can tell it apart from a properly closed block.
496        if input.syntax == Syntax::Css && matches!(input.cursor.peek()?.token, Token::Eof(..)) {
497            let end = input.cursor.peek()?.span.start;
498            input
499                .recoverable_errors
500                .push(Error { kind: ErrorKind::EofInBlock, span: Span { start, end: start + 1 } });
501            return Ok(SimpleBlock { statements, span: Span { start, end } });
502        }
503
504        if is_sass {
505            match input.cursor.bump()? {
506                TokenWithSpan { token: Token::Dedent(..) | Token::Eof(..), span } => {
507                    let end = statements.last().map_or(span.start, |last| last.span().end);
508                    Ok(SimpleBlock { statements, span: Span { start, end } })
509                }
510                TokenWithSpan { span, .. } => {
511                    Err(Error { kind: ErrorKind::ExpectDedentOrEof, span })
512                }
513            }
514        } else {
515            let end = input.cursor.expect_r_brace()?.1.end;
516            Ok(SimpleBlock { statements, span: Span { start, end } })
517        }
518    }
519}
520
521// https://drafts.csswg.org/css-syntax-3/#parse-a-stylesheet
522//
523// <stylesheet> = <rule-list>
524impl<'a> Parse<'a> for Stylesheet<'a> {
525    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
526        let statements = input.parse_statements(/* is_top_level */ true)?;
527        input.cursor.expect_eof()?;
528        Ok(Stylesheet { statements, span: Span { start: 0, end: input.source.len() } })
529    }
530}
531
532impl<'a> Parser<'a> {
533    /// `<declaration-value>` consumed as raw tokens (CSS Syntax "preserved
534    /// tokens"), balancing `()`/`[]`/`{}` pairs, until a top-level `;`, an
535    /// unbalanced closer, or a statement boundary. Used for custom-property
536    /// values and as the fallback for CSS values the typed grammar rejects.
537    ///
538    /// <https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value>
539    ///
540    /// `stop_at_top_level_brace` implements the CSS Nesting disambiguation: a
541    /// `{` at the top level of a normal declaration's value means the whole
542    /// construct is really a qualified rule, so the value must end there.
543    /// Custom properties are exempt (`--foo: {a:b}` is a valid value).
544    pub(super) fn parse_declaration_value_tokens(
545        &mut self,
546        stop_at_top_level_brace: bool,
547    ) -> PResult<oxc_allocator::Vec<'a, ComponentValue<'a>>> {
548        let mut values = self.vec_with_capacity(3);
549        let mut pairs = Vec::with_capacity(1);
550        // Span of the outermost currently-open pair (kept in sync with `pairs`
551        // going empty↔non-empty), so the EOF parse error can point at the
552        // opener like `SimpleBlock` does, not at the end of file.
553        let mut outermost_pair_span: Option<Span> = None;
554        loop {
555            match &self.cursor.peek()?.token {
556                Token::Dedent(..) | Token::Linebreak(..) => break,
557                // CSS Syntax: EOF closes any still-open `(`/`[`/`{` group; recovery
558                // is unchanged, but record the parse error so downstream consumers
559                // can tell it apart from a balanced value. Report the outermost
560                // unclosed opener; a raw-value `{` (e.g. `--x: {` — legal in custom
561                // properties) is a block, not a paren.
562                Token::Eof(..) => {
563                    if let (Some(pair), Some(span)) = (pairs.first(), outermost_pair_span) {
564                        let kind = match pair {
565                            crate::util::PairedToken::Brace => ErrorKind::EofInBlock,
566                            _ => ErrorKind::UnclosedParen,
567                        };
568                        self.recoverable_errors.push(Error { kind, span });
569                    }
570                    break;
571                }
572                Token::Semicolon(..) if pairs.is_empty() => {
573                    break;
574                }
575                Token::LBrace(..) if stop_at_top_level_brace && pairs.is_empty() => {
576                    break;
577                }
578                // An unterminated string survives as a preserved token (a parse
579                // error kept verbatim; CSS Syntax §4.3.5). The tokenizer emits a
580                // `BadStr` for both recoverable forms; recover the spec's split
581                // from where the string stopped — at EOF it is a `<string-token>`
582                // (canonically closable by appending the quote), at a newline a
583                // `<bad-string-token>`.
584                Token::BadStr(..) => {
585                    let span = self.cursor.peek()?.span;
586                    let kind = if span.end == self.source.len() {
587                        ErrorKind::UnterminatedString
588                    } else {
589                        ErrorKind::BadString
590                    };
591                    self.recoverable_errors.push(Error { kind, span });
592                }
593                // An interpolated string (e.g. `'#{$expr}'` inside
594                // `filter: progid:...`) must be parsed structurally:
595                // the tokenizer needs `scan_string_template` to resume
596                // the string after each `#{...}`, so consuming its
597                // tokens as a plain stream would mis-lex the rest.
598                Token::StrTemplate(..) => {
599                    values.push(ComponentValue::InterpolableStr(self.parse()?));
600                    continue;
601                }
602                token => {
603                    let was_empty = pairs.is_empty();
604                    if !crate::util::track_paired_token(token, &mut pairs) {
605                        break;
606                    }
607                    if was_empty && !pairs.is_empty() {
608                        outermost_pair_span = Some(self.cursor.peek()?.span);
609                    }
610                }
611            }
612            values.push(ComponentValue::TokenWithSpan(self.cursor.bump()?));
613        }
614        Ok(values)
615    }
616
617    // The typed form of `<declaration-value>`: a list of `<component-value>` up to
618    // the declaration terminator (`;`, `!`, `}`, or a statement boundary).
619    pub(super) fn parse_declaration_value(
620        &mut self,
621    ) -> PResult<oxc_allocator::Vec<'a, ComponentValue<'a>>> {
622        let mut values = self.vec_with_capacity(3);
623        loop {
624            match &self.cursor.peek()?.token {
625                Token::RBrace(..)
626                | Token::RParen(..)
627                | Token::Semicolon(..)
628                | Token::Dedent(..)
629                | Token::Linebreak(..)
630                | Token::Exclamation(..)
631                | Token::Eof(..) => break,
632                _ => {
633                    let value = self.parse::<ComponentValue>()?;
634                    match &value {
635                        ComponentValue::SassNestingDeclaration(..)
636                            if matches!(self.syntax, Syntax::Scss | Syntax::Sass) =>
637                        {
638                            values.push(value);
639                            break;
640                        }
641                        _ => values.push(value),
642                    }
643                }
644            }
645        }
646        Ok(values)
647    }
648
649    /// In a `@keyframes` body, an ident may start a keyframe block (`from {`)
650    /// or — in real-world code — a plain declaration (`blah: blee;`); dart-sass
651    /// accepts both. Returns the statement and whether it opened a block.
652    fn parse_keyframe_block_or_declaration(&mut self) -> PResult<(Statement<'a>, bool)> {
653        if let Ok(block) = self.try_parse(KeyframeBlock::parse) {
654            Ok((Statement::KeyframeBlock(block), true))
655        } else {
656            match self.parse_statement_declaration() {
657                Ok(decl) => Ok((Statement::Declaration(decl), false)),
658                Err(error_decl) => {
659                    // postcss accepts the declaration-shaped rule inside `@keyframes` too
660                    if let Some(rule) = self.try_declaration_shaped_rule(&error_decl) {
661                        return Ok((Statement::UnknownQualifiedRule(rule), true));
662                    }
663                    Err(error_decl)
664                }
665            }
666        }
667    }
668
669    /// A declaration in statement position.
670    /// CSS snapshots the statement start so `try_declaration_shaped_rule` can re-consume it;
671    /// dialects have no fallback, and an unrecovered Err aborts the parse, so no snapshot.
672    fn parse_statement_declaration(&mut self) -> PResult<Declaration<'a>> {
673        if self.syntax == Syntax::Css {
674            self.try_parse(Parser::parse_style_rule_declaration)
675        } else {
676            self.parse_style_rule_declaration()
677        }
678    }
679
680    /// The §5.5.5 re-consume (see `UnknownQualifiedRule`), CSS only: dialects
681    /// keep their reference compilers' strictness (Scss types the shape as
682    /// nested properties; less.js rejects it).
683    fn try_declaration_shaped_rule(
684        &mut self,
685        error_decl: &Error,
686    ) -> Option<UnknownQualifiedRule<'a>> {
687        if !matches!(error_decl.kind, ErrorKind::BlockInDeclarationValue)
688            || self.syntax != Syntax::Css
689        {
690            return None;
691        }
692        self.try_parse(UnknownQualifiedRule::parse).ok()
693    }
694
695    /// The CSS Nesting `<style-block>` ambiguity: parse a qualified rule, falling
696    /// back to a declaration when the `foo: bar` vs `foo { }` prelude is
697    /// ambiguous. Returns the statement and whether it opened a block (for the
698    /// caller's `is_block_element`).
699    /// `prefer_rule_error` picks which attempt's error surfaces when both fail
700    /// (inside a block the declaration's: `color red;` reports the missing `:`).
701    ///
702    /// <https://drafts.csswg.org/css-nesting-1/#syntax>
703    fn parse_rule_or_declaration(
704        &mut self,
705        is_top_level: bool,
706        prefer_rule_error: bool,
707    ) -> PResult<(Statement<'a>, bool)> {
708        match self.try_parse(QualifiedRule::parse) {
709            Ok(rule) => Ok((Statement::QualifiedRule(rule), true)),
710            Err(error_rule) => match self.parse_statement_declaration() {
711                Ok(decl) => {
712                    // Only Scss/Sass produce `SassNestingDeclaration`; in CSS this is
713                    // always `false`, matching the previous per-syntax behavior.
714                    let is_block_element = matches!(
715                        decl.value.last(),
716                        Some(ComponentValue::SassNestingDeclaration(..))
717                    );
718                    // A root declaration is a statement only in the css-in-js
719                    // parse mode (README "Acceptance").
720                    if is_top_level && self.options.template_placeholder.is_none() {
721                        self.recoverable_errors
722                            .push(Error { kind: ErrorKind::TopLevelDeclaration, span: decl.span });
723                    }
724                    Ok((Statement::Declaration(decl), is_block_element))
725                }
726                Err(error_decl) => {
727                    if let Some(rule) = self.try_declaration_shaped_rule(&error_decl) {
728                        return Ok((Statement::UnknownQualifiedRule(rule), true));
729                    }
730                    Err(if prefer_rule_error { error_rule } else { error_decl })
731                }
732            },
733        }
734    }
735
736    /// A Css statement led by anything but an ident or an at-keyword:
737    /// a qualified rule, a declaration with a postcss property name (`+color: red`),
738    /// or, numeric-led, a §5.5.5 qualified rule with no selector prelude
739    /// (`50% { }` inside a raw-prelude rule, oxc-project/oxc#26291). `"foo" {}` stays rejected.
740    fn parse_css_statement(&mut self, is_top_level: bool) -> PResult<(Statement<'a>, bool)> {
741        let TokenWithSpan { token, span } = self.cursor.peek()?;
742        let span = *span;
743        let numeric =
744            matches!(token, Token::Percentage(..) | Token::Number(..) | Token::Dimension(..));
745        // A non-ident lead is first of all a selector, so its rule error is
746        // the useful one (`[attr {` reports the attribute matcher, not a colon).
747        match self.parse_rule_or_declaration(is_top_level, true) {
748            Err(_) if numeric => {
749                let rule = self
750                    .parse::<UnknownQualifiedRule>()
751                    .map_err(|_| Error { kind: ErrorKind::ExpectRule, span })?;
752                Ok((Statement::UnknownQualifiedRule(rule), true))
753            }
754            result => result,
755        }
756    }
757
758    /// Parse a declaration in statement position (`ParserState::in_statement`);
759    /// feature-query declarations call `Declaration::parse` directly.
760    fn parse_style_rule_declaration(&mut self) -> PResult<Declaration<'a>> {
761        self.with_state(ParserState { in_statement: true, ..self.state.clone() }).parse()
762    }
763
764    // Block contents: a mix of declarations, nested style rules and at-rules
765    // (CSS Syntax `<block-contents>`; `is_top_level` selects the `<stylesheet>`
766    // rule-list, where a declaration is a `TopLevelDeclaration` error except
767    // in the css-in-js parse mode and in Less).
768    // https://drafts.csswg.org/css-syntax-3/#consume-block-contents
769    fn parse_statements(
770        &mut self,
771        is_top_level: bool,
772    ) -> PResult<oxc_allocator::Vec<'a, Statement<'a>>> {
773        let mut statements = self.vec_with_capacity(1);
774        loop {
775            // Set true for braced blocks AND `${}` placeholder statements: both
776            // make the trailing terminator optional. A placeholder substitutes a
777            // whole statement/declaration and, like postcss, needs no `;`, so the
778            // next statement may follow directly (`${mixin}\n@media {...}`,
779            // `${a} ${b}`, `${foo}: ${bar}`).
780            let mut is_block_element = false;
781            let TokenWithSpan { token, span } = self.cursor.peek()?;
782            match token {
783                Token::Ident(..) | Token::HashLBrace(..) | Token::AtLBraceVar(..) => {
784                    match self.syntax {
785                        Syntax::Css => {
786                            if self.state.in_keyframes_at_rule {
787                                let (stmt, is_block) =
788                                    self.parse_keyframe_block_or_declaration()?;
789                                is_block_element = is_block;
790                                statements.push(stmt);
791                            } else {
792                                let (stmt, is_block) =
793                                    self.parse_rule_or_declaration(is_top_level, is_top_level)?;
794                                is_block_element = is_block;
795                                statements.push(stmt);
796                            }
797                        }
798                        Syntax::Scss | Syntax::Sass => {
799                            if let Ok(sass_var_decl) =
800                                self.try_parse(SassVariableDeclaration::parse)
801                            {
802                                statements.push(Statement::SassVariableDeclaration(
803                                    self.alloc(sass_var_decl),
804                                ));
805                            } else if self.state.in_keyframes_at_rule {
806                                let (stmt, is_block) =
807                                    self.parse_keyframe_block_or_declaration()?;
808                                is_block_element = is_block;
809                                statements.push(stmt);
810                            } else {
811                                let (stmt, is_block) =
812                                    self.parse_rule_or_declaration(is_top_level, is_top_level)?;
813                                is_block_element = is_block;
814                                statements.push(stmt);
815                            }
816                        }
817                        Syntax::Less => {
818                            if let Ok(stmt) = self.try_parse(Parser::parse_less_qualified_rule) {
819                                statements.push(stmt);
820                                is_block_element = true;
821                            } else if let Ok(decl) =
822                                // less.js parses root-level declarations and
823                                // only rejects them at eval time.
824                                self.try_parse(Declaration::parse)
825                            {
826                                statements.push(Statement::Declaration(decl));
827                            } else if self.state.in_keyframes_at_rule {
828                                statements.push(Statement::KeyframeBlock(self.parse()?));
829                                is_block_element = true;
830                            } else {
831                                let fn_call = self.parse::<Function>()?;
832                                is_block_element = matches!(
833                                    fn_call.args.last(),
834                                    Some(ComponentValue::LessDetachedRuleset(..))
835                                );
836                                statements.push(Statement::LessFunctionCall(fn_call));
837                            }
838                        }
839                    }
840                }
841                // `5:-` — less.js's ruleProperty regex (`[_a-zA-Z0-9-]+`)
842                // allows digit-only declaration names
843                Token::Number(..)
844                    if self.syntax == Syntax::Less
845                        && !is_top_level
846                        && self.source.as_bytes().get(span.end) == Some(&b':') =>
847                {
848                    let decl = self.parse_style_rule_declaration()?;
849                    statements.push(Statement::Declaration(decl));
850                }
851                // `.3D(...)` — less.js allows digit-led mixin names, which
852                // arrive as one <dimension-token>; they behave exactly like
853                // `.foo` (`.3D ()`, `.3D;`), so only the leading `.` matters
854                Token::Dot(..) | Token::Hash(..) | Token::Dimension(..)
855                    if self.syntax == Syntax::Less
856                        && (!matches!(token, Token::Dimension(..))
857                            || self.source.as_bytes().get(span.start) == Some(&b'.')) =>
858                {
859                    let stmt = if let Ok(stmt) = self.try_parse(Parser::parse_less_qualified_rule) {
860                        is_block_element = true;
861                        stmt
862                    } else if let Ok(mixin_def) = self.try_parse(LessMixinDefinition::parse) {
863                        is_block_element = true;
864                        Statement::LessMixinDefinition(self.alloc(mixin_def))
865                    } else {
866                        self.parse().map(Statement::LessMixinCall)?
867                    };
868                    statements.push(stmt);
869                }
870                // Css takes every remaining lead token through `parse_css_statement` below.
871                Token::Dot(..) | Token::Hash(..)
872                    if self.syntax != Syntax::Css && !self.state.in_keyframes_at_rule =>
873                {
874                    statements.push(Statement::QualifiedRule(self.parse()?));
875                    is_block_element = true;
876                }
877                Token::Ampersand(..)
878                | Token::LBracket(..)
879                | Token::Colon(..)
880                | Token::ColonColon(..)
881                | Token::Asterisk(..)
882                | Token::Bar(..)
883                | Token::NumberSign(..)
884                    if self.syntax != Syntax::Css && !self.state.in_keyframes_at_rule =>
885                {
886                    if matches!(self.cursor.peek()?.token, Token::Asterisk(..)) {
887                        // `*color: red` / `*zoom: 1` (an IE<=7 hack) looks like a `*`
888                        // universal selector but is a declaration; try the rule, then
889                        // fall back to a declaration. (A `*` never starts a
890                        // `LessExtendRule`, so this can precede the Less split.)
891                        if self.syntax == Syntax::Less {
892                            match self.try_parse(Parser::parse_less_qualified_rule) {
893                                Ok(stmt) => {
894                                    statements.push(stmt);
895                                    is_block_element = true;
896                                }
897                                // less.js parses a root declaration (ident-led path above)
898                                // but not a root `*` hack; keep root-level `*zoom: 1` an error.
899                                Err(rule_err) if is_top_level => return Err(rule_err),
900                                Err(_) => {
901                                    let decl = self.parse_style_rule_declaration()?;
902                                    statements.push(Statement::Declaration(decl));
903                                }
904                            }
905                        } else {
906                            let (stmt, is_block) =
907                                self.parse_rule_or_declaration(is_top_level, is_top_level)?;
908                            is_block_element = is_block;
909                            statements.push(stmt);
910                        }
911                    } else if self.syntax == Syntax::Less {
912                        if let Ok(extend_rule) = self.try_parse(LessExtendRule::parse) {
913                            statements.push(Statement::LessExtendRule(extend_rule));
914                        } else {
915                            statements.push(self.parse_less_qualified_rule()?);
916                            is_block_element = true;
917                        }
918                    } else {
919                        statements.push(Statement::QualifiedRule(self.parse()?));
920                        is_block_element = true;
921                    }
922                }
923                Token::AtKeyword(..) => match self.syntax {
924                    Syntax::Css => {
925                        let at_rule = self.parse::<AtRule>()?;
926                        is_block_element = at_rule.block.is_some();
927                        statements.push(Statement::AtRule(at_rule));
928                    }
929                    Syntax::Scss | Syntax::Sass => {
930                        let at_keyword_name =
931                            self.cursor.peek()?.at_keyword(self.source).unwrap().ident.name();
932                        match &*at_keyword_name {
933                            "if" => {
934                                let sass_if_at_rule = self.parse()?;
935                                statements
936                                    .push(Statement::SassIfAtRule(self.alloc(sass_if_at_rule)));
937                                is_block_element = true;
938                            }
939                            "else" => {
940                                return Err(Error {
941                                    kind: ErrorKind::UnexpectedSassElseAtRule,
942                                    span: self.cursor.bump()?.span,
943                                });
944                            }
945                            _ => {
946                                let at_rule = self.parse::<AtRule>()?;
947                                is_block_element = at_rule.block.is_some();
948                                statements.push(Statement::AtRule(at_rule));
949                            }
950                        }
951                    }
952                    Syntax::Less => {
953                        if let Ok(less_variable_declaration) =
954                            self.try_parse(LessVariableDeclaration::parse)
955                        {
956                            is_block_element = matches!(
957                                less_variable_declaration.value,
958                                ComponentValue::LessDetachedRuleset(..)
959                            );
960                            statements.push(Statement::LessVariableDeclaration(
961                                self.alloc(less_variable_declaration),
962                            ));
963                        } else if let Ok(variable_call) = self.try_parse(LessVariableCall::parse) {
964                            statements.push(Statement::LessVariableCall(variable_call));
965                        } else {
966                            let at_rule = self.parse::<AtRule>()?;
967                            is_block_element = at_rule.block.is_some();
968                            statements.push(Statement::AtRule(at_rule));
969                        }
970                    }
971                },
972                Token::Placeholder(..) => {
973                    // A placeholder may start a qualified rule (a substituted
974                    // selector, e.g. CSS-in-JS `${Component} { ... }`) or stand
975                    // alone as a statement (e.g. `` `PLACEHOLDER-0`; ``).
976                    //
977                    // A placeholder-led selector must not absorb across a newline:
978                    // prettier keeps `${mixin}` on its own line and the following
979                    // selector as a separate rule (`${mixin}\n& > .x {}` is two
980                    // statements, not one). So only attempt the rule when the block
981                    // `{` is reachable without an intervening newline-then-selector.
982                    //
983                    // A placeholder may also be a declaration property name
984                    // (`${foo}: ${bar}`), so try a declaration before falling back
985                    // to a bare placeholder statement. Same-line only: the `:` of
986                    // a rule on the next line (`${mixin}\n:hover { ... }`) must
987                    // not be absorbed as a declaration colon — like the qualified
988                    // rule check above, a newline ends what the placeholder can own.
989                    let ph_end = self.cursor.peek()?.span.end;
990                    if self.placeholder_starts_qualified_rule(ph_end)
991                        && let Ok(rule) = self.try_parse(QualifiedRule::parse)
992                    {
993                        statements.push(Statement::QualifiedRule(rule));
994                        is_block_element = true;
995                    } else if self.placeholder_starts_declaration(ph_end)
996                        && let Ok(declaration) = self.try_parse(Declaration::parse)
997                    {
998                        // Reached only via the placeholder token above, so this
999                        // is the `${foo}: ${bar}` form (placeholder property name).
1000                        statements.push(Statement::Declaration(declaration));
1001                        is_block_element = true;
1002                    } else {
1003                        let (placeholder, span) = self.cursor.expect_placeholder()?;
1004                        statements.push(Statement::Placeholder((placeholder, span).into()));
1005                        is_block_element = true;
1006                    }
1007                }
1008                // Css too: postcss-extend-rule's `%thick-border {}`
1009                // (see the placeholder arm in `SimpleSelector`'s parser).
1010                Token::Percent(..)
1011                    if matches!(self.syntax, Syntax::Scss | Syntax::Sass | Syntax::Css) =>
1012                {
1013                    statements.push(Statement::QualifiedRule(self.parse()?));
1014                    is_block_element = true;
1015                }
1016                Token::DollarVar(..) if matches!(self.syntax, Syntax::Scss | Syntax::Sass) => {
1017                    let declaration = self.parse()?;
1018                    statements.push(Statement::SassVariableDeclaration(self.alloc(declaration)));
1019                }
1020                Token::DollarVar(..) if self.syntax == Syntax::Css => {
1021                    // Prefer the typed postcss-simple-vars node for an exact
1022                    // `$name: value` declaration. If the name continues
1023                    // (`$name+`, `$name.foo`) or a top-level block makes the
1024                    // statement a raw-prelude rule, use the general Css
1025                    // rule/declaration disambiguation instead.
1026                    if let Ok(declaration) = self.try_parse(PostcssSimpleVarDeclaration::parse) {
1027                        statements
1028                            .push(Statement::PostcssSimpleVarDeclaration(self.alloc(declaration)));
1029                    } else {
1030                        let (stmt, is_block) = self.parse_css_statement(is_top_level)?;
1031                        is_block_element = is_block;
1032                        statements.push(stmt);
1033                    }
1034                }
1035                // Indented-syntax shorthands: `=name` defines a mixin
1036                // (`@mixin name`) and `+name` includes one (`@include name`).
1037                // A spaced `+ b` stays a sibling-combinator selector: `+` is
1038                // an include only when glued to an identifier.
1039                Token::Equal(..) if self.syntax == Syntax::Sass => {
1040                    let eq_span = self.cursor.bump()?.span;
1041                    self.eat_sass_line_continuation()?;
1042                    let prelude = self.parse::<SassMixin>()?;
1043                    let block = self
1044                        .with_state(ParserState {
1045                            sass_ctx: self.state.sass_ctx
1046                                | super::state::SASS_CTX_ALLOW_KEYFRAME_BLOCK,
1047                            ..self.state.clone()
1048                        })
1049                        .parse::<SimpleBlock>()?;
1050                    let span = Span { start: eq_span.start, end: block.span.end };
1051                    statements.push(Statement::AtRule(AtRule {
1052                        name: Ident { name: "mixin", raw: "=", span: eq_span },
1053                        prelude: Some(AtRulePrelude::SassMixin(self.alloc(prelude))),
1054                        block: Some(block),
1055                        span,
1056                    }));
1057                    is_block_element = true;
1058                }
1059                Token::Plus(..)
1060                    if self.syntax == Syntax::Sass
1061                        && crate::tokenizer::ident_starts_at(self.source, span.end) =>
1062                {
1063                    let plus_span = self.cursor.bump()?.span;
1064                    let prelude = self.parse::<SassInclude>()?;
1065                    let block = if matches!(
1066                        self.cursor.peek()?.token,
1067                        Token::LBrace(..) | Token::Indent(..)
1068                    ) {
1069                        Some(
1070                            self.with_state(ParserState {
1071                                sass_ctx: self.state.sass_ctx
1072                                    | super::state::SASS_CTX_ALLOW_KEYFRAME_BLOCK,
1073                                ..self.state.clone()
1074                            })
1075                            .parse::<SimpleBlock>()?,
1076                        )
1077                    } else {
1078                        None
1079                    };
1080                    let end = block.as_ref().map_or(prelude.span.end, |block| block.span.end);
1081                    let span = Span { start: plus_span.start, end };
1082                    is_block_element = block.is_some();
1083                    statements.push(Statement::AtRule(AtRule {
1084                        name: Ident { name: "include", raw: "+", span: plus_span },
1085                        prelude: Some(AtRulePrelude::SassInclude(self.alloc(prelude))),
1086                        block,
1087                        span,
1088                    }));
1089                }
1090                Token::GreaterThan(..) | Token::Plus(..) | Token::Tilde(..) | Token::BarBar(..)
1091                    if self.syntax != Syntax::Css =>
1092                {
1093                    if self.syntax == Syntax::Less {
1094                        statements.push(self.parse_less_qualified_rule()?);
1095                    } else {
1096                        statements.push(Statement::QualifiedRule(self.parse()?));
1097                    }
1098                    is_block_element = true;
1099                }
1100                Token::DollarLBraceVar(..) if self.syntax == Syntax::Less => {
1101                    statements.push(self.parse().map(Statement::Declaration)?);
1102                }
1103                Token::Cdo(..) | Token::Cdc(..) => {
1104                    self.cursor.bump()?;
1105                    continue;
1106                }
1107                Token::At(..) if matches!(self.syntax, Syntax::Scss | Syntax::Sass) => {
1108                    let unknown_sass_at_rule = self.parse::<UnknownSassAtRule>()?;
1109                    is_block_element = unknown_sass_at_rule.block.is_some();
1110                    statements.push(Statement::UnknownSassAtRule(self.alloc(unknown_sass_at_rule)));
1111                }
1112                Token::Percentage(..)
1113                    if self.state.in_keyframes_at_rule
1114                        || self.state.sass_ctx & super::state::SASS_CTX_ALLOW_KEYFRAME_BLOCK
1115                            != 0
1116                        || self.state.less_ctx & super::state::LESS_CTX_ALLOW_KEYFRAME_BLOCK
1117                            != 0 =>
1118                {
1119                    statements.push(Statement::KeyframeBlock(self.parse()?));
1120                    is_block_element = true;
1121                }
1122                Token::RBrace(..) | Token::Eof(..) | Token::Dedent(..) => break,
1123                Token::Semicolon(..) | Token::Linebreak(..) => {
1124                    self.cursor.bump()?;
1125                    continue;
1126                }
1127                Token::LBrace(..) if self.syntax == Syntax::Css => {
1128                    // An empty selector (`{}`): postcss parses it as a qualified rule
1129                    // with no selector, so build one with an empty selector list.
1130                    let start = span.start;
1131                    let block = self.parse::<SimpleBlock>()?;
1132                    let selector = SelectorList {
1133                        selectors: self.vec(),
1134                        comma_spans: self.vec(),
1135                        span: Span { start, end: start },
1136                    };
1137                    let span = Span { start, end: block.span.end };
1138                    statements.push(Statement::QualifiedRule(QualifiedRule {
1139                        selector,
1140                        block,
1141                        span,
1142                    }));
1143                    is_block_element = true;
1144                }
1145                // `@3: red` is an at-word to postcss, not a property name.
1146                Token::At(..) if self.syntax == Syntax::Css => {
1147                    return Err(Error { kind: ErrorKind::ExpectRule, span: *span });
1148                }
1149                _ if self.syntax == Syntax::Css && !self.state.in_keyframes_at_rule => {
1150                    let (stmt, is_block) = self.parse_css_statement(is_top_level)?;
1151                    is_block_element = is_block;
1152                    statements.push(stmt);
1153                }
1154                _ => {
1155                    return Err(Error {
1156                        kind: if self.state.in_keyframes_at_rule {
1157                            ErrorKind::ExpectKeyframeBlock
1158                        } else {
1159                            ErrorKind::ExpectRule
1160                        },
1161                        span: *span,
1162                    });
1163                }
1164            };
1165            // Drain continuation indents that never became a block (e.g.
1166            // `$a\n  : b` — the deeper line belonged to the statement's own
1167            // clause, so its matching `Dedent` has no block to close). A
1168            // drained `Dedent` is itself a line boundary, so the statement
1169            // separator is already satisfied.
1170            if self.drain_sass_pending_dedents()? {
1171                continue;
1172            }
1173            match &self.cursor.peek()?.token {
1174                Token::RBrace(..) | Token::Eof(..) | Token::Dedent(..) => break,
1175                _ => {
1176                    if self.syntax == Syntax::Sass {
1177                        // The indented syntax also accepts `;` as a statement
1178                        // terminator/separator (`a; b`), like a newline.
1179                        if is_block_element {
1180                            if self.cursor.eat_semicolon()?.is_none() {
1181                                self.cursor.eat_linebreak()?;
1182                            }
1183                        } else if self.cursor.eat_semicolon()?.is_none() {
1184                            self.cursor.expect_linebreak()?;
1185                        }
1186                    } else if is_block_element {
1187                        self.cursor.eat_semicolon()?;
1188                    } else {
1189                        self.cursor.expect_semicolon()?;
1190                    }
1191                }
1192            }
1193        }
1194        Ok(statements)
1195    }
1196
1197    /// Whether a statement-position `${}` placeholder (ending at byte `from`)
1198    /// should be offered to `QualifiedRule::parse`. The css-in-js rule the parser
1199    /// can't see on its own, matching prettier:
1200    /// - a bare `{` after the placeholder IS absorbed — the placeholder is the
1201    ///   selector for that block (`${mixin}\n{ color: red }` is one rule; a bare
1202    ///   `{...}` is meaningless without a selector, so this is the only valid read)
1203    /// - a placeholder separated by whitespace from what follows, then a newline,
1204    ///   then selector content = a separate rule (`${mixin}\n& > .x {}` and
1205    ///   `${a} ${b}\nhtml {}` are two statements, not one — spaced placeholders
1206    ///   are typically mixin invocations, not selector pieces)
1207    /// - but a placeholder IMMEDIATELY glued to non-whitespace (e.g. `${p}:hover`
1208    ///   or `${p},`) is a compound-selector piece, so a multi-line selector list
1209    ///   (`${p}:hover &,\n${q}:focus &, { ... }`) is one rule — keep scanning for `{` across newlines.
1210    ///
1211    /// The real grammar (strings, comments, `#{...}` interpolations, validity) is
1212    /// left to `QualifiedRule::parse`, which runs next and rolls back if this guess was wrong.
1213    /// Deliberately NOT a tokenizer: it never early-exits on `;`/`}`
1214    /// (those may sit inside an attribute string or comment),
1215    /// so it can't misclassify a same-line selector containing them.
1216    fn placeholder_starts_qualified_rule(&self, from: usize) -> bool {
1217        let bytes = &self.source.as_bytes()[from..];
1218        // Immediately-adjacent non-whitespace (`${p}:hover`, `${p},`) means the
1219        // placeholder is a compound-selector piece: only `{` matters from here, regardless of newlines.
1220        if bytes.first().is_some_and(|b| !b.is_ascii_whitespace()) {
1221            return bytes.contains(&b'{');
1222        }
1223        // Otherwise the placeholder is separated by whitespace from what follows.
1224        // A `{` on the same line (whitespace-only prefix) still makes the
1225        // placeholder its selector; any non-whitespace after a newline starts a separate rule.
1226        let mut newline_seen = false;
1227        for &b in bytes {
1228            match b {
1229                b'{' => return true,
1230                // `\r`, `\r\n`, and `\n` all count as a newline (the tokenizer
1231                // treats a bare `\r` as a line break too).
1232                b'\n' | b'\r' => newline_seen = true,
1233                _ if b.is_ascii_whitespace() => {}
1234                _ if newline_seen => return false,
1235                _ => {}
1236            }
1237        }
1238        // No block at all -> a declaration or a bare placeholder, not a rule.
1239        false
1240    }
1241
1242    /// Whether a statement-position `${}` placeholder (ending at byte `from`)
1243    /// should be offered to `Declaration::parse` as a property name
1244    /// (`${foo}: ${bar}`). Same-line only: a newline before the next
1245    /// non-whitespace means the placeholder stands alone and what follows is a
1246    /// separate statement (`${mixin}\n\n:disabled { ... }` must not become a
1247    /// declaration `${mixin}: disabled { ... }`). Whether a same-line follower
1248    /// actually forms a declaration is left to `Declaration::parse`, which
1249    /// rolls back if this guess was wrong.
1250    fn placeholder_starts_declaration(&self, from: usize) -> bool {
1251        for &b in &self.source.as_bytes()[from..] {
1252            match b {
1253                // A bare `\r` counts as a newline too, same as
1254                // `placeholder_starts_qualified_rule` above.
1255                b'\n' | b'\r' => return false,
1256                _ if b.is_ascii_whitespace() => {}
1257                _ => return true,
1258            }
1259        }
1260        false
1261    }
1262}