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 TokenWithSpan { token: Token::Number(number), span } = input.cursor.peek()?
51            && number.raw.bytes().all(|b| b.is_ascii_digit())
52        {
53            let raw = number.raw;
54            let span = span.clone();
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 TokenWithSpan { token: Token::Hash(hash), span } = input.cursor.bump()? else {
64                unreachable!()
65            };
66            name_prefix = Some((span.start, '#'));
67            let name = if hash.escaped {
68                crate::util::handle_escape_in(hash.raw, input.allocator)
69            } else {
70                hash.raw
71            };
72            InterpolableIdent::Literal(Ident {
73                name,
74                raw: hash.raw,
75                span: Span { start: span.start + 1, end: span.end },
76            })
77        } else {
78            input
79                .with_state(ParserState {
80                    qualified_rule_ctx: Some(QualifiedRuleContext::DeclarationName),
81                    ..input.state
82                })
83                .parse::<InterpolableIdent>()?
84        };
85
86        // https://tailwindcss.com/docs/theme#overriding-the-default-theme
87        let name_suffix = if let TokenWithSpan { token: Token::Asterisk(..), span } =
88            input.cursor.peek()?
89            && name.span().end == span.start
90        {
91            input.cursor.bump()?;
92            Some('*')
93        } else {
94            None
95        };
96
97        // Less property merge (`prop+: v`, `prop+_: v`) — also accepted for
98        // plain CSS since Less serializes the flag into its output.
99        let less_property_merge =
100            if matches!(input.syntax, Syntax::Less | Syntax::Css) { input.parse()? } else { None };
101
102        let (_, colon_span) = input.cursor.expect_colon()?;
103        let (mut value, mut important) = {
104            let mut parser = input.with_state(ParserState {
105                qualified_rule_ctx: Some(QualifiedRuleContext::DeclarationValue),
106                ..input.state
107            });
108            match &name {
109                InterpolableIdent::Literal(ident)
110                    if ident.name.starts_with("--")
111                        || matches!(
112                            &parser.cursor.peek()?.token,
113                            // for IE-compatibility, regardless of the property
114                            // name (`filter`, `-ms-filter`, vendor variants...):
115                            // filter: progid:DXImageTransform.Microsoft...
116                            Token::Ident(ident) if ident.name().eq_ignore_ascii_case("progid")
117                        ) =>
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 {
151                                kind: ErrorKind::ExpectComponentValue,
152                                span: next.span.clone(),
153                            })
154                        }
155                    });
156                    match typed {
157                        Ok(value_and_important) => value_and_important,
158                        Err(error) => {
159                            // A CSS custom function body holds declarations
160                            // only, so a top-level `{}` there is part of the
161                            // value; elsewhere it means this construct is
162                            // really a qualified rule (CSS Nesting
163                            // disambiguation) and the declaration is rejected.
164                            let in_fn_body = parser.state.in_css_function_body;
165                            let values = parser.parse_declaration_value_tokens(!in_fn_body)?;
166                            if !in_fn_body && let Token::LBrace(..) = &parser.cursor.peek()?.token {
167                                return Err(error);
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-simple-block
277//
278// <simple-block> = '{' <block-contents> '}'
279// (Sass indented syntax substitutes Indent/Dedent for the braces.)
280impl<'a> Parse<'a> for SimpleBlock<'a> {
281    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
282        let is_sass = input.syntax == Syntax::Sass;
283        let start = if is_sass {
284            // A continuation line deeper than this block's own level leaves a
285            // pending indent whose `Dedent` arrives before the block opens
286            // (`a,\n    b\n  c: d`); cancel those out first.
287            let drained = input.drain_sass_pending_dedents()?;
288            if let Some((_, span)) = input.cursor.eat_indent()? {
289                span.end
290            } else if drained
291                && input.sass_pending_indents == 0
292                && input.cursor.tokenizer.reopen_indent_level()
293            {
294                // The block's level sat between two known indents, so its
295                // `Indent` was never emitted; re-open it directly.
296                input.cursor.peek()?.span.start
297            } else if input.sass_pending_indents > 0 {
298                // The statement's clause consumed this block's `Indent` as a
299                // line continuation (`@each $a in\n  b, c\n  .x\n    ...`);
300                // enter the block "virtually" at that depth.
301                input.sass_pending_indents -= 1;
302                input.cursor.peek()?.span.start
303            } else {
304                let offset = input.cursor.peek()?.span.start;
305                return Ok(SimpleBlock {
306                    statements: input.vec(),
307                    span: Span { start: offset, end: offset },
308                });
309            }
310        } else {
311            input.cursor.expect_l_brace()?.1.start
312        };
313
314        let statements = input.parse_statements(/* is_top_level */ false)?;
315
316        // CSS Syntax: EOF closes all open constructs (a parse error, but the
317        // tree is valid — browsers accept unclosed blocks at EOF). The
318        // dialects' reference compilers reject them.
319        if input.syntax == Syntax::Css && matches!(input.cursor.peek()?.token, Token::Eof(..)) {
320            let end = input.cursor.peek()?.span.start;
321            return Ok(SimpleBlock { statements, span: Span { start, end } });
322        }
323
324        if is_sass {
325            match input.cursor.bump()? {
326                TokenWithSpan { token: Token::Dedent(..) | Token::Eof(..), span } => {
327                    let end = statements.last().map_or(span.start, |last| last.span().end);
328                    Ok(SimpleBlock { statements, span: Span { start, end } })
329                }
330                TokenWithSpan { span, .. } => {
331                    Err(Error { kind: ErrorKind::ExpectDedentOrEof, span })
332                }
333            }
334        } else {
335            let end = input.cursor.expect_r_brace()?.1.end;
336            Ok(SimpleBlock { statements, span: Span { start, end } })
337        }
338    }
339}
340
341// https://drafts.csswg.org/css-syntax-3/#parse-a-stylesheet
342//
343// <stylesheet> = <rule-list>
344impl<'a> Parse<'a> for Stylesheet<'a> {
345    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
346        let statements = input.parse_statements(/* is_top_level */ true)?;
347        input.cursor.expect_eof()?;
348        Ok(Stylesheet { statements, span: Span { start: 0, end: input.source.len() } })
349    }
350}
351
352impl<'a> Parser<'a> {
353    /// `<declaration-value>` consumed as raw tokens (CSS Syntax "preserved
354    /// tokens"), balancing `()`/`[]`/`{}` pairs, until a top-level `;`, an
355    /// unbalanced closer, or a statement boundary. Used for custom-property
356    /// values and as the fallback for CSS values the typed grammar rejects.
357    ///
358    /// <https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value>
359    ///
360    /// `stop_at_top_level_brace` implements the CSS Nesting disambiguation: a
361    /// `{` at the top level of a normal declaration's value means the whole
362    /// construct is really a qualified rule, so the value must end there.
363    /// Custom properties are exempt (`--foo: {a:b}` is a valid value).
364    pub(super) fn parse_declaration_value_tokens(
365        &mut self,
366        stop_at_top_level_brace: bool,
367    ) -> PResult<oxc_allocator::Vec<'a, ComponentValue<'a>>> {
368        let mut values = self.vec_with_capacity(3);
369        let mut pairs = Vec::with_capacity(1);
370        loop {
371            match &self.cursor.peek()?.token {
372                Token::Dedent(..) | Token::Linebreak(..) | Token::Eof(..) => break,
373                Token::Semicolon(..) if pairs.is_empty() => {
374                    break;
375                }
376                Token::LBrace(..) if stop_at_top_level_brace && pairs.is_empty() => {
377                    break;
378                }
379                // An interpolated string (e.g. `'#{$expr}'` inside
380                // `filter: progid:...`) must be parsed structurally:
381                // the tokenizer needs `scan_string_template` to resume
382                // the string after each `#{...}`, so consuming its
383                // tokens as a plain stream would mis-lex the rest.
384                Token::StrTemplate(..) => {
385                    values.push(ComponentValue::InterpolableStr(self.parse()?));
386                    continue;
387                }
388                token => {
389                    if !crate::util::track_paired_token(token, &mut pairs) {
390                        break;
391                    }
392                }
393            }
394            values.push(ComponentValue::TokenWithSpan(self.cursor.bump()?));
395        }
396        Ok(values)
397    }
398
399    // The typed form of `<declaration-value>`: a list of `<component-value>` up to
400    // the declaration terminator (`;`, `!`, `}`, or a statement boundary).
401    pub(super) fn parse_declaration_value(
402        &mut self,
403    ) -> PResult<oxc_allocator::Vec<'a, ComponentValue<'a>>> {
404        let mut values = self.vec_with_capacity(3);
405        loop {
406            match &self.cursor.peek()?.token {
407                Token::RBrace(..)
408                | Token::RParen(..)
409                | Token::Semicolon(..)
410                | Token::Dedent(..)
411                | Token::Linebreak(..)
412                | Token::Exclamation(..)
413                | Token::Eof(..) => break,
414                _ => {
415                    let value = self.parse::<ComponentValue>()?;
416                    match &value {
417                        ComponentValue::SassNestingDeclaration(..)
418                            if matches!(self.syntax, Syntax::Scss | Syntax::Sass) =>
419                        {
420                            values.push(value);
421                            break;
422                        }
423                        _ => values.push(value),
424                    }
425                }
426            }
427        }
428        Ok(values)
429    }
430
431    /// In a `@keyframes` body, an ident may start a keyframe block (`from {`)
432    /// or — in real-world code — a plain declaration (`blah: blee;`); dart-sass
433    /// accepts both. Returns the statement and whether it opened a block.
434    fn parse_keyframe_block_or_declaration(&mut self) -> PResult<(Statement<'a>, bool)> {
435        if let Ok(block) = self.try_parse(KeyframeBlock::parse) {
436            Ok((Statement::KeyframeBlock(block), true))
437        } else {
438            let decl = self.parse_style_rule_declaration()?;
439            Ok((Statement::Declaration(decl), false))
440        }
441    }
442
443    /// The CSS Nesting `<style-block>` ambiguity: parse a qualified rule, falling
444    /// back to a declaration when the `foo: bar` vs `foo { }` prelude is
445    /// ambiguous. Returns the statement and whether it opened a block (for the
446    /// caller's `is_block_element`).
447    ///
448    /// <https://drafts.csswg.org/css-nesting-1/#syntax>
449    fn parse_rule_or_declaration(&mut self, is_top_level: bool) -> PResult<(Statement<'a>, bool)> {
450        match self.try_parse(QualifiedRule::parse) {
451            Ok(rule) => Ok((Statement::QualifiedRule(rule), true)),
452            Err(error_rule) => match self.parse_style_rule_declaration() {
453                Ok(decl) => {
454                    // Only Scss/Sass produce `SassNestingDeclaration`; in CSS this is
455                    // always `false`, matching the previous per-syntax behavior.
456                    let is_block_element = matches!(
457                        decl.value.last(),
458                        Some(ComponentValue::SassNestingDeclaration(..))
459                    );
460                    if is_top_level {
461                        self.recoverable_errors.push(Error {
462                            kind: ErrorKind::TopLevelDeclaration,
463                            span: decl.span.clone(),
464                        });
465                    }
466                    Ok((Statement::Declaration(decl), is_block_element))
467                }
468                Err(error_decl) => Err(if is_top_level { error_rule } else { error_decl }),
469            },
470        }
471    }
472
473    /// Parse a declaration that is a statement in a style-rule block, enabling the
474    /// IE `*color` hack (see `ParserState::allow_ie_star_hack`). Feature-query
475    /// declarations (`@supports`, `@container style()`, `@import supports()`) call
476    /// `Declaration::parse` directly and so never enable it.
477    fn parse_style_rule_declaration(&mut self) -> PResult<Declaration<'a>> {
478        self.with_state(ParserState { allow_ie_star_hack: true, ..self.state.clone() }).parse()
479    }
480
481    // Block contents: a mix of declarations, nested style rules and at-rules
482    // (CSS Syntax `<block-contents>`; `is_top_level` selects the `<stylesheet>`
483    // rule-list, which has no declarations).
484    // https://drafts.csswg.org/css-syntax-3/#consume-block-contents
485    fn parse_statements(
486        &mut self,
487        is_top_level: bool,
488    ) -> PResult<oxc_allocator::Vec<'a, Statement<'a>>> {
489        let mut statements = self.vec_with_capacity(1);
490        loop {
491            // Set true for braced blocks AND `${}` placeholder statements: both
492            // make the trailing terminator optional. A placeholder substitutes a
493            // whole statement/declaration and, like postcss, needs no `;`, so the
494            // next statement may follow directly (`${mixin}\n@media {...}`,
495            // `${a} ${b}`, `${foo}: ${bar}`).
496            let mut is_block_element = false;
497            let TokenWithSpan { token, span } = self.cursor.peek()?;
498            match token {
499                Token::Ident(..) | Token::HashLBrace(..) | Token::AtLBraceVar(..) => {
500                    match self.syntax {
501                        Syntax::Css => {
502                            if self.state.in_keyframes_at_rule {
503                                let (stmt, is_block) =
504                                    self.parse_keyframe_block_or_declaration()?;
505                                is_block_element = is_block;
506                                statements.push(stmt);
507                            } else {
508                                let (stmt, is_block) =
509                                    self.parse_rule_or_declaration(is_top_level)?;
510                                is_block_element = is_block;
511                                statements.push(stmt);
512                            }
513                        }
514                        Syntax::Scss | Syntax::Sass => {
515                            if let Ok(sass_var_decl) =
516                                self.try_parse(SassVariableDeclaration::parse)
517                            {
518                                statements.push(Statement::SassVariableDeclaration(
519                                    self.alloc(sass_var_decl),
520                                ));
521                            } else if self.state.in_keyframes_at_rule {
522                                let (stmt, is_block) =
523                                    self.parse_keyframe_block_or_declaration()?;
524                                is_block_element = is_block;
525                                statements.push(stmt);
526                            } else {
527                                let (stmt, is_block) =
528                                    self.parse_rule_or_declaration(is_top_level)?;
529                                is_block_element = is_block;
530                                statements.push(stmt);
531                            }
532                        }
533                        Syntax::Less => {
534                            if let Ok(stmt) = self.try_parse(Parser::parse_less_qualified_rule) {
535                                statements.push(stmt);
536                                is_block_element = true;
537                            } else if let Ok(decl) =
538                                // less.js parses root-level declarations and
539                                // only rejects them at eval time.
540                                self.try_parse(Declaration::parse)
541                            {
542                                statements.push(Statement::Declaration(decl));
543                            } else if self.state.in_keyframes_at_rule {
544                                statements.push(Statement::KeyframeBlock(self.parse()?));
545                                is_block_element = true;
546                            } else {
547                                let fn_call = self.parse::<Function>()?;
548                                is_block_element = matches!(
549                                    fn_call.args.last(),
550                                    Some(ComponentValue::LessDetachedRuleset(..))
551                                );
552                                statements.push(Statement::LessFunctionCall(fn_call));
553                            }
554                        }
555                    }
556                }
557                // `5:-` — less.js's ruleProperty regex (`[_a-zA-Z0-9-]+`)
558                // allows digit-only declaration names
559                Token::Number(..)
560                    if self.syntax == Syntax::Less
561                        && !is_top_level
562                        && self.source.as_bytes().get(span.end) == Some(&b':') =>
563                {
564                    let decl = self.parse_style_rule_declaration()?;
565                    statements.push(Statement::Declaration(decl));
566                }
567                // `.3D(...)` — less.js allows digit-led mixin names, which
568                // arrive as one <dimension-token>; they behave exactly like
569                // `.foo` (`.3D ()`, `.3D;`), so only the leading `.` matters
570                Token::Dot(..) | Token::Hash(..) | Token::Dimension(..)
571                    if self.syntax == Syntax::Less
572                        && (!matches!(token, Token::Dimension(..))
573                            || self.source.as_bytes().get(span.start) == Some(&b'.')) =>
574                {
575                    let stmt = if let Ok(stmt) = self.try_parse(Parser::parse_less_qualified_rule) {
576                        is_block_element = true;
577                        stmt
578                    } else if let Ok(mixin_def) = self.try_parse(LessMixinDefinition::parse) {
579                        is_block_element = true;
580                        Statement::LessMixinDefinition(self.alloc(mixin_def))
581                    } else {
582                        self.parse().map(Statement::LessMixinCall)?
583                    };
584                    statements.push(stmt);
585                }
586                Token::Dot(..) | Token::Hash(..) if !self.state.in_keyframes_at_rule => {
587                    if self.syntax == Syntax::Css {
588                        let (stmt, is_block) = self.parse_rule_or_declaration(is_top_level)?;
589                        is_block_element = is_block;
590                        statements.push(stmt);
591                    } else {
592                        statements.push(Statement::QualifiedRule(self.parse()?));
593                        is_block_element = true;
594                    }
595                }
596                Token::Ampersand(..)
597                | Token::LBracket(..)
598                | Token::Colon(..)
599                | Token::ColonColon(..)
600                | Token::Asterisk(..)
601                | Token::Bar(..)
602                | Token::NumberSign(..)
603                    if !self.state.in_keyframes_at_rule =>
604                {
605                    if matches!(self.cursor.peek()?.token, Token::Asterisk(..)) {
606                        // `*color: red` / `*zoom: 1` (an IE<=7 hack) looks like a `*`
607                        // universal selector but is a declaration; try the rule, then
608                        // fall back to a declaration. (A `*` never starts a
609                        // `LessExtendRule`, so this can precede the Less split.)
610                        if self.syntax == Syntax::Less {
611                            match self.try_parse(Parser::parse_less_qualified_rule) {
612                                Ok(stmt) => {
613                                    statements.push(stmt);
614                                    is_block_element = true;
615                                }
616                                // Less refuses declarations at the top level, like the
617                                // ident-led path; keep root-level `*zoom: 1` an error.
618                                Err(rule_err) if is_top_level => return Err(rule_err),
619                                Err(_) => {
620                                    let decl = self.parse_style_rule_declaration()?;
621                                    statements.push(Statement::Declaration(decl));
622                                }
623                            }
624                        } else {
625                            let (stmt, is_block) = self.parse_rule_or_declaration(is_top_level)?;
626                            is_block_element = is_block;
627                            statements.push(stmt);
628                        }
629                    } else if self.syntax == Syntax::Less {
630                        if let Ok(extend_rule) = self.try_parse(LessExtendRule::parse) {
631                            statements.push(Statement::LessExtendRule(extend_rule));
632                        } else {
633                            statements.push(self.parse_less_qualified_rule()?);
634                            is_block_element = true;
635                        }
636                    } else if self.syntax == Syntax::Css
637                        && matches!(self.cursor.peek()?.token, Token::Colon(..))
638                    {
639                        let (stmt, is_block) = self.parse_rule_or_declaration(is_top_level)?;
640                        is_block_element = is_block;
641                        statements.push(stmt);
642                    } else {
643                        statements.push(Statement::QualifiedRule(self.parse()?));
644                        is_block_element = true;
645                    }
646                }
647                Token::AtKeyword(at_keyword) => match self.syntax {
648                    Syntax::Css => {
649                        let at_rule = self.parse::<AtRule>()?;
650                        is_block_element = at_rule.block.is_some();
651                        statements.push(Statement::AtRule(at_rule));
652                    }
653                    Syntax::Scss | Syntax::Sass => {
654                        let at_keyword_name = at_keyword.ident.name();
655                        match &*at_keyword_name {
656                            "if" => {
657                                let sass_if_at_rule = self.parse()?;
658                                statements
659                                    .push(Statement::SassIfAtRule(self.alloc(sass_if_at_rule)));
660                                is_block_element = true;
661                            }
662                            "else" => {
663                                return Err(Error {
664                                    kind: ErrorKind::UnexpectedSassElseAtRule,
665                                    span: self.cursor.bump()?.span,
666                                });
667                            }
668                            _ => {
669                                let at_rule = self.parse::<AtRule>()?;
670                                is_block_element = at_rule.block.is_some();
671                                statements.push(Statement::AtRule(at_rule));
672                            }
673                        }
674                    }
675                    Syntax::Less => {
676                        if let Ok(less_variable_declaration) =
677                            self.try_parse(LessVariableDeclaration::parse)
678                        {
679                            is_block_element = matches!(
680                                less_variable_declaration.value,
681                                ComponentValue::LessDetachedRuleset(..)
682                            );
683                            statements.push(Statement::LessVariableDeclaration(
684                                self.alloc(less_variable_declaration),
685                            ));
686                        } else if let Ok(variable_call) = self.try_parse(LessVariableCall::parse) {
687                            statements.push(Statement::LessVariableCall(variable_call));
688                        } else {
689                            let at_rule = self.parse::<AtRule>()?;
690                            is_block_element = at_rule.block.is_some();
691                            statements.push(Statement::AtRule(at_rule));
692                        }
693                    }
694                },
695                Token::Placeholder(..) => {
696                    // A placeholder may start a qualified rule (a substituted
697                    // selector, e.g. CSS-in-JS `${Component} { ... }`) or stand
698                    // alone as a statement (e.g. `` `PLACEHOLDER-0`; ``).
699                    //
700                    // A placeholder-led selector must not absorb across a newline:
701                    // prettier keeps `${mixin}` on its own line and the following
702                    // selector as a separate rule (`${mixin}\n& > .x {}` is two
703                    // statements, not one). So only attempt the rule when the block
704                    // `{` is reachable without an intervening newline-then-selector.
705                    //
706                    // A placeholder may also be a declaration property name
707                    // (`${foo}: ${bar}`), so try a declaration before falling back
708                    // to a bare placeholder statement.
709                    let ph_end = self.cursor.peek()?.span.end;
710                    if self.placeholder_starts_qualified_rule(ph_end)
711                        && let Ok(rule) = self.try_parse(QualifiedRule::parse)
712                    {
713                        statements.push(Statement::QualifiedRule(rule));
714                        is_block_element = true;
715                    } else if let Ok(declaration) = self.try_parse(Declaration::parse) {
716                        // Reached only via the placeholder token above, so this
717                        // is the `${foo}: ${bar}` form (placeholder property name).
718                        statements.push(Statement::Declaration(declaration));
719                        is_block_element = true;
720                    } else {
721                        let (placeholder, span) = self.cursor.expect_placeholder()?;
722                        statements.push(Statement::Placeholder((placeholder, span).into()));
723                        is_block_element = true;
724                    }
725                }
726                // Css too: postcss-extend-rule's `%thick-border {}`
727                // (see the placeholder arm in `SimpleSelector`'s parser).
728                Token::Percent(..)
729                    if matches!(self.syntax, Syntax::Scss | Syntax::Sass | Syntax::Css) =>
730                {
731                    statements.push(Statement::QualifiedRule(self.parse()?));
732                    is_block_element = true;
733                }
734                Token::DollarVar(..) if matches!(self.syntax, Syntax::Scss | Syntax::Sass) => {
735                    let declaration = self.parse()?;
736                    statements.push(Statement::SassVariableDeclaration(self.alloc(declaration)));
737                }
738                Token::DollarVar(..)
739                    if self.syntax == Syntax::Css && self.options.allow_postcss_simple_vars =>
740                {
741                    let declaration = self.parse()?;
742                    statements
743                        .push(Statement::PostcssSimpleVarDeclaration(self.alloc(declaration)));
744                }
745                // Indented-syntax shorthands: `=name` defines a mixin
746                // (`@mixin name`) and `+name` includes one (`@include name`).
747                // A spaced `+ b` stays a sibling-combinator selector: `+` is
748                // an include only when glued to an identifier.
749                Token::Equal(..) if self.syntax == Syntax::Sass => {
750                    let eq_span = self.cursor.bump()?.span;
751                    self.eat_sass_line_continuation()?;
752                    let prelude = self.parse::<SassMixin>()?;
753                    let block = self
754                        .with_state(ParserState {
755                            sass_ctx: self.state.sass_ctx
756                                | super::state::SASS_CTX_ALLOW_KEYFRAME_BLOCK,
757                            ..self.state.clone()
758                        })
759                        .parse::<SimpleBlock>()?;
760                    let span = Span { start: eq_span.start, end: block.span.end };
761                    statements.push(Statement::AtRule(AtRule {
762                        name: Ident { name: "mixin", raw: "=", span: eq_span },
763                        prelude: Some(AtRulePrelude::SassMixin(self.alloc(prelude))),
764                        block: Some(block),
765                        span,
766                    }));
767                    is_block_element = true;
768                }
769                Token::Plus(..)
770                    if self.syntax == Syntax::Sass
771                        && crate::tokenizer::ident_starts_at(self.source, span.end) =>
772                {
773                    let plus_span = self.cursor.bump()?.span;
774                    let prelude = self.parse::<SassInclude>()?;
775                    let block = if matches!(
776                        self.cursor.peek()?.token,
777                        Token::LBrace(..) | Token::Indent(..)
778                    ) {
779                        Some(
780                            self.with_state(ParserState {
781                                sass_ctx: self.state.sass_ctx
782                                    | super::state::SASS_CTX_ALLOW_KEYFRAME_BLOCK,
783                                ..self.state.clone()
784                            })
785                            .parse::<SimpleBlock>()?,
786                        )
787                    } else {
788                        None
789                    };
790                    let end = block.as_ref().map_or(prelude.span.end, |block| block.span.end);
791                    let span = Span { start: plus_span.start, end };
792                    is_block_element = block.is_some();
793                    statements.push(Statement::AtRule(AtRule {
794                        name: Ident { name: "include", raw: "+", span: plus_span },
795                        prelude: Some(AtRulePrelude::SassInclude(self.alloc(prelude))),
796                        block,
797                        span,
798                    }));
799                }
800                Token::GreaterThan(..) | Token::Plus(..) | Token::Tilde(..) | Token::BarBar(..) => {
801                    if self.syntax == Syntax::Less {
802                        statements.push(self.parse_less_qualified_rule()?);
803                    } else {
804                        statements.push(Statement::QualifiedRule(self.parse()?));
805                    }
806                    is_block_element = true;
807                }
808                Token::DollarLBraceVar(..) if self.syntax == Syntax::Less => {
809                    statements.push(self.parse().map(Statement::Declaration)?);
810                }
811                Token::Cdo(..) | Token::Cdc(..) => {
812                    self.cursor.bump()?;
813                    continue;
814                }
815                Token::At(..) if matches!(self.syntax, Syntax::Scss | Syntax::Sass) => {
816                    let unknown_sass_at_rule = self.parse::<UnknownSassAtRule>()?;
817                    is_block_element = unknown_sass_at_rule.block.is_some();
818                    statements.push(Statement::UnknownSassAtRule(self.alloc(unknown_sass_at_rule)));
819                }
820                Token::Percentage(..)
821                    if self.state.in_keyframes_at_rule
822                        || self.state.sass_ctx & super::state::SASS_CTX_ALLOW_KEYFRAME_BLOCK
823                            != 0
824                        || self.state.less_ctx & super::state::LESS_CTX_ALLOW_KEYFRAME_BLOCK
825                            != 0 =>
826                {
827                    statements.push(Statement::KeyframeBlock(self.parse()?));
828                    is_block_element = true;
829                }
830                Token::RBrace(..) | Token::Eof(..) | Token::Dedent(..) => break,
831                Token::Semicolon(..) | Token::Linebreak(..) => {
832                    self.cursor.bump()?;
833                    continue;
834                }
835                Token::LBrace(..) if self.syntax == Syntax::Css => {
836                    // An empty selector (`{}`): postcss parses it as a qualified rule
837                    // with no selector, so build one with an empty selector list.
838                    let start = span.start;
839                    let block = self.parse::<SimpleBlock>()?;
840                    let selector = SelectorList {
841                        selectors: self.vec(),
842                        comma_spans: self.vec(),
843                        span: Span { start, end: start },
844                    };
845                    let span = Span { start, end: block.span.end };
846                    statements.push(Statement::QualifiedRule(QualifiedRule {
847                        selector,
848                        block,
849                        span,
850                    }));
851                    is_block_element = true;
852                }
853                _ => {
854                    return Err(Error {
855                        kind: if self.state.in_keyframes_at_rule {
856                            ErrorKind::ExpectKeyframeBlock
857                        } else {
858                            ErrorKind::ExpectRule
859                        },
860                        span: span.clone(),
861                    });
862                }
863            };
864            // Drain continuation indents that never became a block (e.g.
865            // `$a\n  : b` — the deeper line belonged to the statement's own
866            // clause, so its matching `Dedent` has no block to close). A
867            // drained `Dedent` is itself a line boundary, so the statement
868            // separator is already satisfied.
869            if self.drain_sass_pending_dedents()? {
870                continue;
871            }
872            match &self.cursor.peek()?.token {
873                Token::RBrace(..) | Token::Eof(..) | Token::Dedent(..) => break,
874                _ => {
875                    if self.syntax == Syntax::Sass {
876                        // The indented syntax also accepts `;` as a statement
877                        // terminator/separator (`a; b`), like a newline.
878                        if is_block_element {
879                            if self.cursor.eat_semicolon()?.is_none() {
880                                self.cursor.eat_linebreak()?;
881                            }
882                        } else if self.cursor.eat_semicolon()?.is_none() {
883                            self.cursor.expect_linebreak()?;
884                        }
885                    } else if is_block_element {
886                        self.cursor.eat_semicolon()?;
887                    } else {
888                        self.cursor.expect_semicolon()?;
889                    }
890                }
891            }
892        }
893        Ok(statements)
894    }
895
896    /// Whether a statement-position `${}` placeholder (ending at byte `from`)
897    /// should be offered to `QualifiedRule::parse`. The css-in-js rule the parser
898    /// can't see on its own, matching prettier:
899    /// - a bare `{` after the placeholder IS absorbed — the placeholder is the
900    ///   selector for that block (`${mixin}\n{ color: red }` is one rule; a bare
901    ///   `{...}` is meaningless without a selector, so this is the only valid read)
902    /// - a placeholder separated by whitespace from what follows, then a newline,
903    ///   then selector content = a separate rule (`${mixin}\n& > .x {}` and
904    ///   `${a} ${b}\nhtml {}` are two statements, not one — spaced placeholders
905    ///   are typically mixin invocations, not selector pieces)
906    /// - but a placeholder IMMEDIATELY glued to non-whitespace (e.g. `${p}:hover`
907    ///   or `${p},`) is a compound-selector piece, so a multi-line selector list
908    ///   (`${p}:hover &,\n${q}:focus &, { ... }`) is one rule — keep scanning for `{` across newlines.
909    ///
910    /// The real grammar (strings, comments, `#{...}` interpolations, validity) is
911    /// left to `QualifiedRule::parse`, which runs next and rolls back if this guess was wrong.
912    /// Deliberately NOT a tokenizer: it never early-exits on `;`/`}`
913    /// (those may sit inside an attribute string or comment),
914    /// so it can't misclassify a same-line selector containing them.
915    fn placeholder_starts_qualified_rule(&self, from: usize) -> bool {
916        let bytes = &self.source.as_bytes()[from..];
917        // Immediately-adjacent non-whitespace (`${p}:hover`, `${p},`) means the
918        // placeholder is a compound-selector piece: only `{` matters from here, regardless of newlines.
919        if bytes.first().is_some_and(|b| !b.is_ascii_whitespace()) {
920            return bytes.contains(&b'{');
921        }
922        // Otherwise the placeholder is separated by whitespace from what follows.
923        // A `{` on the same line (whitespace-only prefix) still makes the
924        // placeholder its selector; any non-whitespace after a newline starts a separate rule.
925        let mut newline_seen = false;
926        for &b in bytes {
927            match b {
928                b'{' => return true,
929                // `\r`, `\r\n`, and `\n` all count as a newline (the tokenizer
930                // treats a bare `\r` as a line break too).
931                b'\n' | b'\r' => newline_seen = true,
932                _ if b.is_ascii_whitespace() => {}
933                _ if newline_seen => return false,
934                _ => {}
935            }
936        }
937        // No block at all -> a declaration or a bare placeholder, not a rule.
938        false
939    }
940}