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