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}