From 5b72842a341e4383e8d29dea0a9c3fd150767caf Mon Sep 17 00:00:00 2001 From: Denis Bezrukov <6227442+denbezrukov@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:20:06 +0200 Subject: [PATCH 1/3] feat(css): add support for SCSS `@for` at-rule --- .../src/generated/node_factory.rs | 22 + .../src/generated/syntax_factory.rs | 61 +++ .../src/css/any/at_rule.rs | 1 + crates/biome_css_formatter/src/generated.rs | 38 ++ .../src/scss/statements/for_at_rule.rs | 39 ++ .../src/scss/statements/mod.rs | 1 + .../tests/specs/css/scss/at-rule/for.scss | 17 + crates/biome_css_parser/src/lexer/mod.rs | 2 + crates/biome_css_parser/src/lexer/tests.rs | 8 +- .../src/syntax/at_rule/mod.rs | 7 +- .../src/syntax/scss/at_rule/for_at_rule.rs | 75 +++ .../src/syntax/scss/at_rule/mod.rs | 2 + .../biome_css_parser/src/syntax/scss/mod.rs | 4 +- .../error/scss/at-rule/for.scss | 17 + .../error/scss/at-rule/for.scss.snap | 508 ++++++++++++++++++ .../css_test_suite/ok/scss/at-rule/for.scss | 13 + .../ok/scss/at-rule/for.scss.snap | 400 ++++++++++++++ crates/biome_css_syntax/src/generated/kind.rs | 9 +- .../biome_css_syntax/src/generated/macros.rs | 4 + .../biome_css_syntax/src/generated/nodes.rs | 148 +++++ .../src/generated/nodes_mut.rs | 44 ++ .../css_target_language/generated_mappings.rs | 1 + xtask/codegen/css.ungram | 12 + xtask/codegen/src/css_kinds_src.rs | 3 + 24 files changed, 1429 insertions(+), 7 deletions(-) create mode 100644 crates/biome_css_formatter/src/scss/statements/for_at_rule.rs create mode 100644 crates/biome_css_formatter/tests/specs/css/scss/at-rule/for.scss create mode 100644 crates/biome_css_parser/src/syntax/scss/at_rule/for_at_rule.rs create mode 100644 crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/for.scss create mode 100644 crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/for.scss.snap create mode 100644 crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/for.scss create mode 100644 crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/for.scss.snap diff --git a/crates/biome_css_factory/src/generated/node_factory.rs b/crates/biome_css_factory/src/generated/node_factory.rs index 484214581bb9..bba9586a378a 100644 --- a/crates/biome_css_factory/src/generated/node_factory.rs +++ b/crates/biome_css_factory/src/generated/node_factory.rs @@ -3188,6 +3188,28 @@ pub fn scss_expression(items: ScssExpressionItemList) -> ScssExpression { [Some(SyntaxElement::Node(items.into_syntax()))], )) } +pub fn scss_for_at_rule( + for_token: SyntaxToken, + variable: ScssIdentifier, + from_token: SyntaxToken, + lower_bound: ScssExpression, + operator_token: SyntaxToken, + upper_bound: ScssExpression, + block: CssDeclarationOrRuleBlock, +) -> ScssForAtRule { + ScssForAtRule::unwrap_cast(SyntaxNode::new_detached( + CssSyntaxKind::SCSS_FOR_AT_RULE, + [ + Some(SyntaxElement::Token(for_token)), + Some(SyntaxElement::Node(variable.into_syntax())), + Some(SyntaxElement::Token(from_token)), + Some(SyntaxElement::Node(lower_bound.into_syntax())), + Some(SyntaxElement::Token(operator_token)), + Some(SyntaxElement::Node(upper_bound.into_syntax())), + Some(SyntaxElement::Node(block.into_syntax())), + ], + )) +} pub fn scss_identifier(dollar_token: SyntaxToken, name: CssIdentifier) -> ScssIdentifier { ScssIdentifier::unwrap_cast(SyntaxNode::new_detached( CssSyntaxKind::SCSS_IDENTIFIER, diff --git a/crates/biome_css_factory/src/generated/syntax_factory.rs b/crates/biome_css_factory/src/generated/syntax_factory.rs index 134366471417..ed7dbebf67be 100644 --- a/crates/biome_css_factory/src/generated/syntax_factory.rs +++ b/crates/biome_css_factory/src/generated/syntax_factory.rs @@ -6592,6 +6592,67 @@ impl SyntaxFactory for CssSyntaxFactory { } slots.into_node(SCSS_EXPRESSION, children) } + SCSS_FOR_AT_RULE => { + let mut elements = (&children).into_iter(); + let mut slots: RawNodeSlots<7usize> = RawNodeSlots::default(); + let mut current_element = elements.next(); + if let Some(element) = ¤t_element + && element.kind() == T![for] + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && ScssIdentifier::can_cast(element.kind()) + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && element.kind() == T![from] + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && ScssExpression::can_cast(element.kind()) + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && matches!(element.kind(), T![to] | T![through]) + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && ScssExpression::can_cast(element.kind()) + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && CssDeclarationOrRuleBlock::can_cast(element.kind()) + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if current_element.is_some() { + return RawSyntaxNode::new( + SCSS_FOR_AT_RULE.to_bogus(), + children.into_iter().map(Some), + ); + } + slots.into_node(SCSS_FOR_AT_RULE, children) + } SCSS_IDENTIFIER => { let mut elements = (&children).into_iter(); let mut slots: RawNodeSlots<2usize> = RawNodeSlots::default(); diff --git a/crates/biome_css_formatter/src/css/any/at_rule.rs b/crates/biome_css_formatter/src/css/any/at_rule.rs index 1768113123f7..a01ae6d349c7 100644 --- a/crates/biome_css_formatter/src/css/any/at_rule.rs +++ b/crates/biome_css_formatter/src/css/any/at_rule.rs @@ -36,6 +36,7 @@ impl FormatRule for FormatAnyCssAtRule { AnyCssAtRule::ScssDebugAtRule(node) => node.format().fmt(f), AnyCssAtRule::ScssEachAtRule(node) => node.format().fmt(f), AnyCssAtRule::ScssErrorAtRule(node) => node.format().fmt(f), + AnyCssAtRule::ScssForAtRule(node) => node.format().fmt(f), AnyCssAtRule::ScssIfAtRule(node) => node.format().fmt(f), AnyCssAtRule::ScssWarnAtRule(node) => node.format().fmt(f), AnyCssAtRule::ScssWhileAtRule(node) => node.format().fmt(f), diff --git a/crates/biome_css_formatter/src/generated.rs b/crates/biome_css_formatter/src/generated.rs index 9348bb98d469..6e41dd7bc077 100644 --- a/crates/biome_css_formatter/src/generated.rs +++ b/crates/biome_css_formatter/src/generated.rs @@ -7381,6 +7381,44 @@ impl IntoFormat for biome_css_syntax::ScssExpression { ) } } +impl FormatRule + for crate::scss::statements::for_at_rule::FormatScssForAtRule +{ + type Context = CssFormatContext; + #[inline(always)] + fn fmt( + &self, + node: &biome_css_syntax::ScssForAtRule, + f: &mut CssFormatter, + ) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl AsFormat for biome_css_syntax::ScssForAtRule { + type Format<'a> = FormatRefWithRule< + 'a, + biome_css_syntax::ScssForAtRule, + crate::scss::statements::for_at_rule::FormatScssForAtRule, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::scss::statements::for_at_rule::FormatScssForAtRule::default(), + ) + } +} +impl IntoFormat for biome_css_syntax::ScssForAtRule { + type Format = FormatOwnedWithRule< + biome_css_syntax::ScssForAtRule, + crate::scss::statements::for_at_rule::FormatScssForAtRule, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::scss::statements::for_at_rule::FormatScssForAtRule::default(), + ) + } +} impl FormatRule for crate::scss::value::identifier::FormatScssIdentifier { diff --git a/crates/biome_css_formatter/src/scss/statements/for_at_rule.rs b/crates/biome_css_formatter/src/scss/statements/for_at_rule.rs new file mode 100644 index 000000000000..a833d4488e35 --- /dev/null +++ b/crates/biome_css_formatter/src/scss/statements/for_at_rule.rs @@ -0,0 +1,39 @@ +use crate::prelude::*; +use biome_css_syntax::{ScssForAtRule, ScssForAtRuleFields}; +use biome_formatter::write; + +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatScssForAtRule; + +impl FormatNodeRule for FormatScssForAtRule { + fn fmt_fields(&self, node: &ScssForAtRule, f: &mut CssFormatter) -> FormatResult<()> { + let ScssForAtRuleFields { + for_token, + variable, + from_token, + lower_bound, + operator, + upper_bound, + block, + } = node.as_fields(); + + write!( + f, + [ + for_token.format(), + space(), + variable.format(), + space(), + from_token.format(), + space(), + lower_bound.format(), + space(), + operator.format(), + space(), + upper_bound.format(), + space(), + block.format() + ] + ) + } +} diff --git a/crates/biome_css_formatter/src/scss/statements/mod.rs b/crates/biome_css_formatter/src/scss/statements/mod.rs index 3503ae944b24..8e5d2e548336 100644 --- a/crates/biome_css_formatter/src/scss/statements/mod.rs +++ b/crates/biome_css_formatter/src/scss/statements/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod debug_at_rule; pub(crate) mod each_at_rule; pub(crate) mod error_at_rule; +pub(crate) mod for_at_rule; pub(crate) mod if_at_rule; pub(crate) mod warn_at_rule; pub(crate) mod while_at_rule; diff --git a/crates/biome_css_formatter/tests/specs/css/scss/at-rule/for.scss b/crates/biome_css_formatter/tests/specs/css/scss/at-rule/for.scss new file mode 100644 index 000000000000..d9f3a58a2b10 --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/scss/at-rule/for.scss @@ -0,0 +1,17 @@ +.a{ +@for $i from 1 through 3{ +width:$i * 1px; +} +} + +.b{ +@for +$step +from +$start+1 +to +$end - 1 +{ +margin:$step; +} +} diff --git a/crates/biome_css_parser/src/lexer/mod.rs b/crates/biome_css_parser/src/lexer/mod.rs index 0b7a98d7b97e..346b9a47a4b4 100644 --- a/crates/biome_css_parser/src/lexer/mod.rs +++ b/crates/biome_css_parser/src/lexer/mod.rs @@ -795,6 +795,7 @@ impl<'src> CssLexer<'src> { b"important" => IMPORTANT_KW, b"from" => FROM_KW, b"to" => TO_KW, + b"through" => THROUGH_KW, b"var" => VAR_KW, b"highlight" => HIGHLIGHT_KW, b"part" => PART_KW, @@ -838,6 +839,7 @@ impl<'src> CssLexer<'src> { b"debug" => DEBUG_KW, b"warn" => WARN_KW, b"error" => ERROR_KW, + b"for" => FOR_KW, b"while" => WHILE_KW, b"sass" => SASS_KW, b"style" => STYLE_KW, diff --git a/crates/biome_css_parser/src/lexer/tests.rs b/crates/biome_css_parser/src/lexer/tests.rs index d4f3c62b5608..a636e45a408d 100644 --- a/crates/biome_css_parser/src/lexer/tests.rs +++ b/crates/biome_css_parser/src/lexer/tests.rs @@ -309,7 +309,7 @@ fn cdo_and_cdc() { #[test] fn keywords() { assert_lex! { - "media keyframes important from sass each debug warn error", + "media keyframes important from through sass each debug warn error for", MEDIA_KW:5, WHITESPACE:1, KEYFRAMES_KW:9, @@ -318,6 +318,8 @@ fn keywords() { WHITESPACE:1, FROM_KW:4, WHITESPACE:1, + THROUGH_KW:7, + WHITESPACE:1, SASS_KW:4, WHITESPACE:1, EACH_KW:4, @@ -326,7 +328,9 @@ fn keywords() { WHITESPACE:1, WARN_KW:4, WHITESPACE:1, - ERROR_KW:5 + ERROR_KW:5, + WHITESPACE:1, + FOR_KW:3 } } diff --git a/crates/biome_css_parser/src/syntax/at_rule/mod.rs b/crates/biome_css_parser/src/syntax/at_rule/mod.rs index 878616dad1bb..4209f09fedb9 100644 --- a/crates/biome_css_parser/src/syntax/at_rule/mod.rs +++ b/crates/biome_css_parser/src/syntax/at_rule/mod.rs @@ -76,8 +76,8 @@ use crate::syntax::CssSyntaxFeatures; use crate::syntax::parse_error::{expected_any_at_rule, tailwind_disabled}; use crate::syntax::scss::{ parse_bogus_scss_else_at_rule, parse_scss_debug_at_rule, parse_scss_each_at_rule, - parse_scss_error_at_rule, parse_scss_if_at_rule, parse_scss_warn_at_rule, - parse_scss_while_at_rule, + parse_scss_error_at_rule, parse_scss_for_at_rule, parse_scss_if_at_rule, + parse_scss_warn_at_rule, parse_scss_while_at_rule, }; use biome_css_syntax::CssSyntaxKind::*; use biome_css_syntax::T; @@ -142,6 +142,9 @@ pub(crate) fn parse_any_at_rule(p: &mut CssParser) -> ParsedSyntax { T![each] => CssSyntaxFeatures::Scss .parse_supported_syntax(p, parse_scss_each_at_rule) .or_else(|| parse_unknown_at_rule(p)), + T![for] => CssSyntaxFeatures::Scss + .parse_supported_syntax(p, parse_scss_for_at_rule) + .or_else(|| parse_unknown_at_rule(p)), T![if] => CssSyntaxFeatures::Scss .parse_supported_syntax(p, parse_scss_if_at_rule) .or_else(|| parse_unknown_at_rule(p)), diff --git a/crates/biome_css_parser/src/syntax/scss/at_rule/for_at_rule.rs b/crates/biome_css_parser/src/syntax/scss/at_rule/for_at_rule.rs new file mode 100644 index 000000000000..7af1f8d1051d --- /dev/null +++ b/crates/biome_css_parser/src/syntax/scss/at_rule/for_at_rule.rs @@ -0,0 +1,75 @@ +use crate::parser::CssParser; +use crate::syntax::block::parse_declaration_or_rule_list_block; +use crate::syntax::scss::{ + expected_scss_expression, parse_scss_expression_until, parse_scss_identifier, +}; +use biome_css_syntax::CssSyntaxKind::{self, SCSS_FOR_AT_RULE}; +use biome_css_syntax::T; +use biome_parser::prelude::ParsedSyntax::{Absent, Present}; +use biome_parser::prelude::*; +use biome_parser::{TokenSet, token_set}; + +const SCSS_FOR_LOWER_BOUND_END_SET: TokenSet = + token_set![T![to], T![through], T!['{']]; +const SCSS_FOR_UPPER_BOUND_END_SET: TokenSet = token_set![T!['{']]; + +/// Parses the SCSS `@for` at-rule. +/// +/// # Example +/// +/// ```scss +/// @for $i from 1 through 3 { +/// width: $i * 1px; +/// } +/// ``` +/// +/// Docs: https://sass-lang.com/documentation/at-rules/control/for/ +#[inline] +pub(crate) fn parse_scss_for_at_rule(p: &mut CssParser) -> ParsedSyntax { + if !is_at_scss_for_at_rule(p) { + return Absent; + } + + let m = p.start(); + + p.bump(T![for]); + parse_scss_identifier(p).or_add_diagnostic(p, expected_scss_for_binding); + p.expect(T![from]); + parse_scss_expression_until(p, SCSS_FOR_LOWER_BOUND_END_SET) + .or_add_diagnostic(p, expected_scss_expression); + parse_scss_for_range_operator(p); + parse_scss_expression_until(p, SCSS_FOR_UPPER_BOUND_END_SET) + .or_add_diagnostic(p, expected_scss_expression); + parse_declaration_or_rule_list_block(p); + + Present(m.complete(p, SCSS_FOR_AT_RULE)) +} + +#[inline] +fn is_at_scss_for_at_rule(p: &mut CssParser) -> bool { + p.at(T![for]) +} + +#[inline] +fn parse_scss_for_range_operator(p: &mut CssParser) { + if p.at(T![to]) || p.at(T![through]) { + p.bump_any(); + } else { + p.error(expected_scss_for_range_operator(p, p.cur_range())); + } +} + +#[inline] +fn expected_scss_for_binding(p: &CssParser, range: biome_rowan::TextRange) -> ParseDiagnostic { + p.err_builder("Expected a variable binding after `@for`.", range) + .with_hint("Add a variable like `$i` before `from`.") +} + +#[inline] +fn expected_scss_for_range_operator( + p: &CssParser, + range: biome_rowan::TextRange, +) -> ParseDiagnostic { + p.err_builder("Expected `to` or `through` in the `@for` range.", range) + .with_hint("Use `to` or `through` between the lower and upper bounds.") +} diff --git a/crates/biome_css_parser/src/syntax/scss/at_rule/mod.rs b/crates/biome_css_parser/src/syntax/scss/at_rule/mod.rs index fa33791d0463..ebdee6cd0c66 100644 --- a/crates/biome_css_parser/src/syntax/scss/at_rule/mod.rs +++ b/crates/biome_css_parser/src/syntax/scss/at_rule/mod.rs @@ -2,6 +2,7 @@ mod debug; mod each_at_rule; mod else_clause; mod error; +mod for_at_rule; mod if_at_rule; mod warn; mod while_at_rule; @@ -17,6 +18,7 @@ pub(crate) use debug::parse_scss_debug_at_rule; pub(crate) use each_at_rule::parse_scss_each_at_rule; pub(crate) use else_clause::parse_bogus_scss_else_at_rule; pub(crate) use error::parse_scss_error_at_rule; +pub(crate) use for_at_rule::parse_scss_for_at_rule; pub(crate) use if_at_rule::parse_scss_if_at_rule; pub(crate) use warn::parse_scss_warn_at_rule; pub(crate) use while_at_rule::parse_scss_while_at_rule; diff --git a/crates/biome_css_parser/src/syntax/scss/mod.rs b/crates/biome_css_parser/src/syntax/scss/mod.rs index 0e504f5f1bce..f74067039f46 100644 --- a/crates/biome_css_parser/src/syntax/scss/mod.rs +++ b/crates/biome_css_parser/src/syntax/scss/mod.rs @@ -9,8 +9,8 @@ mod value; pub(crate) use at_rule::{ parse_bogus_scss_else_at_rule, parse_scss_debug_at_rule, parse_scss_each_at_rule, - parse_scss_error_at_rule, parse_scss_if_at_rule, parse_scss_warn_at_rule, - parse_scss_while_at_rule, + parse_scss_error_at_rule, parse_scss_for_at_rule, parse_scss_if_at_rule, + parse_scss_warn_at_rule, parse_scss_while_at_rule, }; pub(crate) use declaration::{ is_at_scss_declaration, is_at_scss_nesting_declaration, is_at_scss_variable_modifier_start, diff --git a/crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/for.scss b/crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/for.scss new file mode 100644 index 000000000000..74bec7818d59 --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/for.scss @@ -0,0 +1,17 @@ +@for from 1 through 3 { + width: 1px; +} + +@for $i 1 through 3 { + width: 1px; +} + +@for $i from 1 { + width: 1px; +} + +@for $i from 1 to { + width: 1px; +} + +@for $i from 1 through 3 diff --git a/crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/for.scss.snap b/crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/for.scss.snap new file mode 100644 index 000000000000..9ce80c524370 --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/for.scss.snap @@ -0,0 +1,508 @@ +--- +source: crates/biome_css_parser/tests/spec_test.rs +assertion_line: 208 +expression: snapshot +--- + +## Input + +```css +@for from 1 through 3 { + width: 1px; +} + +@for $i 1 through 3 { + width: 1px; +} + +@for $i from 1 { + width: 1px; +} + +@for $i from 1 to { + width: 1px; +} + +@for $i from 1 through 3 + +``` + + +## AST + +``` +CssRoot { + bom_token: missing (optional), + items: CssRootItemList [ + CssAtRule { + at_token: AT@0..1 "@" [] [], + rule: ScssForAtRule { + for_token: FOR_KW@1..5 "for" [] [Whitespace(" ")], + variable: missing (required), + from_token: FROM_KW@5..10 "from" [] [Whitespace(" ")], + lower_bound: ScssExpression { + items: ScssExpressionItemList [ + CssNumber { + value_token: CSS_NUMBER_LITERAL@10..12 "1" [] [Whitespace(" ")], + }, + ], + }, + operator: THROUGH_KW@12..20 "through" [] [Whitespace(" ")], + upper_bound: ScssExpression { + items: ScssExpressionItemList [ + CssNumber { + value_token: CSS_NUMBER_LITERAL@20..22 "3" [] [Whitespace(" ")], + }, + ], + }, + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@22..23 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@23..31 "width" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@31..33 ":" [] [Whitespace(" ")], + value: ScssExpression { + items: ScssExpressionItemList [ + CssRegularDimension { + value_token: CSS_NUMBER_LITERAL@33..34 "1" [] [], + unit_token: IDENT@34..36 "px" [] [], + }, + ], + }, + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@36..37 ";" [] [], + }, + ], + r_curly_token: R_CURLY@37..39 "}" [Newline("\n")] [], + }, + }, + }, + CssAtRule { + at_token: AT@39..42 "@" [Newline("\n"), Newline("\n")] [], + rule: ScssForAtRule { + for_token: FOR_KW@42..46 "for" [] [Whitespace(" ")], + variable: ScssIdentifier { + dollar_token: DOLLAR@46..47 "$" [] [], + name: CssIdentifier { + value_token: IDENT@47..49 "i" [] [Whitespace(" ")], + }, + }, + from_token: missing (required), + lower_bound: ScssExpression { + items: ScssExpressionItemList [ + CssNumber { + value_token: CSS_NUMBER_LITERAL@49..51 "1" [] [Whitespace(" ")], + }, + ], + }, + operator: THROUGH_KW@51..59 "through" [] [Whitespace(" ")], + upper_bound: ScssExpression { + items: ScssExpressionItemList [ + CssNumber { + value_token: CSS_NUMBER_LITERAL@59..61 "3" [] [Whitespace(" ")], + }, + ], + }, + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@61..62 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@62..70 "width" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@70..72 ":" [] [Whitespace(" ")], + value: ScssExpression { + items: ScssExpressionItemList [ + CssRegularDimension { + value_token: CSS_NUMBER_LITERAL@72..73 "1" [] [], + unit_token: IDENT@73..75 "px" [] [], + }, + ], + }, + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@75..76 ";" [] [], + }, + ], + r_curly_token: R_CURLY@76..78 "}" [Newline("\n")] [], + }, + }, + }, + CssAtRule { + at_token: AT@78..81 "@" [Newline("\n"), Newline("\n")] [], + rule: ScssForAtRule { + for_token: FOR_KW@81..85 "for" [] [Whitespace(" ")], + variable: ScssIdentifier { + dollar_token: DOLLAR@85..86 "$" [] [], + name: CssIdentifier { + value_token: IDENT@86..88 "i" [] [Whitespace(" ")], + }, + }, + from_token: FROM_KW@88..93 "from" [] [Whitespace(" ")], + lower_bound: ScssExpression { + items: ScssExpressionItemList [ + CssNumber { + value_token: CSS_NUMBER_LITERAL@93..95 "1" [] [Whitespace(" ")], + }, + ], + }, + operator: missing (required), + upper_bound: missing (required), + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@95..96 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@96..104 "width" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@104..106 ":" [] [Whitespace(" ")], + value: ScssExpression { + items: ScssExpressionItemList [ + CssRegularDimension { + value_token: CSS_NUMBER_LITERAL@106..107 "1" [] [], + unit_token: IDENT@107..109 "px" [] [], + }, + ], + }, + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@109..110 ";" [] [], + }, + ], + r_curly_token: R_CURLY@110..112 "}" [Newline("\n")] [], + }, + }, + }, + CssAtRule { + at_token: AT@112..115 "@" [Newline("\n"), Newline("\n")] [], + rule: ScssForAtRule { + for_token: FOR_KW@115..119 "for" [] [Whitespace(" ")], + variable: ScssIdentifier { + dollar_token: DOLLAR@119..120 "$" [] [], + name: CssIdentifier { + value_token: IDENT@120..122 "i" [] [Whitespace(" ")], + }, + }, + from_token: FROM_KW@122..127 "from" [] [Whitespace(" ")], + lower_bound: ScssExpression { + items: ScssExpressionItemList [ + CssNumber { + value_token: CSS_NUMBER_LITERAL@127..129 "1" [] [Whitespace(" ")], + }, + ], + }, + operator: TO_KW@129..132 "to" [] [Whitespace(" ")], + upper_bound: missing (required), + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@132..133 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@133..141 "width" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@141..143 ":" [] [Whitespace(" ")], + value: ScssExpression { + items: ScssExpressionItemList [ + CssRegularDimension { + value_token: CSS_NUMBER_LITERAL@143..144 "1" [] [], + unit_token: IDENT@144..146 "px" [] [], + }, + ], + }, + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@146..147 ";" [] [], + }, + ], + r_curly_token: R_CURLY@147..149 "}" [Newline("\n")] [], + }, + }, + }, + CssAtRule { + at_token: AT@149..152 "@" [Newline("\n"), Newline("\n")] [], + rule: CssBogusAtRule { + items: [ + FOR_KW@152..156 "for" [] [Whitespace(" ")], + ScssIdentifier { + dollar_token: DOLLAR@156..157 "$" [] [], + name: CssIdentifier { + value_token: IDENT@157..159 "i" [] [Whitespace(" ")], + }, + }, + FROM_KW@159..164 "from" [] [Whitespace(" ")], + ScssExpression { + items: ScssExpressionItemList [ + CssNumber { + value_token: CSS_NUMBER_LITERAL@164..166 "1" [] [Whitespace(" ")], + }, + ], + }, + THROUGH_KW@166..174 "through" [] [Whitespace(" ")], + ScssExpression { + items: ScssExpressionItemList [ + CssNumber { + value_token: CSS_NUMBER_LITERAL@174..175 "3" [] [], + }, + ], + }, + CssBogusBlock { + items: [], + }, + ], + }, + }, + ], + eof_token: EOF@175..176 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: CSS_ROOT@0..176 + 0: (empty) + 1: CSS_ROOT_ITEM_LIST@0..175 + 0: CSS_AT_RULE@0..39 + 0: AT@0..1 "@" [] [] + 1: SCSS_FOR_AT_RULE@1..39 + 0: FOR_KW@1..5 "for" [] [Whitespace(" ")] + 1: (empty) + 2: FROM_KW@5..10 "from" [] [Whitespace(" ")] + 3: SCSS_EXPRESSION@10..12 + 0: SCSS_EXPRESSION_ITEM_LIST@10..12 + 0: CSS_NUMBER@10..12 + 0: CSS_NUMBER_LITERAL@10..12 "1" [] [Whitespace(" ")] + 4: THROUGH_KW@12..20 "through" [] [Whitespace(" ")] + 5: SCSS_EXPRESSION@20..22 + 0: SCSS_EXPRESSION_ITEM_LIST@20..22 + 0: CSS_NUMBER@20..22 + 0: CSS_NUMBER_LITERAL@20..22 "3" [] [Whitespace(" ")] + 6: CSS_DECLARATION_OR_RULE_BLOCK@22..39 + 0: L_CURLY@22..23 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@23..37 + 0: CSS_DECLARATION_WITH_SEMICOLON@23..37 + 0: CSS_DECLARATION@23..36 + 0: CSS_GENERIC_PROPERTY@23..36 + 0: CSS_IDENTIFIER@23..31 + 0: IDENT@23..31 "width" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@31..33 ":" [] [Whitespace(" ")] + 2: SCSS_EXPRESSION@33..36 + 0: SCSS_EXPRESSION_ITEM_LIST@33..36 + 0: CSS_REGULAR_DIMENSION@33..36 + 0: CSS_NUMBER_LITERAL@33..34 "1" [] [] + 1: IDENT@34..36 "px" [] [] + 1: (empty) + 1: SEMICOLON@36..37 ";" [] [] + 2: R_CURLY@37..39 "}" [Newline("\n")] [] + 1: CSS_AT_RULE@39..78 + 0: AT@39..42 "@" [Newline("\n"), Newline("\n")] [] + 1: SCSS_FOR_AT_RULE@42..78 + 0: FOR_KW@42..46 "for" [] [Whitespace(" ")] + 1: SCSS_IDENTIFIER@46..49 + 0: DOLLAR@46..47 "$" [] [] + 1: CSS_IDENTIFIER@47..49 + 0: IDENT@47..49 "i" [] [Whitespace(" ")] + 2: (empty) + 3: SCSS_EXPRESSION@49..51 + 0: SCSS_EXPRESSION_ITEM_LIST@49..51 + 0: CSS_NUMBER@49..51 + 0: CSS_NUMBER_LITERAL@49..51 "1" [] [Whitespace(" ")] + 4: THROUGH_KW@51..59 "through" [] [Whitespace(" ")] + 5: SCSS_EXPRESSION@59..61 + 0: SCSS_EXPRESSION_ITEM_LIST@59..61 + 0: CSS_NUMBER@59..61 + 0: CSS_NUMBER_LITERAL@59..61 "3" [] [Whitespace(" ")] + 6: CSS_DECLARATION_OR_RULE_BLOCK@61..78 + 0: L_CURLY@61..62 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@62..76 + 0: CSS_DECLARATION_WITH_SEMICOLON@62..76 + 0: CSS_DECLARATION@62..75 + 0: CSS_GENERIC_PROPERTY@62..75 + 0: CSS_IDENTIFIER@62..70 + 0: IDENT@62..70 "width" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@70..72 ":" [] [Whitespace(" ")] + 2: SCSS_EXPRESSION@72..75 + 0: SCSS_EXPRESSION_ITEM_LIST@72..75 + 0: CSS_REGULAR_DIMENSION@72..75 + 0: CSS_NUMBER_LITERAL@72..73 "1" [] [] + 1: IDENT@73..75 "px" [] [] + 1: (empty) + 1: SEMICOLON@75..76 ";" [] [] + 2: R_CURLY@76..78 "}" [Newline("\n")] [] + 2: CSS_AT_RULE@78..112 + 0: AT@78..81 "@" [Newline("\n"), Newline("\n")] [] + 1: SCSS_FOR_AT_RULE@81..112 + 0: FOR_KW@81..85 "for" [] [Whitespace(" ")] + 1: SCSS_IDENTIFIER@85..88 + 0: DOLLAR@85..86 "$" [] [] + 1: CSS_IDENTIFIER@86..88 + 0: IDENT@86..88 "i" [] [Whitespace(" ")] + 2: FROM_KW@88..93 "from" [] [Whitespace(" ")] + 3: SCSS_EXPRESSION@93..95 + 0: SCSS_EXPRESSION_ITEM_LIST@93..95 + 0: CSS_NUMBER@93..95 + 0: CSS_NUMBER_LITERAL@93..95 "1" [] [Whitespace(" ")] + 4: (empty) + 5: (empty) + 6: CSS_DECLARATION_OR_RULE_BLOCK@95..112 + 0: L_CURLY@95..96 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@96..110 + 0: CSS_DECLARATION_WITH_SEMICOLON@96..110 + 0: CSS_DECLARATION@96..109 + 0: CSS_GENERIC_PROPERTY@96..109 + 0: CSS_IDENTIFIER@96..104 + 0: IDENT@96..104 "width" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@104..106 ":" [] [Whitespace(" ")] + 2: SCSS_EXPRESSION@106..109 + 0: SCSS_EXPRESSION_ITEM_LIST@106..109 + 0: CSS_REGULAR_DIMENSION@106..109 + 0: CSS_NUMBER_LITERAL@106..107 "1" [] [] + 1: IDENT@107..109 "px" [] [] + 1: (empty) + 1: SEMICOLON@109..110 ";" [] [] + 2: R_CURLY@110..112 "}" [Newline("\n")] [] + 3: CSS_AT_RULE@112..149 + 0: AT@112..115 "@" [Newline("\n"), Newline("\n")] [] + 1: SCSS_FOR_AT_RULE@115..149 + 0: FOR_KW@115..119 "for" [] [Whitespace(" ")] + 1: SCSS_IDENTIFIER@119..122 + 0: DOLLAR@119..120 "$" [] [] + 1: CSS_IDENTIFIER@120..122 + 0: IDENT@120..122 "i" [] [Whitespace(" ")] + 2: FROM_KW@122..127 "from" [] [Whitespace(" ")] + 3: SCSS_EXPRESSION@127..129 + 0: SCSS_EXPRESSION_ITEM_LIST@127..129 + 0: CSS_NUMBER@127..129 + 0: CSS_NUMBER_LITERAL@127..129 "1" [] [Whitespace(" ")] + 4: TO_KW@129..132 "to" [] [Whitespace(" ")] + 5: (empty) + 6: CSS_DECLARATION_OR_RULE_BLOCK@132..149 + 0: L_CURLY@132..133 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@133..147 + 0: CSS_DECLARATION_WITH_SEMICOLON@133..147 + 0: CSS_DECLARATION@133..146 + 0: CSS_GENERIC_PROPERTY@133..146 + 0: CSS_IDENTIFIER@133..141 + 0: IDENT@133..141 "width" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@141..143 ":" [] [Whitespace(" ")] + 2: SCSS_EXPRESSION@143..146 + 0: SCSS_EXPRESSION_ITEM_LIST@143..146 + 0: CSS_REGULAR_DIMENSION@143..146 + 0: CSS_NUMBER_LITERAL@143..144 "1" [] [] + 1: IDENT@144..146 "px" [] [] + 1: (empty) + 1: SEMICOLON@146..147 ";" [] [] + 2: R_CURLY@147..149 "}" [Newline("\n")] [] + 4: CSS_AT_RULE@149..175 + 0: AT@149..152 "@" [Newline("\n"), Newline("\n")] [] + 1: CSS_BOGUS_AT_RULE@152..175 + 0: FOR_KW@152..156 "for" [] [Whitespace(" ")] + 1: SCSS_IDENTIFIER@156..159 + 0: DOLLAR@156..157 "$" [] [] + 1: CSS_IDENTIFIER@157..159 + 0: IDENT@157..159 "i" [] [Whitespace(" ")] + 2: FROM_KW@159..164 "from" [] [Whitespace(" ")] + 3: SCSS_EXPRESSION@164..166 + 0: SCSS_EXPRESSION_ITEM_LIST@164..166 + 0: CSS_NUMBER@164..166 + 0: CSS_NUMBER_LITERAL@164..166 "1" [] [Whitespace(" ")] + 4: THROUGH_KW@166..174 "through" [] [Whitespace(" ")] + 5: SCSS_EXPRESSION@174..175 + 0: SCSS_EXPRESSION_ITEM_LIST@174..175 + 0: CSS_NUMBER@174..175 + 0: CSS_NUMBER_LITERAL@174..175 "3" [] [] + 6: CSS_BOGUS_BLOCK@175..175 + 2: EOF@175..176 "" [Newline("\n")] [] + +``` + +## Diagnostics + +``` +for.scss:1:6 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a variable binding after `@for`. + + > 1 │ @for from 1 through 3 { + │ ^^^^ + 2 │ width: 1px; + 3 │ } + + i Add a variable like `$i` before `from`. + +for.scss:5:9 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `from` but instead found `1` + + 3 │ } + 4 │ + > 5 │ @for $i 1 through 3 { + │ ^ + 6 │ width: 1px; + 7 │ } + + i Remove 1 + +for.scss:9:16 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected `to` or `through` in the `@for` range. + + 7 │ } + 8 │ + > 9 │ @for $i from 1 { + │ ^ + 10 │ width: 1px; + 11 │ } + + i Use `to` or `through` between the lower and upper bounds. + +for.scss:13:19 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a SCSS expression but instead found '{'. + + 11 │ } + 12 │ + > 13 │ @for $i from 1 to { + │ ^ + 14 │ width: 1px; + 15 │ } + + i Expected a SCSS expression here. + + 11 │ } + 12 │ + > 13 │ @for $i from 1 to { + │ ^ + 14 │ width: 1px; + 15 │ } + +for.scss:18:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `{` but instead the file ends + + 17 │ @for $i from 1 through 3 + > 18 │ + │ + + i the file ends here + + 17 │ @for $i from 1 through 3 + > 18 │ + │ + +``` diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/for.scss b/crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/for.scss new file mode 100644 index 000000000000..54c5cbc9f493 --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/for.scss @@ -0,0 +1,13 @@ +@for $i from 1 through 3 { + width: $i * 1px; +} + +@for $step from $start + 1 to $end - 1 { + margin: $step; +} + +.grid { + @for $column from 1 through 4 { + order: $column; + } +} diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/for.scss.snap b/crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/for.scss.snap new file mode 100644 index 000000000000..8d3fb81da6b8 --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/for.scss.snap @@ -0,0 +1,400 @@ +--- +source: crates/biome_css_parser/tests/spec_test.rs +assertion_line: 208 +expression: snapshot +--- + +## Input + +```css +@for $i from 1 through 3 { + width: $i * 1px; +} + +@for $step from $start + 1 to $end - 1 { + margin: $step; +} + +.grid { + @for $column from 1 through 4 { + order: $column; + } +} + +``` + + +## AST + +``` +CssRoot { + bom_token: missing (optional), + items: CssRootItemList [ + CssAtRule { + at_token: AT@0..1 "@" [] [], + rule: ScssForAtRule { + for_token: FOR_KW@1..5 "for" [] [Whitespace(" ")], + variable: ScssIdentifier { + dollar_token: DOLLAR@5..6 "$" [] [], + name: CssIdentifier { + value_token: IDENT@6..8 "i" [] [Whitespace(" ")], + }, + }, + from_token: FROM_KW@8..13 "from" [] [Whitespace(" ")], + lower_bound: ScssExpression { + items: ScssExpressionItemList [ + CssNumber { + value_token: CSS_NUMBER_LITERAL@13..15 "1" [] [Whitespace(" ")], + }, + ], + }, + operator: THROUGH_KW@15..23 "through" [] [Whitespace(" ")], + upper_bound: ScssExpression { + items: ScssExpressionItemList [ + CssNumber { + value_token: CSS_NUMBER_LITERAL@23..25 "3" [] [Whitespace(" ")], + }, + ], + }, + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@25..26 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@26..34 "width" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@34..36 ":" [] [Whitespace(" ")], + value: ScssExpression { + items: ScssExpressionItemList [ + ScssBinaryExpression { + left: ScssIdentifier { + dollar_token: DOLLAR@36..37 "$" [] [], + name: CssIdentifier { + value_token: IDENT@37..39 "i" [] [Whitespace(" ")], + }, + }, + operator: STAR@39..41 "*" [] [Whitespace(" ")], + right: CssRegularDimension { + value_token: CSS_NUMBER_LITERAL@41..42 "1" [] [], + unit_token: IDENT@42..44 "px" [] [], + }, + }, + ], + }, + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@44..45 ";" [] [], + }, + ], + r_curly_token: R_CURLY@45..47 "}" [Newline("\n")] [], + }, + }, + }, + CssAtRule { + at_token: AT@47..50 "@" [Newline("\n"), Newline("\n")] [], + rule: ScssForAtRule { + for_token: FOR_KW@50..54 "for" [] [Whitespace(" ")], + variable: ScssIdentifier { + dollar_token: DOLLAR@54..55 "$" [] [], + name: CssIdentifier { + value_token: IDENT@55..60 "step" [] [Whitespace(" ")], + }, + }, + from_token: FROM_KW@60..65 "from" [] [Whitespace(" ")], + lower_bound: ScssExpression { + items: ScssExpressionItemList [ + ScssBinaryExpression { + left: ScssIdentifier { + dollar_token: DOLLAR@65..66 "$" [] [], + name: CssIdentifier { + value_token: IDENT@66..72 "start" [] [Whitespace(" ")], + }, + }, + operator: PLUS@72..74 "+" [] [Whitespace(" ")], + right: CssNumber { + value_token: CSS_NUMBER_LITERAL@74..76 "1" [] [Whitespace(" ")], + }, + }, + ], + }, + operator: TO_KW@76..79 "to" [] [Whitespace(" ")], + upper_bound: ScssExpression { + items: ScssExpressionItemList [ + ScssBinaryExpression { + left: ScssIdentifier { + dollar_token: DOLLAR@79..80 "$" [] [], + name: CssIdentifier { + value_token: IDENT@80..84 "end" [] [Whitespace(" ")], + }, + }, + operator: MINUS@84..86 "-" [] [Whitespace(" ")], + right: CssNumber { + value_token: CSS_NUMBER_LITERAL@86..88 "1" [] [Whitespace(" ")], + }, + }, + ], + }, + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@88..89 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@89..98 "margin" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@98..100 ":" [] [Whitespace(" ")], + value: ScssExpression { + items: ScssExpressionItemList [ + ScssIdentifier { + dollar_token: DOLLAR@100..101 "$" [] [], + name: CssIdentifier { + value_token: IDENT@101..105 "step" [] [], + }, + }, + ], + }, + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@105..106 ";" [] [], + }, + ], + r_curly_token: R_CURLY@106..108 "}" [Newline("\n")] [], + }, + }, + }, + CssQualifiedRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@108..111 "." [Newline("\n"), Newline("\n")] [], + name: CssCustomIdentifier { + value_token: IDENT@111..116 "grid" [] [Whitespace(" ")], + }, + }, + ], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@116..117 "{" [] [], + items: CssDeclarationOrRuleList [ + CssAtRule { + at_token: AT@117..121 "@" [Newline("\n"), Whitespace(" ")] [], + rule: ScssForAtRule { + for_token: FOR_KW@121..125 "for" [] [Whitespace(" ")], + variable: ScssIdentifier { + dollar_token: DOLLAR@125..126 "$" [] [], + name: CssIdentifier { + value_token: IDENT@126..133 "column" [] [Whitespace(" ")], + }, + }, + from_token: FROM_KW@133..138 "from" [] [Whitespace(" ")], + lower_bound: ScssExpression { + items: ScssExpressionItemList [ + CssNumber { + value_token: CSS_NUMBER_LITERAL@138..140 "1" [] [Whitespace(" ")], + }, + ], + }, + operator: THROUGH_KW@140..148 "through" [] [Whitespace(" ")], + upper_bound: ScssExpression { + items: ScssExpressionItemList [ + CssNumber { + value_token: CSS_NUMBER_LITERAL@148..150 "4" [] [Whitespace(" ")], + }, + ], + }, + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@150..151 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@151..161 "order" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@161..163 ":" [] [Whitespace(" ")], + value: ScssExpression { + items: ScssExpressionItemList [ + ScssIdentifier { + dollar_token: DOLLAR@163..164 "$" [] [], + name: CssIdentifier { + value_token: IDENT@164..170 "column" [] [], + }, + }, + ], + }, + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@170..171 ";" [] [], + }, + ], + r_curly_token: R_CURLY@171..175 "}" [Newline("\n"), Whitespace(" ")] [], + }, + }, + }, + ], + r_curly_token: R_CURLY@175..177 "}" [Newline("\n")] [], + }, + }, + ], + eof_token: EOF@177..178 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: CSS_ROOT@0..178 + 0: (empty) + 1: CSS_ROOT_ITEM_LIST@0..177 + 0: CSS_AT_RULE@0..47 + 0: AT@0..1 "@" [] [] + 1: SCSS_FOR_AT_RULE@1..47 + 0: FOR_KW@1..5 "for" [] [Whitespace(" ")] + 1: SCSS_IDENTIFIER@5..8 + 0: DOLLAR@5..6 "$" [] [] + 1: CSS_IDENTIFIER@6..8 + 0: IDENT@6..8 "i" [] [Whitespace(" ")] + 2: FROM_KW@8..13 "from" [] [Whitespace(" ")] + 3: SCSS_EXPRESSION@13..15 + 0: SCSS_EXPRESSION_ITEM_LIST@13..15 + 0: CSS_NUMBER@13..15 + 0: CSS_NUMBER_LITERAL@13..15 "1" [] [Whitespace(" ")] + 4: THROUGH_KW@15..23 "through" [] [Whitespace(" ")] + 5: SCSS_EXPRESSION@23..25 + 0: SCSS_EXPRESSION_ITEM_LIST@23..25 + 0: CSS_NUMBER@23..25 + 0: CSS_NUMBER_LITERAL@23..25 "3" [] [Whitespace(" ")] + 6: CSS_DECLARATION_OR_RULE_BLOCK@25..47 + 0: L_CURLY@25..26 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@26..45 + 0: CSS_DECLARATION_WITH_SEMICOLON@26..45 + 0: CSS_DECLARATION@26..44 + 0: CSS_GENERIC_PROPERTY@26..44 + 0: CSS_IDENTIFIER@26..34 + 0: IDENT@26..34 "width" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@34..36 ":" [] [Whitespace(" ")] + 2: SCSS_EXPRESSION@36..44 + 0: SCSS_EXPRESSION_ITEM_LIST@36..44 + 0: SCSS_BINARY_EXPRESSION@36..44 + 0: SCSS_IDENTIFIER@36..39 + 0: DOLLAR@36..37 "$" [] [] + 1: CSS_IDENTIFIER@37..39 + 0: IDENT@37..39 "i" [] [Whitespace(" ")] + 1: STAR@39..41 "*" [] [Whitespace(" ")] + 2: CSS_REGULAR_DIMENSION@41..44 + 0: CSS_NUMBER_LITERAL@41..42 "1" [] [] + 1: IDENT@42..44 "px" [] [] + 1: (empty) + 1: SEMICOLON@44..45 ";" [] [] + 2: R_CURLY@45..47 "}" [Newline("\n")] [] + 1: CSS_AT_RULE@47..108 + 0: AT@47..50 "@" [Newline("\n"), Newline("\n")] [] + 1: SCSS_FOR_AT_RULE@50..108 + 0: FOR_KW@50..54 "for" [] [Whitespace(" ")] + 1: SCSS_IDENTIFIER@54..60 + 0: DOLLAR@54..55 "$" [] [] + 1: CSS_IDENTIFIER@55..60 + 0: IDENT@55..60 "step" [] [Whitespace(" ")] + 2: FROM_KW@60..65 "from" [] [Whitespace(" ")] + 3: SCSS_EXPRESSION@65..76 + 0: SCSS_EXPRESSION_ITEM_LIST@65..76 + 0: SCSS_BINARY_EXPRESSION@65..76 + 0: SCSS_IDENTIFIER@65..72 + 0: DOLLAR@65..66 "$" [] [] + 1: CSS_IDENTIFIER@66..72 + 0: IDENT@66..72 "start" [] [Whitespace(" ")] + 1: PLUS@72..74 "+" [] [Whitespace(" ")] + 2: CSS_NUMBER@74..76 + 0: CSS_NUMBER_LITERAL@74..76 "1" [] [Whitespace(" ")] + 4: TO_KW@76..79 "to" [] [Whitespace(" ")] + 5: SCSS_EXPRESSION@79..88 + 0: SCSS_EXPRESSION_ITEM_LIST@79..88 + 0: SCSS_BINARY_EXPRESSION@79..88 + 0: SCSS_IDENTIFIER@79..84 + 0: DOLLAR@79..80 "$" [] [] + 1: CSS_IDENTIFIER@80..84 + 0: IDENT@80..84 "end" [] [Whitespace(" ")] + 1: MINUS@84..86 "-" [] [Whitespace(" ")] + 2: CSS_NUMBER@86..88 + 0: CSS_NUMBER_LITERAL@86..88 "1" [] [Whitespace(" ")] + 6: CSS_DECLARATION_OR_RULE_BLOCK@88..108 + 0: L_CURLY@88..89 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@89..106 + 0: CSS_DECLARATION_WITH_SEMICOLON@89..106 + 0: CSS_DECLARATION@89..105 + 0: CSS_GENERIC_PROPERTY@89..105 + 0: CSS_IDENTIFIER@89..98 + 0: IDENT@89..98 "margin" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@98..100 ":" [] [Whitespace(" ")] + 2: SCSS_EXPRESSION@100..105 + 0: SCSS_EXPRESSION_ITEM_LIST@100..105 + 0: SCSS_IDENTIFIER@100..105 + 0: DOLLAR@100..101 "$" [] [] + 1: CSS_IDENTIFIER@101..105 + 0: IDENT@101..105 "step" [] [] + 1: (empty) + 1: SEMICOLON@105..106 ";" [] [] + 2: R_CURLY@106..108 "}" [Newline("\n")] [] + 2: CSS_QUALIFIED_RULE@108..177 + 0: CSS_SELECTOR_LIST@108..116 + 0: CSS_COMPOUND_SELECTOR@108..116 + 0: CSS_NESTED_SELECTOR_LIST@108..108 + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@108..116 + 0: CSS_CLASS_SELECTOR@108..116 + 0: DOT@108..111 "." [Newline("\n"), Newline("\n")] [] + 1: CSS_CUSTOM_IDENTIFIER@111..116 + 0: IDENT@111..116 "grid" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@116..177 + 0: L_CURLY@116..117 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@117..175 + 0: CSS_AT_RULE@117..175 + 0: AT@117..121 "@" [Newline("\n"), Whitespace(" ")] [] + 1: SCSS_FOR_AT_RULE@121..175 + 0: FOR_KW@121..125 "for" [] [Whitespace(" ")] + 1: SCSS_IDENTIFIER@125..133 + 0: DOLLAR@125..126 "$" [] [] + 1: CSS_IDENTIFIER@126..133 + 0: IDENT@126..133 "column" [] [Whitespace(" ")] + 2: FROM_KW@133..138 "from" [] [Whitespace(" ")] + 3: SCSS_EXPRESSION@138..140 + 0: SCSS_EXPRESSION_ITEM_LIST@138..140 + 0: CSS_NUMBER@138..140 + 0: CSS_NUMBER_LITERAL@138..140 "1" [] [Whitespace(" ")] + 4: THROUGH_KW@140..148 "through" [] [Whitespace(" ")] + 5: SCSS_EXPRESSION@148..150 + 0: SCSS_EXPRESSION_ITEM_LIST@148..150 + 0: CSS_NUMBER@148..150 + 0: CSS_NUMBER_LITERAL@148..150 "4" [] [Whitespace(" ")] + 6: CSS_DECLARATION_OR_RULE_BLOCK@150..175 + 0: L_CURLY@150..151 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@151..171 + 0: CSS_DECLARATION_WITH_SEMICOLON@151..171 + 0: CSS_DECLARATION@151..170 + 0: CSS_GENERIC_PROPERTY@151..170 + 0: CSS_IDENTIFIER@151..161 + 0: IDENT@151..161 "order" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@161..163 ":" [] [Whitespace(" ")] + 2: SCSS_EXPRESSION@163..170 + 0: SCSS_EXPRESSION_ITEM_LIST@163..170 + 0: SCSS_IDENTIFIER@163..170 + 0: DOLLAR@163..164 "$" [] [] + 1: CSS_IDENTIFIER@164..170 + 0: IDENT@164..170 "column" [] [] + 1: (empty) + 1: SEMICOLON@170..171 ";" [] [] + 2: R_CURLY@171..175 "}" [Newline("\n"), Whitespace(" ")] [] + 2: R_CURLY@175..177 "}" [Newline("\n")] [] + 2: EOF@177..178 "" [Newline("\n")] [] + +``` diff --git a/crates/biome_css_syntax/src/generated/kind.rs b/crates/biome_css_syntax/src/generated/kind.rs index 01792cae0871..b17ed795a6ca 100644 --- a/crates/biome_css_syntax/src/generated/kind.rs +++ b/crates/biome_css_syntax/src/generated/kind.rs @@ -99,6 +99,7 @@ pub enum CssSyntaxKind { DEBUG_KW, WARN_KW, ERROR_KW, + FOR_KW, WHILE_KW, SASS_KW, STYLE_KW, @@ -110,6 +111,7 @@ pub enum CssSyntaxKind { OF_KW, FROM_KW, TO_KW, + THROUGH_KW, VAR_KW, URL_KW, IF_KW, @@ -551,6 +553,7 @@ pub enum CssSyntaxKind { CSS_RETURNS_STATEMENT, SCSS_EACH_AT_RULE, SCSS_EACH_BINDING_LIST, + SCSS_FOR_AT_RULE, SCSS_IF_AT_RULE, SCSS_WHILE_AT_RULE, SCSS_ELSE_CLAUSE, @@ -803,6 +806,7 @@ impl CssSyntaxKind { "debug" => DEBUG_KW, "warn" => WARN_KW, "error" => ERROR_KW, + "for" => FOR_KW, "while" => WHILE_KW, "sass" => SASS_KW, "style" => STYLE_KW, @@ -814,6 +818,7 @@ impl CssSyntaxKind { "of" => OF_KW, "from" => FROM_KW, "to" => TO_KW, + "through" => THROUGH_KW, "var" => VAR_KW, "url" => URL_KW, "if" => IF_KW, @@ -1062,6 +1067,7 @@ impl CssSyntaxKind { DEBUG_KW => "debug", WARN_KW => "warn", ERROR_KW => "error", + FOR_KW => "for", WHILE_KW => "while", SASS_KW => "sass", STYLE_KW => "style", @@ -1073,6 +1079,7 @@ impl CssSyntaxKind { OF_KW => "of", FROM_KW => "from", TO_KW => "to", + THROUGH_KW => "through", VAR_KW => "var", URL_KW => "url", IF_KW => "if", @@ -1237,4 +1244,4 @@ impl CssSyntaxKind { } #[doc = r" Utility macro for creating a SyntaxKind through simple macro syntax"] #[macro_export] -macro_rules ! T { [;] => { $ crate :: CssSyntaxKind :: SEMICOLON } ; [,] => { $ crate :: CssSyntaxKind :: COMMA } ; ['('] => { $ crate :: CssSyntaxKind :: L_PAREN } ; [')'] => { $ crate :: CssSyntaxKind :: R_PAREN } ; ['{'] => { $ crate :: CssSyntaxKind :: L_CURLY } ; ['}'] => { $ crate :: CssSyntaxKind :: R_CURLY } ; ['['] => { $ crate :: CssSyntaxKind :: L_BRACK } ; [']'] => { $ crate :: CssSyntaxKind :: R_BRACK } ; [<] => { $ crate :: CssSyntaxKind :: L_ANGLE } ; [>] => { $ crate :: CssSyntaxKind :: R_ANGLE } ; [~] => { $ crate :: CssSyntaxKind :: TILDE } ; [$] => { $ crate :: CssSyntaxKind :: DOLLAR } ; [#] => { $ crate :: CssSyntaxKind :: HASH } ; [&] => { $ crate :: CssSyntaxKind :: AMP } ; [|] => { $ crate :: CssSyntaxKind :: PIPE } ; [||] => { $ crate :: CssSyntaxKind :: PIPE2 } ; [+] => { $ crate :: CssSyntaxKind :: PLUS } ; [*] => { $ crate :: CssSyntaxKind :: STAR } ; [/] => { $ crate :: CssSyntaxKind :: SLASH } ; [^] => { $ crate :: CssSyntaxKind :: CARET } ; [%] => { $ crate :: CssSyntaxKind :: PERCENT } ; [.] => { $ crate :: CssSyntaxKind :: DOT } ; [...] => { $ crate :: CssSyntaxKind :: DOT3 } ; [:] => { $ crate :: CssSyntaxKind :: COLON } ; [::] => { $ crate :: CssSyntaxKind :: COLON2 } ; [=] => { $ crate :: CssSyntaxKind :: EQ } ; [==] => { $ crate :: CssSyntaxKind :: EQ2 } ; [!] => { $ crate :: CssSyntaxKind :: BANG } ; [!=] => { $ crate :: CssSyntaxKind :: NEQ } ; [-] => { $ crate :: CssSyntaxKind :: MINUS } ; [<=] => { $ crate :: CssSyntaxKind :: LTEQ } ; [>=] => { $ crate :: CssSyntaxKind :: GTEQ } ; [+=] => { $ crate :: CssSyntaxKind :: PLUSEQ } ; [|=] => { $ crate :: CssSyntaxKind :: PIPEEQ } ; [&=] => { $ crate :: CssSyntaxKind :: AMPEQ } ; [^=] => { $ crate :: CssSyntaxKind :: CARETEQ } ; [/=] => { $ crate :: CssSyntaxKind :: SLASHEQ } ; [*=] => { $ crate :: CssSyntaxKind :: STAREQ } ; [%=] => { $ crate :: CssSyntaxKind :: PERCENTEQ } ; [@] => { $ crate :: CssSyntaxKind :: AT } ; ["$="] => { $ crate :: CssSyntaxKind :: DOLLAR_EQ } ; [~=] => { $ crate :: CssSyntaxKind :: TILDE_EQ } ; [-->] => { $ crate :: CssSyntaxKind :: CDC } ; [] => { $ crate :: CssSyntaxKind :: CDC } ; [