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