diff --git a/crates/biome_css_analyze/src/assist/source/use_sorted_properties.rs b/crates/biome_css_analyze/src/assist/source/use_sorted_properties.rs index 96bab0ef84a5..a5d7ba8eefad 100644 --- a/crates/biome_css_analyze/src/assist/source/use_sorted_properties.rs +++ b/crates/biome_css_analyze/src/assist/source/use_sorted_properties.rs @@ -243,6 +243,7 @@ impl RecessOrderMember { AnyCssDeclarationOrRule::CssBogus(_) => NodeKindOrder::UnknownKind, AnyCssDeclarationOrRule::CssMetavariable(_) => NodeKindOrder::UnknownKind, AnyCssDeclarationOrRule::ScssDeclaration(_) => NodeKindOrder::UnknownKind, + AnyCssDeclarationOrRule::ScssNestingDeclaration(_) => NodeKindOrder::UnknownKind, AnyCssDeclarationOrRule::AnyCssRule(rule) => match rule { AnyCssRule::CssAtRule(_) => NodeKindOrder::NestedRuleOrAtRule, AnyCssRule::CssBogusRule(_) => NodeKindOrder::UnknownKind, diff --git a/crates/biome_css_factory/src/generated/node_factory.rs b/crates/biome_css_factory/src/generated/node_factory.rs index 0ebc7eb4fa79..2311492ef04c 100644 --- a/crates/biome_css_factory/src/generated/node_factory.rs +++ b/crates/biome_css_factory/src/generated/node_factory.rs @@ -3039,6 +3039,22 @@ pub fn scss_namespaced_identifier( ], )) } +pub fn scss_nesting_declaration( + name: CssIdentifier, + colon_token: SyntaxToken, + value: CssGenericComponentValueList, + block: AnyCssDeclarationOrRuleBlock, +) -> ScssNestingDeclaration { + ScssNestingDeclaration::unwrap_cast(SyntaxNode::new_detached( + CssSyntaxKind::SCSS_NESTING_DECLARATION, + [ + Some(SyntaxElement::Node(name.into_syntax())), + Some(SyntaxElement::Token(colon_token)), + Some(SyntaxElement::Node(value.into_syntax())), + Some(SyntaxElement::Node(block.into_syntax())), + ], + )) +} pub fn scss_qualified_name( module: CssIdentifier, dot_token: SyntaxToken, diff --git a/crates/biome_css_factory/src/generated/syntax_factory.rs b/crates/biome_css_factory/src/generated/syntax_factory.rs index ff73d4814901..5625227597f9 100644 --- a/crates/biome_css_factory/src/generated/syntax_factory.rs +++ b/crates/biome_css_factory/src/generated/syntax_factory.rs @@ -6226,6 +6226,46 @@ impl SyntaxFactory for CssSyntaxFactory { } slots.into_node(SCSS_NAMESPACED_IDENTIFIER, children) } + SCSS_NESTING_DECLARATION => { + let mut elements = (&children).into_iter(); + let mut slots: RawNodeSlots<4usize> = RawNodeSlots::default(); + let mut current_element = elements.next(); + if let Some(element) = ¤t_element + && CssIdentifier::can_cast(element.kind()) + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && element.kind() == T ! [:] + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && CssGenericComponentValueList::can_cast(element.kind()) + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && AnyCssDeclarationOrRuleBlock::can_cast(element.kind()) + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if current_element.is_some() { + return RawSyntaxNode::new( + SCSS_NESTING_DECLARATION.to_bogus(), + children.into_iter().map(Some), + ); + } + slots.into_node(SCSS_NESTING_DECLARATION, children) + } SCSS_QUALIFIED_NAME => { let mut elements = (&children).into_iter(); let mut slots: RawNodeSlots<3usize> = RawNodeSlots::default(); diff --git a/crates/biome_css_formatter/src/css/any/declaration.rs b/crates/biome_css_formatter/src/css/any/declaration.rs index 0f7f8405eb6b..5fcbe617f3f0 100644 --- a/crates/biome_css_formatter/src/css/any/declaration.rs +++ b/crates/biome_css_formatter/src/css/any/declaration.rs @@ -11,6 +11,7 @@ impl FormatRule for FormatAnyCssDeclaration { AnyCssDeclaration::CssDeclarationWithSemicolon(node) => node.format().fmt(f), AnyCssDeclaration::CssEmptyDeclaration(node) => node.format().fmt(f), AnyCssDeclaration::ScssDeclaration(node) => node.format().fmt(f), + AnyCssDeclaration::ScssNestingDeclaration(node) => node.format().fmt(f), } } } diff --git a/crates/biome_css_formatter/src/css/any/declaration_or_at_rule.rs b/crates/biome_css_formatter/src/css/any/declaration_or_at_rule.rs index 1a0c58f70eaf..d4ff050ef480 100644 --- a/crates/biome_css_formatter/src/css/any/declaration_or_at_rule.rs +++ b/crates/biome_css_formatter/src/css/any/declaration_or_at_rule.rs @@ -12,6 +12,7 @@ impl FormatRule for FormatAnyCssDeclarationOrAtRule { AnyCssDeclarationOrAtRule::CssDeclarationWithSemicolon(node) => node.format().fmt(f), AnyCssDeclarationOrAtRule::CssEmptyDeclaration(node) => node.format().fmt(f), AnyCssDeclarationOrAtRule::ScssDeclaration(node) => node.format().fmt(f), + AnyCssDeclarationOrAtRule::ScssNestingDeclaration(node) => node.format().fmt(f), } } } diff --git a/crates/biome_css_formatter/src/css/any/declaration_or_rule.rs b/crates/biome_css_formatter/src/css/any/declaration_or_rule.rs index 6884ff730e03..a7773afcd237 100644 --- a/crates/biome_css_formatter/src/css/any/declaration_or_rule.rs +++ b/crates/biome_css_formatter/src/css/any/declaration_or_rule.rs @@ -14,6 +14,7 @@ impl FormatRule for FormatAnyCssDeclarationOrRule { AnyCssDeclarationOrRule::CssEmptyDeclaration(node) => node.format().fmt(f), AnyCssDeclarationOrRule::CssMetavariable(node) => node.format().fmt(f), AnyCssDeclarationOrRule::ScssDeclaration(node) => node.format().fmt(f), + AnyCssDeclarationOrRule::ScssNestingDeclaration(node) => node.format().fmt(f), } } } diff --git a/crates/biome_css_formatter/src/generated.rs b/crates/biome_css_formatter/src/generated.rs index dc8ff5780d6a..9e5cc775161a 100644 --- a/crates/biome_css_formatter/src/generated.rs +++ b/crates/biome_css_formatter/src/generated.rs @@ -7110,6 +7110,44 @@ impl IntoFormat for biome_css_syntax::ScssNamespacedIdentifier ) } } +impl FormatRule + for crate::scss::auxiliary::nesting_declaration::FormatScssNestingDeclaration +{ + type Context = CssFormatContext; + #[inline(always)] + fn fmt( + &self, + node: &biome_css_syntax::ScssNestingDeclaration, + f: &mut CssFormatter, + ) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl AsFormat for biome_css_syntax::ScssNestingDeclaration { + type Format<'a> = FormatRefWithRule< + 'a, + biome_css_syntax::ScssNestingDeclaration, + crate::scss::auxiliary::nesting_declaration::FormatScssNestingDeclaration, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::scss::auxiliary::nesting_declaration::FormatScssNestingDeclaration::default(), + ) + } +} +impl IntoFormat for biome_css_syntax::ScssNestingDeclaration { + type Format = FormatOwnedWithRule< + biome_css_syntax::ScssNestingDeclaration, + crate::scss::auxiliary::nesting_declaration::FormatScssNestingDeclaration, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::scss::auxiliary::nesting_declaration::FormatScssNestingDeclaration::default(), + ) + } +} impl FormatRule for crate::scss::auxiliary::qualified_name::FormatScssQualifiedName { diff --git a/crates/biome_css_formatter/src/scss/auxiliary/mod.rs b/crates/biome_css_formatter/src/scss/auxiliary/mod.rs index acac6a0d8478..bec1ba300c52 100644 --- a/crates/biome_css_formatter/src/scss/auxiliary/mod.rs +++ b/crates/biome_css_formatter/src/scss/auxiliary/mod.rs @@ -1,5 +1,6 @@ //! This is a generated file. Don't modify it by hand! Run 'cargo codegen formatter' to re-generate the file. pub(crate) mod declaration; +pub(crate) mod nesting_declaration; pub(crate) mod qualified_name; pub(crate) mod variable_modifier; diff --git a/crates/biome_css_formatter/src/scss/auxiliary/nesting_declaration.rs b/crates/biome_css_formatter/src/scss/auxiliary/nesting_declaration.rs new file mode 100644 index 000000000000..ce38190e79cf --- /dev/null +++ b/crates/biome_css_formatter/src/scss/auxiliary/nesting_declaration.rs @@ -0,0 +1,24 @@ +use crate::prelude::*; +use biome_css_syntax::{ScssNestingDeclaration, ScssNestingDeclarationFields}; +use biome_formatter::write; + +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatScssNestingDeclaration; +impl FormatNodeRule for FormatScssNestingDeclaration { + fn fmt_fields(&self, node: &ScssNestingDeclaration, f: &mut CssFormatter) -> FormatResult<()> { + let ScssNestingDeclarationFields { + name, + colon_token, + value, + block, + } = node.as_fields(); + + write!(f, [name.format(), colon_token.format()])?; + + if !value.is_empty() { + write!(f, [space(), value.format()])?; + } + + write!(f, [space(), block.format()]) + } +} diff --git a/crates/biome_css_formatter/tests/specs/css/scss/declaration/ambiguous-selector-vs-nesting.scss b/crates/biome_css_formatter/tests/specs/css/scss/declaration/ambiguous-selector-vs-nesting.scss new file mode 100644 index 000000000000..c5da54bc142f --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/scss/declaration/ambiguous-selector-vs-nesting.scss @@ -0,0 +1,6 @@ +.test{ + label:hover{color:red;} + font:bold{color:blue;} + font:12px{family:sans-serif;} + font: bold{family:serif;} +} diff --git a/crates/biome_css_formatter/tests/specs/css/scss/declaration/ambiguous-selector-vs-nesting.scss.snap b/crates/biome_css_formatter/tests/specs/css/scss/declaration/ambiguous-selector-vs-nesting.scss.snap new file mode 100644 index 000000000000..d55916415710 --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/scss/declaration/ambiguous-selector-vs-nesting.scss.snap @@ -0,0 +1,50 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +info: css/scss/declaration/ambiguous-selector-vs-nesting.scss +--- + +# Input + +```scss +.test{ + label:hover{color:red;} + font:bold{color:blue;} + font:12px{family:sans-serif;} + font: bold{family:serif;} +} + +``` + + +============================= + +# Outputs + +## Output 1 + +----- +Indent style: Tab +Indent width: 2 +Line ending: LF +Line width: 80 +Quote style: Double Quotes +Trailing newline: true +----- + +```scss +.test { + label:hover { + color: red; + } + font:bold { + color: blue; + } + font: 12px { + family: sans-serif; + } + font: bold { + family: serif; + } +} + +``` diff --git a/crates/biome_css_formatter/tests/specs/css/scss/declaration/nested-properties-empty-value.scss b/crates/biome_css_formatter/tests/specs/css/scss/declaration/nested-properties-empty-value.scss new file mode 100644 index 000000000000..d9e33367c795 --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/scss/declaration/nested-properties-empty-value.scss @@ -0,0 +1,6 @@ +.font{ + color:red; + font:{ + size:12px; + } +} diff --git a/crates/biome_css_formatter/tests/specs/css/scss/declaration/nested-properties-empty-value.scss.snap b/crates/biome_css_formatter/tests/specs/css/scss/declaration/nested-properties-empty-value.scss.snap new file mode 100644 index 000000000000..53feb60450ec --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/scss/declaration/nested-properties-empty-value.scss.snap @@ -0,0 +1,43 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +assertion_line: 212 +info: css/scss/declaration/nested-properties-empty-value.scss +--- + +# Input + +```scss +.font{ + color:red; + font:{ + size:12px; + } +} + +``` + + +============================= + +# Outputs + +## Output 1 + +----- +Indent style: Tab +Indent width: 2 +Line ending: LF +Line width: 80 +Quote style: Double Quotes +Trailing newline: true +----- + +```scss +.font { + color: red; + font: { + size: 12px; + } +} + +``` diff --git a/crates/biome_css_formatter/tests/specs/css/scss/declaration/nested-properties-with-value.scss b/crates/biome_css_formatter/tests/specs/css/scss/declaration/nested-properties-with-value.scss new file mode 100644 index 000000000000..4721b4e6d863 --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/scss/declaration/nested-properties-with-value.scss @@ -0,0 +1,6 @@ +.font{ + color:red; + font:12px/1.2{ + family:sans-serif; + } +} diff --git a/crates/biome_css_formatter/tests/specs/css/scss/declaration/nested-properties-with-value.scss.snap b/crates/biome_css_formatter/tests/specs/css/scss/declaration/nested-properties-with-value.scss.snap new file mode 100644 index 000000000000..710f717b44f8 --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/scss/declaration/nested-properties-with-value.scss.snap @@ -0,0 +1,42 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +info: css/scss/declaration/nested-properties-with-value.scss +--- + +# Input + +```scss +.font{ + color:red; + font:12px/1.2{ + family:sans-serif; + } +} + +``` + + +============================= + +# Outputs + +## Output 1 + +----- +Indent style: Tab +Indent width: 2 +Line ending: LF +Line width: 80 +Quote style: Double Quotes +Trailing newline: true +----- + +```scss +.font { + color: red; + font: 12px / 1.2 { + family: sans-serif; + } +} + +``` diff --git a/crates/biome_css_formatter/tests/specs/css/scss/declaration/spacing-after-colon.scss b/crates/biome_css_formatter/tests/specs/css/scss/declaration/spacing-after-colon.scss new file mode 100644 index 000000000000..8c114f89ed72 --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/scss/declaration/spacing-after-colon.scss @@ -0,0 +1,6 @@ +.test{ + font:bold{color:red;} + font: bold{family:serif;} + font: + bold{family:sans-serif;} +} diff --git a/crates/biome_css_formatter/tests/specs/css/scss/declaration/spacing-after-colon.scss.snap b/crates/biome_css_formatter/tests/specs/css/scss/declaration/spacing-after-colon.scss.snap new file mode 100644 index 000000000000..00402d53d319 --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/scss/declaration/spacing-after-colon.scss.snap @@ -0,0 +1,47 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +info: css/scss/declaration/spacing-after-colon.scss +--- + +# Input + +```scss +.test{ + font:bold{color:red;} + font: bold{family:serif;} + font: + bold{family:sans-serif;} +} + +``` + + +============================= + +# Outputs + +## Output 1 + +----- +Indent style: Tab +Indent width: 2 +Line ending: LF +Line width: 80 +Quote style: Double Quotes +Trailing newline: true +----- + +```scss +.test { + font:bold { + color: red; + } + font: bold { + family: serif; + } + font: bold { + family: sans-serif; + } +} + +``` diff --git a/crates/biome_css_parser/src/lexer/mod.rs b/crates/biome_css_parser/src/lexer/mod.rs index 7b7a9ee96532..e6f08c22b613 100644 --- a/crates/biome_css_parser/src/lexer/mod.rs +++ b/crates/biome_css_parser/src/lexer/mod.rs @@ -76,6 +76,8 @@ pub(crate) struct CssLexer<'src> { /// `true` if there has been a line break between the last non-trivia token and the next non-trivia token. after_newline: bool, + /// `true` if there has been trivia between the last non-trivia token and the next non-trivia token. + after_whitespace: bool, /// If the source starts with a Unicode BOM, this is the number of bytes for that token. unicode_bom_length: usize, @@ -145,10 +147,15 @@ impl<'src> Lexer<'src> for CssLexer<'src> { self.current_flags .set(TokenFlags::PRECEDING_LINE_BREAK, self.after_newline); + self.current_flags + .set(TokenFlags::PRECEDING_WHITESPACE, self.after_whitespace); self.current_kind = kind; if !kind.is_trivia() { self.after_newline = false; + self.after_whitespace = false; + } else if kind == Self::WHITESPACE { + self.after_whitespace = true; } kind @@ -169,6 +176,7 @@ impl<'src> Lexer<'src> for CssLexer<'src> { current_flags, current_kind, after_line_break, + after_whitespace, unicode_bom_length, diagnostics_pos, } = checkpoint; @@ -180,6 +188,7 @@ impl<'src> Lexer<'src> for CssLexer<'src> { self.current_start = current_start; self.current_flags = current_flags; self.after_newline = after_line_break; + self.after_whitespace = after_whitespace; self.unicode_bom_length = unicode_bom_length; self.diagnostics.truncate(diagnostics_pos as usize); } @@ -211,6 +220,7 @@ impl<'src> CssLexer<'src> { Self { source, after_newline: false, + after_whitespace: false, unicode_bom_length: 0, current_kind: TOMBSTONE, current_start: TextSize::from(0), @@ -1437,6 +1447,7 @@ impl<'src> LexerWithCheckpoint<'src> for CssLexer<'src> { current_flags: self.current_flags, current_kind: self.current_kind, after_line_break: self.after_newline, + after_whitespace: self.after_whitespace, unicode_bom_length: self.unicode_bom_length, diagnostics_pos: self.diagnostics.len() as u32, } diff --git a/crates/biome_css_parser/src/syntax/block/declaration_or_at_rule_list_block.rs b/crates/biome_css_parser/src/syntax/block/declaration_or_at_rule_list_block.rs index 71b084d7a74b..cb53b04b83ba 100644 --- a/crates/biome_css_parser/src/syntax/block/declaration_or_at_rule_list_block.rs +++ b/crates/biome_css_parser/src/syntax/block/declaration_or_at_rule_list_block.rs @@ -1,15 +1,21 @@ use crate::parser::CssParser; use crate::syntax::at_rule::{is_at_at_rule, parse_at_rule}; use crate::syntax::block::ParseBlockBody; -use crate::syntax::parse_error::expected_any_declaration_or_at_rule; -use crate::syntax::{is_at_any_declaration_with_semicolon, parse_any_declaration_with_semicolon}; +use crate::syntax::parse_error::{expected_any_declaration_or_at_rule, scss_only_syntax_error}; +use crate::syntax::scss::{ + is_at_scss_declaration, is_at_scss_nesting_declaration, parse_scss_declaration, + parse_scss_nesting_declaration, +}; +use crate::syntax::{ + CssSyntaxFeatures, is_at_any_declaration_with_semicolon, parse_any_declaration_with_semicolon, +}; use biome_css_syntax::CssSyntaxKind::*; use biome_css_syntax::{CssSyntaxKind, T}; use biome_parser::parse_lists::ParseNodeList; use biome_parser::parse_recovery::{ParseRecovery, RecoveryResult}; use biome_parser::parsed_syntax::ParsedSyntax; use biome_parser::parsed_syntax::ParsedSyntax::Absent; -use biome_parser::{CompletedMarker, Parser}; +use biome_parser::{CompletedMarker, Parser, SyntaxFeature}; #[inline] pub(crate) fn parse_declaration_or_at_rule_list_block(p: &mut CssParser) -> CompletedMarker { @@ -32,7 +38,10 @@ impl ParseBlockBody for DeclarationOrAtRuleListBlock { #[inline] fn is_at_declaration_or_at_rule_item(p: &mut CssParser) -> bool { - is_at_at_rule(p) || is_at_any_declaration_with_semicolon(p) + is_at_at_rule(p) + || is_at_scss_declaration(p) + || is_at_scss_nesting_declaration(p) + || is_at_any_declaration_with_semicolon(p) } struct DeclarationOrAtRuleListParseRecovery; @@ -55,6 +64,21 @@ impl ParseNodeList for DeclarationOrAtRuleList { fn parse_element(&mut self, p: &mut Self::Parser<'_>) -> ParsedSyntax { if is_at_at_rule(p) { parse_at_rule(p) + } else if is_at_scss_declaration(p) { + CssSyntaxFeatures::Scss.parse_exclusive_syntax( + p, + parse_scss_declaration, + |p, marker| { + scss_only_syntax_error(p, "SCSS variable declarations", marker.range(p)) + }, + ) + } else if is_at_scss_nesting_declaration(p) { + // Parse nested properties before generic declarations to keep `{` blocks intact. + CssSyntaxFeatures::Scss.parse_exclusive_syntax( + p, + parse_scss_nesting_declaration, + |p, marker| scss_only_syntax_error(p, "SCSS nesting declarations", marker.range(p)), + ) } else if is_at_any_declaration_with_semicolon(p) { parse_any_declaration_with_semicolon(p) } else { diff --git a/crates/biome_css_parser/src/syntax/block/declaration_or_rule_list_block.rs b/crates/biome_css_parser/src/syntax/block/declaration_or_rule_list_block.rs index 57820aab2107..3c65657ced77 100644 --- a/crates/biome_css_parser/src/syntax/block/declaration_or_rule_list_block.rs +++ b/crates/biome_css_parser/src/syntax/block/declaration_or_rule_list_block.rs @@ -1,11 +1,16 @@ use crate::parser::CssParser; use crate::syntax::at_rule::{is_at_at_rule, parse_at_rule}; use crate::syntax::block::ParseBlockBody; -use crate::syntax::parse_error::expected_any_declaration_or_at_rule; +use crate::syntax::parse_error::{expected_any_declaration_or_at_rule, scss_only_syntax_error}; +use crate::syntax::scss::{ + is_at_scss_declaration, is_at_scss_nesting_declaration, parse_scss_declaration, + parse_scss_nesting_declaration, +}; use crate::syntax::{ - is_at_any_declaration_with_semicolon, is_at_metavariable, is_at_nested_qualified_rule, - parse_any_declaration_with_semicolon, parse_metavariable, parse_nested_qualified_rule, - try_parse, + CssSyntaxFeatures, is_at_any_declaration_with_semicolon, is_at_metavariable, + is_at_nested_qualified_rule, is_nth_at_identifier, parse_any_declaration_with_semicolon, + parse_metavariable, parse_nested_qualified_rule, try_parse, + try_parse_nested_qualified_rule_without_selector_recovery, }; use biome_css_syntax::CssSyntaxKind::*; use biome_css_syntax::{CssSyntaxKind, T}; @@ -13,7 +18,8 @@ use biome_parser::parse_lists::ParseNodeList; use biome_parser::parse_recovery::{ParseRecovery, RecoveryResult}; use biome_parser::prelude::ParsedSyntax; use biome_parser::prelude::ParsedSyntax::Absent; -use biome_parser::{CompletedMarker, Parser}; +use biome_parser::token_source::TokenSourceWithBufferedLexer; +use biome_parser::{CompletedMarker, Parser, SyntaxFeature}; #[inline] pub(crate) fn parse_declaration_or_rule_list_block(p: &mut CssParser) -> CompletedMarker { @@ -38,10 +44,32 @@ impl ParseBlockBody for DeclarationOrRuleListBlock { fn is_at_declaration_or_rule_item(p: &mut CssParser) -> bool { is_at_at_rule(p) || is_at_nested_qualified_rule(p) + || is_at_scss_nesting_declaration(p) + || is_at_scss_declaration(p) || is_at_any_declaration_with_semicolon(p) || is_at_metavariable(p) } +#[inline] +fn has_whitespace_after_scss_property_colon(p: &mut CssParser) -> bool { + // We enter this helper at `ident` in `ident:...`. + // `nth_non_trivia(1)` is the `:` token, so `nth_non_trivia(2)` is the first token + // after `:`. Its preceding flags tell us whether there was spacing after the colon. + let Some(after_colon) = p.source_mut().lexer().nth_non_trivia(2) else { + return false; + }; + + after_colon.has_preceding_whitespace() || after_colon.has_preceding_line_break() +} + +#[inline] +fn is_at_ambiguous_scss_nesting_item(p: &mut CssParser) -> bool { + // Match Sass's ambiguity gate: only no-spacing `name:ident` and `name::...` + // forms can be nested selectors. + !has_whitespace_after_scss_property_colon(p) + && (is_nth_at_identifier(p, 2) || p.nth_at(2, T![:])) +} + struct DeclarationOrRuleListParseRecovery { end_kind: CssSyntaxKind, } @@ -80,6 +108,47 @@ impl ParseNodeList for DeclarationOrRuleList { fn parse_element(&mut self, p: &mut Self::Parser<'_>) -> ParsedSyntax { if is_at_at_rule(p) { parse_at_rule(p) + } else if is_at_scss_nesting_declaration(p) { + let is_ambiguous = is_at_ambiguous_scss_nesting_item(p); + + if is_ambiguous { + // Match Sass's declaration-first strategy for ambiguous `name:ident` and + // `name::...` forms. Parse as declaration first, then backtrack to selector + // parsing when the result is declaration-like but selector-ambiguous. + let declaration = try_parse(p, |p| { + let declaration = parse_scss_nesting_declaration(p); + + match declaration.kind(p) { + Some(SCSS_NESTING_DECLARATION) => Err(()), + Some(CSS_DECLARATION_WITH_SEMICOLON) + if matches!(p.last(), Some(T![;])) || p.at(self.end_kind) => + { + Ok(declaration) + } + _ => Err(()), + } + }); + + if let Ok(declaration) = declaration { + return declaration; + } + + if let Ok(rule) = + try_parse_nested_qualified_rule_without_selector_recovery(p, self.end_kind) + { + return rule; + } + } + + parse_scss_nesting_declaration(p) + } else if is_at_scss_declaration(p) { + CssSyntaxFeatures::Scss.parse_exclusive_syntax( + p, + parse_scss_declaration, + |p, marker| { + scss_only_syntax_error(p, "SCSS variable declarations", marker.range(p)) + }, + ) } else if is_at_any_declaration_with_semicolon(p) { // if we are at a declaration, // we still can have a nested qualified rule or a declaration @@ -119,11 +188,8 @@ impl ParseNodeList for DeclarationOrRuleList { // } <--- // The closing brace indicates the end of the declaration block. // If either condition is true, the declaration is considered valid. - if matches!(p.last(), Some(T![;])) || p.at(self.end_kind) { - Ok(declaration) - } else { - Err(()) - } + let valid = matches!(p.last(), Some(T![;])) || p.at(self.end_kind); + if valid { Ok(declaration) } else { Err(()) } }); // If parsing as a declaration was successful, return the parsed declaration. diff --git a/crates/biome_css_parser/src/syntax/declaration.rs b/crates/biome_css_parser/src/syntax/declaration.rs index 06a80c8d5798..fd348b1719ca 100644 --- a/crates/biome_css_parser/src/syntax/declaration.rs +++ b/crates/biome_css_parser/src/syntax/declaration.rs @@ -2,7 +2,10 @@ use crate::parser::CssParser; use crate::syntax::CssSyntaxFeatures; use crate::syntax::parse_error::{expected_declaration_item, scss_only_syntax_error}; use crate::syntax::property::{is_at_any_property, parse_any_property}; -use crate::syntax::scss::{is_at_scss_declaration, parse_scss_declaration}; +use crate::syntax::scss::{ + is_at_scss_declaration, is_at_scss_nesting_declaration, parse_scss_declaration, + parse_scss_nesting_declaration, +}; use biome_css_syntax::CssSyntaxKind::*; use biome_css_syntax::{CssSyntaxKind, T}; use biome_parser::parse_lists::ParseNodeList; @@ -19,7 +22,15 @@ impl ParseNodeList for DeclarationList { const LIST_KIND: Self::Kind = CSS_DECLARATION_LIST; fn parse_element(&mut self, p: &mut Self::Parser<'_>) -> ParsedSyntax { - parse_any_declaration_with_semicolon(p) + if is_at_scss_nesting_declaration(p) { + CssSyntaxFeatures::Scss.parse_exclusive_syntax( + p, + parse_scss_nesting_declaration, + |p, marker| scss_only_syntax_error(p, "SCSS nesting declarations", marker.range(p)), + ) + } else { + parse_any_declaration_with_semicolon(p) + } } fn is_at_list_end(&self, p: &mut Self::Parser<'_>) -> bool { diff --git a/crates/biome_css_parser/src/syntax/mod.rs b/crates/biome_css_parser/src/syntax/mod.rs index e2ae5c179366..5e4749dab0f8 100644 --- a/crates/biome_css_parser/src/syntax/mod.rs +++ b/crates/biome_css_parser/src/syntax/mod.rs @@ -245,17 +245,67 @@ pub(crate) fn is_at_nested_qualified_rule(p: &mut CssParser) -> bool { /// the success of parsing the block. #[inline] pub(crate) fn parse_nested_qualified_rule(p: &mut CssParser) -> ParsedSyntax { + parse_nested_qualified_rule_with_selector_recovery(p, false).map_or(Absent, |(rule, _)| rule) +} + +/// Speculatively parses a nested qualified rule without selector recovery. +/// +/// This is used to disambiguate SCSS nesting declarations from nested qualified rules. +/// The parse is considered successful only when the selector is strict and the parsed +/// block is complete. +#[inline] +pub(crate) fn try_parse_nested_qualified_rule_without_selector_recovery( + p: &mut CssParser, + end_kind: CssSyntaxKind, +) -> Result { + try_parse(p, |p| { + let Some((rule, block_kind)) = parse_nested_qualified_rule_with_selector_recovery(p, true) + else { + return Err(()); + }; + + if block_kind != CSS_DECLARATION_OR_RULE_BLOCK + || p.last().is_none_or(|kind| kind != end_kind) + { + return Err(()); + } + + Ok(rule) + }) +} + +#[inline] +fn parse_nested_qualified_rule_with_selector_recovery( + p: &mut CssParser, + disable_selector_recovery: bool, +) -> Option<(ParsedSyntax, CssSyntaxKind)> { if !is_at_nested_qualified_rule(p) { - return Absent; + return None; } let m = p.start(); - RelativeSelectorList::new(T!['{']).parse_list(p); + if disable_selector_recovery { + RelativeSelectorList::new(T!['{']) + .disable_recovery() + .parse_list(p); - parse_declaration_or_rule_list_block(p); + // In strict mode, reject selectors that don't reach the opening brace. + if !p.at(T!['{']) { + m.abandon(p); + return None; + } + } else { + RelativeSelectorList::new(T!['{']).parse_list(p); + } + + let block = parse_declaration_or_rule_list_block(p); + let block_kind = block.kind(p); - Present(m.complete(p, CSS_NESTED_QUALIFIED_RULE)) + Some(( + Present(m.complete(p, CSS_NESTED_QUALIFIED_RULE)), + block_kind, + )) } #[inline] diff --git a/crates/biome_css_parser/src/syntax/scss/declaration/mod.rs b/crates/biome_css_parser/src/syntax/scss/declaration/mod.rs new file mode 100644 index 000000000000..cefc97da22c2 --- /dev/null +++ b/crates/biome_css_parser/src/syntax/scss/declaration/mod.rs @@ -0,0 +1,5 @@ +mod nesting; +mod variable; + +pub(crate) use nesting::{is_at_scss_nesting_declaration, parse_scss_nesting_declaration}; +pub(crate) use variable::{is_at_scss_declaration, parse_scss_declaration}; diff --git a/crates/biome_css_parser/src/syntax/scss/declaration/nesting.rs b/crates/biome_css_parser/src/syntax/scss/declaration/nesting.rs new file mode 100644 index 000000000000..93c820fbfe21 --- /dev/null +++ b/crates/biome_css_parser/src/syntax/scss/declaration/nesting.rs @@ -0,0 +1,142 @@ +use crate::parser::CssParser; +use crate::syntax::block::parse_declaration_or_rule_list_block; +use crate::syntax::parse_error::expected_component_value; +use crate::syntax::property::parse_generic_component_value; +use crate::syntax::{ + CssSyntaxFeatures, is_at_dashed_identifier, is_at_identifier, parse_regular_identifier, +}; +use biome_css_syntax::CssSyntaxKind::{ + CSS_BOGUS_PROPERTY_VALUE, CSS_DECLARATION, CSS_DECLARATION_IMPORTANT, + CSS_DECLARATION_WITH_SEMICOLON, CSS_GENERIC_PROPERTY, EOF, SCSS_NESTING_DECLARATION, +}; +use biome_css_syntax::{CssSyntaxKind, T}; +use biome_parser::parse_lists::ParseNodeList; +use biome_parser::parse_recovery::{ParseRecoveryTokenSet, RecoveryResult}; +use biome_parser::prelude::ParsedSyntax; +use biome_parser::prelude::ParsedSyntax::{Absent, Present}; +use biome_parser::{CompletedMarker, Parser, SyntaxFeature, token_set}; + +/// Detects nested property syntax (`prop: { ... }`) while excluding custom properties +/// and CSS Modules declarations that must remain regular properties. +/// +/// Example: +/// ```scss +/// font: { size: 12px; } +/// ``` +/// +/// Docs: https://sass-lang.com/documentation/style-rules/declarations#nested-properties +#[inline] +pub(crate) fn is_at_scss_nesting_declaration(p: &mut CssParser) -> bool { + CssSyntaxFeatures::Scss.is_supported(p) + && is_at_identifier(p) + && p.nth_at(1, T![:]) + && !is_at_dashed_identifier(p) + && !p.at(T![composes]) +} + +/// Parses a SCSS nested property declaration block, or falls back to a regular declaration +/// when no block follows. +/// +/// Example: +/// ```scss +/// font: { +/// family: sans-serif; +/// size: 12px; +/// } +/// ``` +/// +/// Specification: https://sass-lang.com/documentation/style-rules/declarations#nested-properties +#[inline] +pub(crate) fn parse_scss_nesting_declaration(p: &mut CssParser) -> ParsedSyntax { + if !is_at_scss_nesting_declaration(p) { + return Absent; + } + + let m = p.start(); + let property = p.start(); + parse_regular_identifier(p).ok(); + p.expect(T![:]); + let missing_value = p.at(T![;]) || p.at(T!['}']) || p.at(EOF) || p.at(T![!]); + ScssNestingDeclarationValueList.parse_list(p); + + if p.at(T!['{']) { + // Upgrade to a nested-property block only if `{` follows the value. + property.abandon(p); + parse_declaration_or_rule_list_block(p); + return Present(m.complete(p, SCSS_NESTING_DECLARATION)); + } + + if missing_value { + p.error(expected_component_value(p, p.cur_range())); + } + + // Otherwise, reinterpret the parsed property/value as a regular declaration. + let property = property.complete(p, CSS_GENERIC_PROPERTY); + let declaration = property.precede(p); + parse_declaration_important(p).ok(); + let declaration = declaration.complete(p, CSS_DECLARATION); + + m.abandon(p); + Present(complete_declaration_with_semicolon(p, declaration)) +} + +#[inline] +fn complete_declaration_with_semicolon( + p: &mut CssParser, + declaration: CompletedMarker, +) -> CompletedMarker { + let m = declaration.precede(p); + + if !p.at(T!['}']) { + if p.nth_at(1, T!['}']) { + p.eat(T![;]); + } else { + p.expect(T![;]); + } + } + + m.complete(p, CSS_DECLARATION_WITH_SEMICOLON) +} + +#[inline] +fn parse_declaration_important(p: &mut CssParser) -> ParsedSyntax { + if !(p.at(T![!]) && p.nth_at(1, T![important])) { + return Absent; + } + + let m = p.start(); + p.bump(T![!]); + p.bump(T![important]); + Present(m.complete(p, CSS_DECLARATION_IMPORTANT)) +} + +struct ScssNestingDeclarationValueList; + +impl ParseNodeList for ScssNestingDeclarationValueList { + type Kind = CssSyntaxKind; + type Parser<'source> = CssParser<'source>; + const LIST_KIND: Self::Kind = CssSyntaxKind::CSS_GENERIC_COMPONENT_VALUE_LIST; + + fn parse_element(&mut self, p: &mut Self::Parser<'_>) -> ParsedSyntax { + parse_generic_component_value(p) + } + + fn is_at_list_end(&self, p: &mut Self::Parser<'_>) -> bool { + p.at(T!['{']) || p.at(T![;]) || p.at(T!['}']) || p.at(EOF) || p.at(T![!]) + } + + fn recover( + &mut self, + p: &mut Self::Parser<'_>, + parsed_element: ParsedSyntax, + ) -> RecoveryResult { + parsed_element.or_recover_with_token_set( + p, + &ParseRecoveryTokenSet::new( + CSS_BOGUS_PROPERTY_VALUE, + token_set!(T!['{'], T![;], T!['}'], T![!], EOF), + ), + expected_component_value, + ) + } +} diff --git a/crates/biome_css_parser/src/syntax/scss/declaration.rs b/crates/biome_css_parser/src/syntax/scss/declaration/variable.rs similarity index 86% rename from crates/biome_css_parser/src/syntax/scss/declaration.rs rename to crates/biome_css_parser/src/syntax/scss/declaration/variable.rs index 2288681ae9a4..4dd9c08baec4 100644 --- a/crates/biome_css_parser/src/syntax/scss/declaration.rs +++ b/crates/biome_css_parser/src/syntax/scss/declaration/variable.rs @@ -1,5 +1,4 @@ -use super::is_at_scss_identifier; -use super::parse_scss_identifier; +use super::super::{is_at_scss_identifier, parse_scss_identifier}; use crate::parser::CssParser; use crate::syntax::property::GenericComponentValueList; use crate::syntax::{is_at_identifier, is_nth_at_identifier, parse_regular_identifier}; @@ -15,6 +14,14 @@ use biome_parser::prelude::ParsedSyntax; use biome_parser::prelude::ParsedSyntax::{Absent, Present}; use biome_parser::{Parser, TokenSet, token_set}; +/// Detects a SCSS variable declaration (including module-qualified variables). +/// +/// Example: +/// ```scss +/// $primary: #c00; +/// ``` +/// +/// Docs: https://sass-lang.com/documentation/variables #[inline] pub(crate) fn is_at_scss_declaration(p: &mut CssParser) -> bool { if is_at_scss_identifier(p) { @@ -26,6 +33,15 @@ pub(crate) fn is_at_scss_declaration(p: &mut CssParser) -> bool { } } +/// Parses a SCSS variable declaration, including trailing `!default`/`!global`. +/// +/// Examples: +/// ```scss +/// $primary: #c00; +/// $spacing: 1rem; +/// ``` +/// +/// Specification: https://sass-lang.com/documentation/variables #[inline] pub(crate) fn parse_scss_declaration(p: &mut CssParser) -> ParsedSyntax { if !is_at_scss_declaration(p) { @@ -42,6 +58,7 @@ pub(crate) fn parse_scss_declaration(p: &mut CssParser) -> ParsedSyntax { if !p.at(T!['}']) && !p.at(EOF) { if p.nth_at(1, T!['}']) { + // Allow a trailing `;` before `}` but don't require it. p.eat(T![;]); } else { p.expect(T![;]); diff --git a/crates/biome_css_parser/src/syntax/scss/mod.rs b/crates/biome_css_parser/src/syntax/scss/mod.rs index 65c9c97afec0..c44f7ec4b555 100644 --- a/crates/biome_css_parser/src/syntax/scss/mod.rs +++ b/crates/biome_css_parser/src/syntax/scss/mod.rs @@ -8,7 +8,10 @@ use biome_parser::Parser; use biome_parser::prelude::ParsedSyntax; use biome_parser::prelude::ParsedSyntax::{Absent, Present}; -pub(crate) use declaration::{is_at_scss_declaration, parse_scss_declaration}; +pub(crate) use declaration::{ + is_at_scss_declaration, is_at_scss_nesting_declaration, parse_scss_declaration, + parse_scss_nesting_declaration, +}; #[inline] pub(crate) fn is_at_scss_identifier(p: &mut CssParser) -> bool { diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/ambiguous-selector-vs-nesting.scss b/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/ambiguous-selector-vs-nesting.scss new file mode 100644 index 000000000000..4e52a603bb06 --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/ambiguous-selector-vs-nesting.scss @@ -0,0 +1,17 @@ +.test { + label:hover { + color: red; + } + + font:bold { + color: blue; + } + + font:12px { + family: sans-serif; + } + + font: bold { + family: serif; + } +} diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/ambiguous-selector-vs-nesting.scss.snap b/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/ambiguous-selector-vs-nesting.scss.snap new file mode 100644 index 000000000000..c5577f894dc7 --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/ambiguous-selector-vs-nesting.scss.snap @@ -0,0 +1,359 @@ +--- +source: crates/biome_css_parser/tests/spec_test.rs +expression: snapshot +--- + +## Input + +```css +.test { + label:hover { + color: red; + } + + font:bold { + color: blue; + } + + font:12px { + family: sans-serif; + } + + font: bold { + family: serif; + } +} + +``` + + +## AST + +``` +CssRoot { + bom_token: missing (optional), + items: CssRootItemList [ + CssQualifiedRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@0..1 "." [] [], + name: CssCustomIdentifier { + value_token: IDENT@1..6 "test" [] [Whitespace(" ")], + }, + }, + ], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@6..7 "{" [] [], + items: CssDeclarationOrRuleList [ + CssNestedQualifiedRule { + prelude: CssRelativeSelectorList [ + CssRelativeSelector { + combinator: missing (optional), + selector: CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: CssTypeSelector { + namespace: missing (optional), + ident: CssIdentifier { + value_token: IDENT@7..15 "label" [Newline("\n"), Whitespace(" ")] [], + }, + }, + sub_selectors: CssSubSelectorList [ + CssPseudoClassSelector { + colon_token: COLON@15..16 ":" [] [], + class: CssPseudoClassIdentifier { + name: CssIdentifier { + value_token: IDENT@16..22 "hover" [] [Whitespace(" ")], + }, + }, + }, + ], + }, + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@22..23 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@23..33 "color" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@33..35 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@35..38 "red" [] [], + }, + ], + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@38..39 ";" [] [], + }, + ], + r_curly_token: R_CURLY@39..43 "}" [Newline("\n"), Whitespace(" ")] [], + }, + }, + CssNestedQualifiedRule { + prelude: CssRelativeSelectorList [ + CssRelativeSelector { + combinator: missing (optional), + selector: CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: CssTypeSelector { + namespace: missing (optional), + ident: CssIdentifier { + value_token: IDENT@43..51 "font" [Newline("\n"), Newline("\n"), Whitespace(" ")] [], + }, + }, + sub_selectors: CssSubSelectorList [ + CssPseudoClassSelector { + colon_token: COLON@51..52 ":" [] [], + class: CssPseudoClassIdentifier { + name: CssIdentifier { + value_token: IDENT@52..57 "bold" [] [Whitespace(" ")], + }, + }, + }, + ], + }, + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@57..58 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@58..68 "color" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@68..70 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@70..74 "blue" [] [], + }, + ], + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@74..75 ";" [] [], + }, + ], + r_curly_token: R_CURLY@75..79 "}" [Newline("\n"), Whitespace(" ")] [], + }, + }, + ScssNestingDeclaration { + name: CssIdentifier { + value_token: IDENT@79..87 "font" [Newline("\n"), Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@87..88 ":" [] [], + value: CssGenericComponentValueList [ + CssRegularDimension { + value_token: CSS_NUMBER_LITERAL@88..90 "12" [] [], + unit_token: IDENT@90..93 "px" [] [Whitespace(" ")], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@93..94 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@94..105 "family" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@105..107 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@107..117 "sans-serif" [] [], + }, + ], + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@117..118 ";" [] [], + }, + ], + r_curly_token: R_CURLY@118..122 "}" [Newline("\n"), Whitespace(" ")] [], + }, + }, + ScssNestingDeclaration { + name: CssIdentifier { + value_token: IDENT@122..130 "font" [Newline("\n"), Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@130..132 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@132..137 "bold" [] [Whitespace(" ")], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@137..138 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@138..149 "family" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@149..151 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@151..156 "serif" [] [], + }, + ], + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@156..157 ";" [] [], + }, + ], + r_curly_token: R_CURLY@157..161 "}" [Newline("\n"), Whitespace(" ")] [], + }, + }, + ], + r_curly_token: R_CURLY@161..163 "}" [Newline("\n")] [], + }, + }, + ], + eof_token: EOF@163..164 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: CSS_ROOT@0..164 + 0: (empty) + 1: CSS_ROOT_ITEM_LIST@0..163 + 0: CSS_QUALIFIED_RULE@0..163 + 0: CSS_SELECTOR_LIST@0..6 + 0: CSS_COMPOUND_SELECTOR@0..6 + 0: CSS_NESTED_SELECTOR_LIST@0..0 + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@0..6 + 0: CSS_CLASS_SELECTOR@0..6 + 0: DOT@0..1 "." [] [] + 1: CSS_CUSTOM_IDENTIFIER@1..6 + 0: IDENT@1..6 "test" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@6..163 + 0: L_CURLY@6..7 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@7..161 + 0: CSS_NESTED_QUALIFIED_RULE@7..43 + 0: CSS_RELATIVE_SELECTOR_LIST@7..22 + 0: CSS_RELATIVE_SELECTOR@7..22 + 0: (empty) + 1: CSS_COMPOUND_SELECTOR@7..22 + 0: CSS_NESTED_SELECTOR_LIST@7..7 + 1: CSS_TYPE_SELECTOR@7..15 + 0: (empty) + 1: CSS_IDENTIFIER@7..15 + 0: IDENT@7..15 "label" [Newline("\n"), Whitespace(" ")] [] + 2: CSS_SUB_SELECTOR_LIST@15..22 + 0: CSS_PSEUDO_CLASS_SELECTOR@15..22 + 0: COLON@15..16 ":" [] [] + 1: CSS_PSEUDO_CLASS_IDENTIFIER@16..22 + 0: CSS_IDENTIFIER@16..22 + 0: IDENT@16..22 "hover" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@22..43 + 0: L_CURLY@22..23 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@23..39 + 0: CSS_DECLARATION_WITH_SEMICOLON@23..39 + 0: CSS_DECLARATION@23..38 + 0: CSS_GENERIC_PROPERTY@23..38 + 0: CSS_IDENTIFIER@23..33 + 0: IDENT@23..33 "color" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@33..35 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@35..38 + 0: CSS_IDENTIFIER@35..38 + 0: IDENT@35..38 "red" [] [] + 1: (empty) + 1: SEMICOLON@38..39 ";" [] [] + 2: R_CURLY@39..43 "}" [Newline("\n"), Whitespace(" ")] [] + 1: CSS_NESTED_QUALIFIED_RULE@43..79 + 0: CSS_RELATIVE_SELECTOR_LIST@43..57 + 0: CSS_RELATIVE_SELECTOR@43..57 + 0: (empty) + 1: CSS_COMPOUND_SELECTOR@43..57 + 0: CSS_NESTED_SELECTOR_LIST@43..43 + 1: CSS_TYPE_SELECTOR@43..51 + 0: (empty) + 1: CSS_IDENTIFIER@43..51 + 0: IDENT@43..51 "font" [Newline("\n"), Newline("\n"), Whitespace(" ")] [] + 2: CSS_SUB_SELECTOR_LIST@51..57 + 0: CSS_PSEUDO_CLASS_SELECTOR@51..57 + 0: COLON@51..52 ":" [] [] + 1: CSS_PSEUDO_CLASS_IDENTIFIER@52..57 + 0: CSS_IDENTIFIER@52..57 + 0: IDENT@52..57 "bold" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@57..79 + 0: L_CURLY@57..58 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@58..75 + 0: CSS_DECLARATION_WITH_SEMICOLON@58..75 + 0: CSS_DECLARATION@58..74 + 0: CSS_GENERIC_PROPERTY@58..74 + 0: CSS_IDENTIFIER@58..68 + 0: IDENT@58..68 "color" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@68..70 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@70..74 + 0: CSS_IDENTIFIER@70..74 + 0: IDENT@70..74 "blue" [] [] + 1: (empty) + 1: SEMICOLON@74..75 ";" [] [] + 2: R_CURLY@75..79 "}" [Newline("\n"), Whitespace(" ")] [] + 2: SCSS_NESTING_DECLARATION@79..122 + 0: CSS_IDENTIFIER@79..87 + 0: IDENT@79..87 "font" [Newline("\n"), Newline("\n"), Whitespace(" ")] [] + 1: COLON@87..88 ":" [] [] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@88..93 + 0: CSS_REGULAR_DIMENSION@88..93 + 0: CSS_NUMBER_LITERAL@88..90 "12" [] [] + 1: IDENT@90..93 "px" [] [Whitespace(" ")] + 3: CSS_DECLARATION_OR_RULE_BLOCK@93..122 + 0: L_CURLY@93..94 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@94..118 + 0: CSS_DECLARATION_WITH_SEMICOLON@94..118 + 0: CSS_DECLARATION@94..117 + 0: CSS_GENERIC_PROPERTY@94..117 + 0: CSS_IDENTIFIER@94..105 + 0: IDENT@94..105 "family" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@105..107 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@107..117 + 0: CSS_IDENTIFIER@107..117 + 0: IDENT@107..117 "sans-serif" [] [] + 1: (empty) + 1: SEMICOLON@117..118 ";" [] [] + 2: R_CURLY@118..122 "}" [Newline("\n"), Whitespace(" ")] [] + 3: SCSS_NESTING_DECLARATION@122..161 + 0: CSS_IDENTIFIER@122..130 + 0: IDENT@122..130 "font" [Newline("\n"), Newline("\n"), Whitespace(" ")] [] + 1: COLON@130..132 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@132..137 + 0: CSS_IDENTIFIER@132..137 + 0: IDENT@132..137 "bold" [] [Whitespace(" ")] + 3: CSS_DECLARATION_OR_RULE_BLOCK@137..161 + 0: L_CURLY@137..138 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@138..157 + 0: CSS_DECLARATION_WITH_SEMICOLON@138..157 + 0: CSS_DECLARATION@138..156 + 0: CSS_GENERIC_PROPERTY@138..156 + 0: CSS_IDENTIFIER@138..149 + 0: IDENT@138..149 "family" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@149..151 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@151..156 + 0: CSS_IDENTIFIER@151..156 + 0: IDENT@151..156 "serif" [] [] + 1: (empty) + 1: SEMICOLON@156..157 ";" [] [] + 2: R_CURLY@157..161 "}" [Newline("\n"), Whitespace(" ")] [] + 2: R_CURLY@161..163 "}" [Newline("\n")] [] + 2: EOF@163..164 "" [Newline("\n")] [] + +``` diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/nested-properties-empty-value.scss b/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/nested-properties-empty-value.scss new file mode 100644 index 000000000000..13e0f577659f --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/nested-properties-empty-value.scss @@ -0,0 +1,5 @@ +.font { + font: { + size: 12px; + } +} diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/nested-properties-empty-value.scss.snap b/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/nested-properties-empty-value.scss.snap new file mode 100644 index 000000000000..170d4fbb734d --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/nested-properties-empty-value.scss.snap @@ -0,0 +1,125 @@ +--- +source: crates/biome_css_parser/tests/spec_test.rs +assertion_line: 208 +expression: snapshot +--- +## Input + +```css +.font { + font: { + size: 12px; + } +} + +``` + + +## AST + +``` +CssRoot { + bom_token: missing (optional), + items: CssRootItemList [ + CssQualifiedRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@0..1 "." [] [], + name: CssCustomIdentifier { + value_token: IDENT@1..6 "font" [] [Whitespace(" ")], + }, + }, + ], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@6..7 "{" [] [], + items: CssDeclarationOrRuleList [ + ScssNestingDeclaration { + name: CssIdentifier { + value_token: IDENT@7..14 "font" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@14..16 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@16..17 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@17..26 "size" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@26..28 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssRegularDimension { + value_token: CSS_NUMBER_LITERAL@28..30 "12" [] [], + unit_token: IDENT@30..32 "px" [] [], + }, + ], + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@32..33 ";" [] [], + }, + ], + r_curly_token: R_CURLY@33..37 "}" [Newline("\n"), Whitespace(" ")] [], + }, + }, + ], + r_curly_token: R_CURLY@37..39 "}" [Newline("\n")] [], + }, + }, + ], + eof_token: EOF@39..40 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: CSS_ROOT@0..40 + 0: (empty) + 1: CSS_ROOT_ITEM_LIST@0..39 + 0: CSS_QUALIFIED_RULE@0..39 + 0: CSS_SELECTOR_LIST@0..6 + 0: CSS_COMPOUND_SELECTOR@0..6 + 0: CSS_NESTED_SELECTOR_LIST@0..0 + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@0..6 + 0: CSS_CLASS_SELECTOR@0..6 + 0: DOT@0..1 "." [] [] + 1: CSS_CUSTOM_IDENTIFIER@1..6 + 0: IDENT@1..6 "font" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@6..39 + 0: L_CURLY@6..7 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@7..37 + 0: SCSS_NESTING_DECLARATION@7..37 + 0: CSS_IDENTIFIER@7..14 + 0: IDENT@7..14 "font" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@14..16 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@16..16 + 3: CSS_DECLARATION_OR_RULE_BLOCK@16..37 + 0: L_CURLY@16..17 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@17..33 + 0: CSS_DECLARATION_WITH_SEMICOLON@17..33 + 0: CSS_DECLARATION@17..32 + 0: CSS_GENERIC_PROPERTY@17..32 + 0: CSS_IDENTIFIER@17..26 + 0: IDENT@17..26 "size" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@26..28 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@28..32 + 0: CSS_REGULAR_DIMENSION@28..32 + 0: CSS_NUMBER_LITERAL@28..30 "12" [] [] + 1: IDENT@30..32 "px" [] [] + 1: (empty) + 1: SEMICOLON@32..33 ";" [] [] + 2: R_CURLY@33..37 "}" [Newline("\n"), Whitespace(" ")] [] + 2: R_CURLY@37..39 "}" [Newline("\n")] [] + 2: EOF@39..40 "" [Newline("\n")] [] + +``` diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/nested-properties-with-value.scss b/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/nested-properties-with-value.scss new file mode 100644 index 000000000000..c19e45c903a2 --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/nested-properties-with-value.scss @@ -0,0 +1,5 @@ +.font { + font: 12px/1.2 { + family: sans-serif; + } +} diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/nested-properties-with-value.scss.snap b/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/nested-properties-with-value.scss.snap new file mode 100644 index 000000000000..e63ea3c41296 --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/nested-properties-with-value.scss.snap @@ -0,0 +1,141 @@ +--- +source: crates/biome_css_parser/tests/spec_test.rs +expression: snapshot +--- + +## Input + +```css +.font { + font: 12px/1.2 { + family: sans-serif; + } +} + +``` + + +## AST + +``` +CssRoot { + bom_token: missing (optional), + items: CssRootItemList [ + CssQualifiedRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@0..1 "." [] [], + name: CssCustomIdentifier { + value_token: IDENT@1..6 "font" [] [Whitespace(" ")], + }, + }, + ], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@6..7 "{" [] [], + items: CssDeclarationOrRuleList [ + ScssNestingDeclaration { + name: CssIdentifier { + value_token: IDENT@7..14 "font" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@14..16 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssRegularDimension { + value_token: CSS_NUMBER_LITERAL@16..18 "12" [] [], + unit_token: IDENT@18..20 "px" [] [], + }, + CssGenericDelimiter { + value: SLASH@20..21 "/" [] [], + }, + CssNumber { + value_token: CSS_NUMBER_LITERAL@21..25 "1.2" [] [Whitespace(" ")], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@25..26 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@26..37 "family" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@37..39 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@39..49 "sans-serif" [] [], + }, + ], + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@49..50 ";" [] [], + }, + ], + r_curly_token: R_CURLY@50..54 "}" [Newline("\n"), Whitespace(" ")] [], + }, + }, + ], + r_curly_token: R_CURLY@54..56 "}" [Newline("\n")] [], + }, + }, + ], + eof_token: EOF@56..57 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: CSS_ROOT@0..57 + 0: (empty) + 1: CSS_ROOT_ITEM_LIST@0..56 + 0: CSS_QUALIFIED_RULE@0..56 + 0: CSS_SELECTOR_LIST@0..6 + 0: CSS_COMPOUND_SELECTOR@0..6 + 0: CSS_NESTED_SELECTOR_LIST@0..0 + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@0..6 + 0: CSS_CLASS_SELECTOR@0..6 + 0: DOT@0..1 "." [] [] + 1: CSS_CUSTOM_IDENTIFIER@1..6 + 0: IDENT@1..6 "font" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@6..56 + 0: L_CURLY@6..7 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@7..54 + 0: SCSS_NESTING_DECLARATION@7..54 + 0: CSS_IDENTIFIER@7..14 + 0: IDENT@7..14 "font" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@14..16 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@16..25 + 0: CSS_REGULAR_DIMENSION@16..20 + 0: CSS_NUMBER_LITERAL@16..18 "12" [] [] + 1: IDENT@18..20 "px" [] [] + 1: CSS_GENERIC_DELIMITER@20..21 + 0: SLASH@20..21 "/" [] [] + 2: CSS_NUMBER@21..25 + 0: CSS_NUMBER_LITERAL@21..25 "1.2" [] [Whitespace(" ")] + 3: CSS_DECLARATION_OR_RULE_BLOCK@25..54 + 0: L_CURLY@25..26 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@26..50 + 0: CSS_DECLARATION_WITH_SEMICOLON@26..50 + 0: CSS_DECLARATION@26..49 + 0: CSS_GENERIC_PROPERTY@26..49 + 0: CSS_IDENTIFIER@26..37 + 0: IDENT@26..37 "family" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@37..39 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@39..49 + 0: CSS_IDENTIFIER@39..49 + 0: IDENT@39..49 "sans-serif" [] [] + 1: (empty) + 1: SEMICOLON@49..50 ";" [] [] + 2: R_CURLY@50..54 "}" [Newline("\n"), Whitespace(" ")] [] + 2: R_CURLY@54..56 "}" [Newline("\n")] [] + 2: EOF@56..57 "" [Newline("\n")] [] + +``` diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/spacing-after-colon.scss b/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/spacing-after-colon.scss new file mode 100644 index 000000000000..583817425764 --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/spacing-after-colon.scss @@ -0,0 +1,27 @@ +.test { + font:bold { + color: red; + } + + font: bold { + family: serif; + } + + font: + bold { + family: sans-serif; + } + + font:/*comment*/bold { + color: blue; + } + + font:/*comment*/ bold { + family: monospace; + } + + font:/*comment*/ + bold { + family: fantasy; + } +} diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/spacing-after-colon.scss.snap b/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/spacing-after-colon.scss.snap new file mode 100644 index 000000000000..2a1d0afeda86 --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/scss/declaration/spacing-after-colon.scss.snap @@ -0,0 +1,479 @@ +--- +source: crates/biome_css_parser/tests/spec_test.rs +expression: snapshot +--- + +## Input + +```css +.test { + font:bold { + color: red; + } + + font: bold { + family: serif; + } + + font: + bold { + family: sans-serif; + } + + font:/*comment*/bold { + color: blue; + } + + font:/*comment*/ bold { + family: monospace; + } + + font:/*comment*/ + bold { + family: fantasy; + } +} + +``` + + +## AST + +``` +CssRoot { + bom_token: missing (optional), + items: CssRootItemList [ + CssQualifiedRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@0..1 "." [] [], + name: CssCustomIdentifier { + value_token: IDENT@1..6 "test" [] [Whitespace(" ")], + }, + }, + ], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@6..7 "{" [] [], + items: CssDeclarationOrRuleList [ + CssNestedQualifiedRule { + prelude: CssRelativeSelectorList [ + CssRelativeSelector { + combinator: missing (optional), + selector: CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: CssTypeSelector { + namespace: missing (optional), + ident: CssIdentifier { + value_token: IDENT@7..14 "font" [Newline("\n"), Whitespace(" ")] [], + }, + }, + sub_selectors: CssSubSelectorList [ + CssPseudoClassSelector { + colon_token: COLON@14..15 ":" [] [], + class: CssPseudoClassIdentifier { + name: CssIdentifier { + value_token: IDENT@15..20 "bold" [] [Whitespace(" ")], + }, + }, + }, + ], + }, + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@20..21 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@21..31 "color" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@31..33 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@33..36 "red" [] [], + }, + ], + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@36..37 ";" [] [], + }, + ], + r_curly_token: R_CURLY@37..41 "}" [Newline("\n"), Whitespace(" ")] [], + }, + }, + ScssNestingDeclaration { + name: CssIdentifier { + value_token: IDENT@41..49 "font" [Newline("\n"), Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@49..51 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@51..56 "bold" [] [Whitespace(" ")], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@56..57 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@57..68 "family" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@68..70 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@70..75 "serif" [] [], + }, + ], + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@75..76 ";" [] [], + }, + ], + r_curly_token: R_CURLY@76..80 "}" [Newline("\n"), Whitespace(" ")] [], + }, + }, + ScssNestingDeclaration { + name: CssIdentifier { + value_token: IDENT@80..88 "font" [Newline("\n"), Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@88..89 ":" [] [], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@89..97 "bold" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@97..98 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@98..109 "family" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@109..111 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@111..121 "sans-serif" [] [], + }, + ], + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@121..122 ";" [] [], + }, + ], + r_curly_token: R_CURLY@122..126 "}" [Newline("\n"), Whitespace(" ")] [], + }, + }, + CssNestedQualifiedRule { + prelude: CssRelativeSelectorList [ + CssRelativeSelector { + combinator: missing (optional), + selector: CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: CssTypeSelector { + namespace: missing (optional), + ident: CssIdentifier { + value_token: IDENT@126..134 "font" [Newline("\n"), Newline("\n"), Whitespace(" ")] [], + }, + }, + sub_selectors: CssSubSelectorList [ + CssPseudoClassSelector { + colon_token: COLON@134..146 ":" [] [Comments("/*comment*/")], + class: CssPseudoClassIdentifier { + name: CssIdentifier { + value_token: IDENT@146..151 "bold" [] [Whitespace(" ")], + }, + }, + }, + ], + }, + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@151..152 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@152..162 "color" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@162..164 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@164..168 "blue" [] [], + }, + ], + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@168..169 ";" [] [], + }, + ], + r_curly_token: R_CURLY@169..173 "}" [Newline("\n"), Whitespace(" ")] [], + }, + }, + ScssNestingDeclaration { + name: CssIdentifier { + value_token: IDENT@173..181 "font" [Newline("\n"), Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@181..194 ":" [] [Comments("/*comment*/"), Whitespace(" ")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@194..199 "bold" [] [Whitespace(" ")], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@199..200 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@200..211 "family" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@211..213 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@213..222 "monospace" [] [], + }, + ], + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@222..223 ";" [] [], + }, + ], + r_curly_token: R_CURLY@223..227 "}" [Newline("\n"), Whitespace(" ")] [], + }, + }, + ScssNestingDeclaration { + name: CssIdentifier { + value_token: IDENT@227..235 "font" [Newline("\n"), Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@235..247 ":" [] [Comments("/*comment*/")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@247..255 "bold" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@255..256 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@256..267 "family" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@267..269 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@269..276 "fantasy" [] [], + }, + ], + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@276..277 ";" [] [], + }, + ], + r_curly_token: R_CURLY@277..281 "}" [Newline("\n"), Whitespace(" ")] [], + }, + }, + ], + r_curly_token: R_CURLY@281..283 "}" [Newline("\n")] [], + }, + }, + ], + eof_token: EOF@283..284 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: CSS_ROOT@0..284 + 0: (empty) + 1: CSS_ROOT_ITEM_LIST@0..283 + 0: CSS_QUALIFIED_RULE@0..283 + 0: CSS_SELECTOR_LIST@0..6 + 0: CSS_COMPOUND_SELECTOR@0..6 + 0: CSS_NESTED_SELECTOR_LIST@0..0 + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@0..6 + 0: CSS_CLASS_SELECTOR@0..6 + 0: DOT@0..1 "." [] [] + 1: CSS_CUSTOM_IDENTIFIER@1..6 + 0: IDENT@1..6 "test" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@6..283 + 0: L_CURLY@6..7 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@7..281 + 0: CSS_NESTED_QUALIFIED_RULE@7..41 + 0: CSS_RELATIVE_SELECTOR_LIST@7..20 + 0: CSS_RELATIVE_SELECTOR@7..20 + 0: (empty) + 1: CSS_COMPOUND_SELECTOR@7..20 + 0: CSS_NESTED_SELECTOR_LIST@7..7 + 1: CSS_TYPE_SELECTOR@7..14 + 0: (empty) + 1: CSS_IDENTIFIER@7..14 + 0: IDENT@7..14 "font" [Newline("\n"), Whitespace(" ")] [] + 2: CSS_SUB_SELECTOR_LIST@14..20 + 0: CSS_PSEUDO_CLASS_SELECTOR@14..20 + 0: COLON@14..15 ":" [] [] + 1: CSS_PSEUDO_CLASS_IDENTIFIER@15..20 + 0: CSS_IDENTIFIER@15..20 + 0: IDENT@15..20 "bold" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@20..41 + 0: L_CURLY@20..21 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@21..37 + 0: CSS_DECLARATION_WITH_SEMICOLON@21..37 + 0: CSS_DECLARATION@21..36 + 0: CSS_GENERIC_PROPERTY@21..36 + 0: CSS_IDENTIFIER@21..31 + 0: IDENT@21..31 "color" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@31..33 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@33..36 + 0: CSS_IDENTIFIER@33..36 + 0: IDENT@33..36 "red" [] [] + 1: (empty) + 1: SEMICOLON@36..37 ";" [] [] + 2: R_CURLY@37..41 "}" [Newline("\n"), Whitespace(" ")] [] + 1: SCSS_NESTING_DECLARATION@41..80 + 0: CSS_IDENTIFIER@41..49 + 0: IDENT@41..49 "font" [Newline("\n"), Newline("\n"), Whitespace(" ")] [] + 1: COLON@49..51 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@51..56 + 0: CSS_IDENTIFIER@51..56 + 0: IDENT@51..56 "bold" [] [Whitespace(" ")] + 3: CSS_DECLARATION_OR_RULE_BLOCK@56..80 + 0: L_CURLY@56..57 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@57..76 + 0: CSS_DECLARATION_WITH_SEMICOLON@57..76 + 0: CSS_DECLARATION@57..75 + 0: CSS_GENERIC_PROPERTY@57..75 + 0: CSS_IDENTIFIER@57..68 + 0: IDENT@57..68 "family" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@68..70 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@70..75 + 0: CSS_IDENTIFIER@70..75 + 0: IDENT@70..75 "serif" [] [] + 1: (empty) + 1: SEMICOLON@75..76 ";" [] [] + 2: R_CURLY@76..80 "}" [Newline("\n"), Whitespace(" ")] [] + 2: SCSS_NESTING_DECLARATION@80..126 + 0: CSS_IDENTIFIER@80..88 + 0: IDENT@80..88 "font" [Newline("\n"), Newline("\n"), Whitespace(" ")] [] + 1: COLON@88..89 ":" [] [] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@89..97 + 0: CSS_IDENTIFIER@89..97 + 0: IDENT@89..97 "bold" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] + 3: CSS_DECLARATION_OR_RULE_BLOCK@97..126 + 0: L_CURLY@97..98 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@98..122 + 0: CSS_DECLARATION_WITH_SEMICOLON@98..122 + 0: CSS_DECLARATION@98..121 + 0: CSS_GENERIC_PROPERTY@98..121 + 0: CSS_IDENTIFIER@98..109 + 0: IDENT@98..109 "family" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@109..111 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@111..121 + 0: CSS_IDENTIFIER@111..121 + 0: IDENT@111..121 "sans-serif" [] [] + 1: (empty) + 1: SEMICOLON@121..122 ";" [] [] + 2: R_CURLY@122..126 "}" [Newline("\n"), Whitespace(" ")] [] + 3: CSS_NESTED_QUALIFIED_RULE@126..173 + 0: CSS_RELATIVE_SELECTOR_LIST@126..151 + 0: CSS_RELATIVE_SELECTOR@126..151 + 0: (empty) + 1: CSS_COMPOUND_SELECTOR@126..151 + 0: CSS_NESTED_SELECTOR_LIST@126..126 + 1: CSS_TYPE_SELECTOR@126..134 + 0: (empty) + 1: CSS_IDENTIFIER@126..134 + 0: IDENT@126..134 "font" [Newline("\n"), Newline("\n"), Whitespace(" ")] [] + 2: CSS_SUB_SELECTOR_LIST@134..151 + 0: CSS_PSEUDO_CLASS_SELECTOR@134..151 + 0: COLON@134..146 ":" [] [Comments("/*comment*/")] + 1: CSS_PSEUDO_CLASS_IDENTIFIER@146..151 + 0: CSS_IDENTIFIER@146..151 + 0: IDENT@146..151 "bold" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@151..173 + 0: L_CURLY@151..152 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@152..169 + 0: CSS_DECLARATION_WITH_SEMICOLON@152..169 + 0: CSS_DECLARATION@152..168 + 0: CSS_GENERIC_PROPERTY@152..168 + 0: CSS_IDENTIFIER@152..162 + 0: IDENT@152..162 "color" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@162..164 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@164..168 + 0: CSS_IDENTIFIER@164..168 + 0: IDENT@164..168 "blue" [] [] + 1: (empty) + 1: SEMICOLON@168..169 ";" [] [] + 2: R_CURLY@169..173 "}" [Newline("\n"), Whitespace(" ")] [] + 4: SCSS_NESTING_DECLARATION@173..227 + 0: CSS_IDENTIFIER@173..181 + 0: IDENT@173..181 "font" [Newline("\n"), Newline("\n"), Whitespace(" ")] [] + 1: COLON@181..194 ":" [] [Comments("/*comment*/"), Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@194..199 + 0: CSS_IDENTIFIER@194..199 + 0: IDENT@194..199 "bold" [] [Whitespace(" ")] + 3: CSS_DECLARATION_OR_RULE_BLOCK@199..227 + 0: L_CURLY@199..200 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@200..223 + 0: CSS_DECLARATION_WITH_SEMICOLON@200..223 + 0: CSS_DECLARATION@200..222 + 0: CSS_GENERIC_PROPERTY@200..222 + 0: CSS_IDENTIFIER@200..211 + 0: IDENT@200..211 "family" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@211..213 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@213..222 + 0: CSS_IDENTIFIER@213..222 + 0: IDENT@213..222 "monospace" [] [] + 1: (empty) + 1: SEMICOLON@222..223 ";" [] [] + 2: R_CURLY@223..227 "}" [Newline("\n"), Whitespace(" ")] [] + 5: SCSS_NESTING_DECLARATION@227..281 + 0: CSS_IDENTIFIER@227..235 + 0: IDENT@227..235 "font" [Newline("\n"), Newline("\n"), Whitespace(" ")] [] + 1: COLON@235..247 ":" [] [Comments("/*comment*/")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@247..255 + 0: CSS_IDENTIFIER@247..255 + 0: IDENT@247..255 "bold" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] + 3: CSS_DECLARATION_OR_RULE_BLOCK@255..281 + 0: L_CURLY@255..256 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@256..277 + 0: CSS_DECLARATION_WITH_SEMICOLON@256..277 + 0: CSS_DECLARATION@256..276 + 0: CSS_GENERIC_PROPERTY@256..276 + 0: CSS_IDENTIFIER@256..267 + 0: IDENT@256..267 "family" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@267..269 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@269..276 + 0: CSS_IDENTIFIER@269..276 + 0: IDENT@269..276 "fantasy" [] [] + 1: (empty) + 1: SEMICOLON@276..277 ";" [] [] + 2: R_CURLY@277..281 "}" [Newline("\n"), Whitespace(" ")] [] + 2: R_CURLY@281..283 "}" [Newline("\n")] [] + 2: EOF@283..284 "" [Newline("\n")] [] + +``` diff --git a/crates/biome_css_parser/tests/quick_test.rs b/crates/biome_css_parser/tests/quick_test.rs index 1ea08167ce73..bb1eb467ac69 100644 --- a/crates/biome_css_parser/tests/quick_test.rs +++ b/crates/biome_css_parser/tests/quick_test.rs @@ -6,18 +6,16 @@ use biome_test_utils::has_bogus_nodes_or_empty_slots; #[test] pub fn quick_test() { let code = r#" -@utility border-overlay-* { - position: relative; - - &::after { - border-width: 1px; + .card { + label:hover { + color: red; } -} + } "#; let root = parse_css( code, - CssFileSource::css(), + CssFileSource::scss(), CssParserOptions::default() .allow_wrong_line_comments() .allow_css_modules() diff --git a/crates/biome_css_syntax/src/generated/kind.rs b/crates/biome_css_syntax/src/generated/kind.rs index 7539c0e2eb70..933bb9395d08 100644 --- a/crates/biome_css_syntax/src/generated/kind.rs +++ b/crates/biome_css_syntax/src/generated/kind.rs @@ -536,6 +536,7 @@ pub enum CssSyntaxKind { CSS_FUNCTION_PARAMETER_LIST, CSS_RETURNS_STATEMENT, SCSS_DECLARATION, + SCSS_NESTING_DECLARATION, SCSS_NAMESPACED_IDENTIFIER, SCSS_QUALIFIED_NAME, SCSS_VARIABLE_MODIFIER_LIST, diff --git a/crates/biome_css_syntax/src/generated/macros.rs b/crates/biome_css_syntax/src/generated/macros.rs index 55d465f935c4..8c43a5afdab3 100644 --- a/crates/biome_css_syntax/src/generated/macros.rs +++ b/crates/biome_css_syntax/src/generated/macros.rs @@ -925,6 +925,10 @@ macro_rules! map_syntax_node { let $pattern = unsafe { $crate::ScssNamespacedIdentifier::new_unchecked(node) }; $body } + $crate::CssSyntaxKind::SCSS_NESTING_DECLARATION => { + let $pattern = unsafe { $crate::ScssNestingDeclaration::new_unchecked(node) }; + $body + } $crate::CssSyntaxKind::SCSS_QUALIFIED_NAME => { let $pattern = unsafe { $crate::ScssQualifiedName::new_unchecked(node) }; $body diff --git a/crates/biome_css_syntax/src/generated/nodes.rs b/crates/biome_css_syntax/src/generated/nodes.rs index 915cff392c0c..fce8ae51d523 100644 --- a/crates/biome_css_syntax/src/generated/nodes.rs +++ b/crates/biome_css_syntax/src/generated/nodes.rs @@ -8893,6 +8893,56 @@ pub struct ScssNamespacedIdentifierFields { pub name: SyntaxResult, } #[derive(Clone, PartialEq, Eq, Hash)] +pub struct ScssNestingDeclaration { + pub(crate) syntax: SyntaxNode, +} +impl ScssNestingDeclaration { + #[doc = r" Create an AstNode from a SyntaxNode without checking its kind"] + #[doc = r""] + #[doc = r" # Safety"] + #[doc = r" This function must be guarded with a call to [AstNode::can_cast]"] + #[doc = r" or a match on [SyntaxNode::kind]"] + #[inline] + pub const unsafe fn new_unchecked(syntax: SyntaxNode) -> Self { + Self { syntax } + } + pub fn as_fields(&self) -> ScssNestingDeclarationFields { + ScssNestingDeclarationFields { + name: self.name(), + colon_token: self.colon_token(), + value: self.value(), + block: self.block(), + } + } + pub fn name(&self) -> SyntaxResult { + support::required_node(&self.syntax, 0usize) + } + pub fn colon_token(&self) -> SyntaxResult { + support::required_token(&self.syntax, 1usize) + } + pub fn value(&self) -> CssGenericComponentValueList { + support::list(&self.syntax, 2usize) + } + pub fn block(&self) -> SyntaxResult { + support::required_node(&self.syntax, 3usize) + } +} +impl Serialize for ScssNestingDeclaration { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.as_fields().serialize(serializer) + } +} +#[derive(Serialize)] +pub struct ScssNestingDeclarationFields { + pub name: SyntaxResult, + pub colon_token: SyntaxResult, + pub value: CssGenericComponentValueList, + pub block: SyntaxResult, +} +#[derive(Clone, PartialEq, Eq, Hash)] pub struct ScssQualifiedName { pub(crate) syntax: SyntaxNode, } @@ -10356,6 +10406,7 @@ pub enum AnyCssDeclaration { CssDeclarationWithSemicolon(CssDeclarationWithSemicolon), CssEmptyDeclaration(CssEmptyDeclaration), ScssDeclaration(ScssDeclaration), + ScssNestingDeclaration(ScssNestingDeclaration), } impl AnyCssDeclaration { pub fn as_css_declaration_with_semicolon(&self) -> Option<&CssDeclarationWithSemicolon> { @@ -10376,6 +10427,12 @@ impl AnyCssDeclaration { _ => None, } } + pub fn as_scss_nesting_declaration(&self) -> Option<&ScssNestingDeclaration> { + match &self { + Self::ScssNestingDeclaration(item) => Some(item), + _ => None, + } + } } #[derive(Clone, PartialEq, Eq, Hash, Serialize)] pub enum AnyCssDeclarationBlock { @@ -10428,6 +10485,7 @@ pub enum AnyCssDeclarationOrAtRule { CssDeclarationWithSemicolon(CssDeclarationWithSemicolon), CssEmptyDeclaration(CssEmptyDeclaration), ScssDeclaration(ScssDeclaration), + ScssNestingDeclaration(ScssNestingDeclaration), } impl AnyCssDeclarationOrAtRule { pub fn as_css_at_rule(&self) -> Option<&CssAtRule> { @@ -10454,6 +10512,12 @@ impl AnyCssDeclarationOrAtRule { _ => None, } } + pub fn as_scss_nesting_declaration(&self) -> Option<&ScssNestingDeclaration> { + match &self { + Self::ScssNestingDeclaration(item) => Some(item), + _ => None, + } + } } #[derive(Clone, PartialEq, Eq, Hash, Serialize)] pub enum AnyCssDeclarationOrAtRuleBlock { @@ -10482,6 +10546,7 @@ pub enum AnyCssDeclarationOrRule { CssEmptyDeclaration(CssEmptyDeclaration), CssMetavariable(CssMetavariable), ScssDeclaration(ScssDeclaration), + ScssNestingDeclaration(ScssNestingDeclaration), } impl AnyCssDeclarationOrRule { pub fn as_any_css_rule(&self) -> Option<&AnyCssRule> { @@ -10520,6 +10585,12 @@ impl AnyCssDeclarationOrRule { _ => None, } } + pub fn as_scss_nesting_declaration(&self) -> Option<&ScssNestingDeclaration> { + match &self { + Self::ScssNestingDeclaration(item) => Some(item), + _ => None, + } + } } #[derive(Clone, PartialEq, Eq, Hash, Serialize)] pub enum AnyCssDeclarationOrRuleBlock { @@ -23487,6 +23558,59 @@ impl From for SyntaxElement { n.syntax.into() } } +impl AstNode for ScssNestingDeclaration { + type Language = Language; + const KIND_SET: SyntaxKindSet = + SyntaxKindSet::from_raw(RawSyntaxKind(SCSS_NESTING_DECLARATION as u16)); + fn can_cast(kind: SyntaxKind) -> bool { + kind == SCSS_NESTING_DECLARATION + } + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } + fn into_syntax(self) -> SyntaxNode { + self.syntax + } +} +impl std::fmt::Debug for ScssNestingDeclaration { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + thread_local! { static DEPTH : std :: cell :: Cell < u8 > = const { std :: cell :: Cell :: new (0) } }; + let current_depth = DEPTH.get(); + let result = if current_depth < 16 { + DEPTH.set(current_depth + 1); + f.debug_struct("ScssNestingDeclaration") + .field("name", &support::DebugSyntaxResult(self.name())) + .field( + "colon_token", + &support::DebugSyntaxResult(self.colon_token()), + ) + .field("value", &self.value()) + .field("block", &support::DebugSyntaxResult(self.block())) + .finish() + } else { + f.debug_struct("ScssNestingDeclaration").finish() + }; + DEPTH.set(current_depth); + result + } +} +impl From for SyntaxNode { + fn from(n: ScssNestingDeclaration) -> Self { + n.syntax + } +} +impl From for SyntaxElement { + fn from(n: ScssNestingDeclaration) -> Self { + n.syntax.into() + } +} impl AstNode for ScssQualifiedName { type Language = Language; const KIND_SET: SyntaxKindSet = @@ -26206,15 +26330,24 @@ impl From for AnyCssDeclaration { Self::ScssDeclaration(node) } } +impl From for AnyCssDeclaration { + fn from(node: ScssNestingDeclaration) -> Self { + Self::ScssNestingDeclaration(node) + } +} impl AstNode for AnyCssDeclaration { type Language = Language; const KIND_SET: SyntaxKindSet = CssDeclarationWithSemicolon::KIND_SET .union(CssEmptyDeclaration::KIND_SET) - .union(ScssDeclaration::KIND_SET); + .union(ScssDeclaration::KIND_SET) + .union(ScssNestingDeclaration::KIND_SET); fn can_cast(kind: SyntaxKind) -> bool { matches!( kind, - CSS_DECLARATION_WITH_SEMICOLON | CSS_EMPTY_DECLARATION | SCSS_DECLARATION + CSS_DECLARATION_WITH_SEMICOLON + | CSS_EMPTY_DECLARATION + | SCSS_DECLARATION + | SCSS_NESTING_DECLARATION ) } fn cast(syntax: SyntaxNode) -> Option { @@ -26224,6 +26357,9 @@ impl AstNode for AnyCssDeclaration { } CSS_EMPTY_DECLARATION => Self::CssEmptyDeclaration(CssEmptyDeclaration { syntax }), SCSS_DECLARATION => Self::ScssDeclaration(ScssDeclaration { syntax }), + SCSS_NESTING_DECLARATION => { + Self::ScssNestingDeclaration(ScssNestingDeclaration { syntax }) + } _ => return None, }; Some(res) @@ -26233,6 +26369,7 @@ impl AstNode for AnyCssDeclaration { Self::CssDeclarationWithSemicolon(it) => it.syntax(), Self::CssEmptyDeclaration(it) => it.syntax(), Self::ScssDeclaration(it) => it.syntax(), + Self::ScssNestingDeclaration(it) => it.syntax(), } } fn into_syntax(self) -> SyntaxNode { @@ -26240,6 +26377,7 @@ impl AstNode for AnyCssDeclaration { Self::CssDeclarationWithSemicolon(it) => it.into_syntax(), Self::CssEmptyDeclaration(it) => it.into_syntax(), Self::ScssDeclaration(it) => it.into_syntax(), + Self::ScssNestingDeclaration(it) => it.into_syntax(), } } } @@ -26249,6 +26387,7 @@ impl std::fmt::Debug for AnyCssDeclaration { Self::CssDeclarationWithSemicolon(it) => std::fmt::Debug::fmt(it, f), Self::CssEmptyDeclaration(it) => std::fmt::Debug::fmt(it, f), Self::ScssDeclaration(it) => std::fmt::Debug::fmt(it, f), + Self::ScssNestingDeclaration(it) => std::fmt::Debug::fmt(it, f), } } } @@ -26258,6 +26397,7 @@ impl From for SyntaxNode { AnyCssDeclaration::CssDeclarationWithSemicolon(it) => it.into_syntax(), AnyCssDeclaration::CssEmptyDeclaration(it) => it.into_syntax(), AnyCssDeclaration::ScssDeclaration(it) => it.into_syntax(), + AnyCssDeclaration::ScssNestingDeclaration(it) => it.into_syntax(), } } } @@ -26423,16 +26563,26 @@ impl From for AnyCssDeclarationOrAtRule { Self::ScssDeclaration(node) } } +impl From for AnyCssDeclarationOrAtRule { + fn from(node: ScssNestingDeclaration) -> Self { + Self::ScssNestingDeclaration(node) + } +} impl AstNode for AnyCssDeclarationOrAtRule { type Language = Language; const KIND_SET: SyntaxKindSet = CssAtRule::KIND_SET .union(CssDeclarationWithSemicolon::KIND_SET) .union(CssEmptyDeclaration::KIND_SET) - .union(ScssDeclaration::KIND_SET); + .union(ScssDeclaration::KIND_SET) + .union(ScssNestingDeclaration::KIND_SET); fn can_cast(kind: SyntaxKind) -> bool { matches!( kind, - CSS_AT_RULE | CSS_DECLARATION_WITH_SEMICOLON | CSS_EMPTY_DECLARATION | SCSS_DECLARATION + CSS_AT_RULE + | CSS_DECLARATION_WITH_SEMICOLON + | CSS_EMPTY_DECLARATION + | SCSS_DECLARATION + | SCSS_NESTING_DECLARATION ) } fn cast(syntax: SyntaxNode) -> Option { @@ -26443,6 +26593,9 @@ impl AstNode for AnyCssDeclarationOrAtRule { } CSS_EMPTY_DECLARATION => Self::CssEmptyDeclaration(CssEmptyDeclaration { syntax }), SCSS_DECLARATION => Self::ScssDeclaration(ScssDeclaration { syntax }), + SCSS_NESTING_DECLARATION => { + Self::ScssNestingDeclaration(ScssNestingDeclaration { syntax }) + } _ => return None, }; Some(res) @@ -26453,6 +26606,7 @@ impl AstNode for AnyCssDeclarationOrAtRule { Self::CssDeclarationWithSemicolon(it) => it.syntax(), Self::CssEmptyDeclaration(it) => it.syntax(), Self::ScssDeclaration(it) => it.syntax(), + Self::ScssNestingDeclaration(it) => it.syntax(), } } fn into_syntax(self) -> SyntaxNode { @@ -26461,6 +26615,7 @@ impl AstNode for AnyCssDeclarationOrAtRule { Self::CssDeclarationWithSemicolon(it) => it.into_syntax(), Self::CssEmptyDeclaration(it) => it.into_syntax(), Self::ScssDeclaration(it) => it.into_syntax(), + Self::ScssNestingDeclaration(it) => it.into_syntax(), } } } @@ -26471,6 +26626,7 @@ impl std::fmt::Debug for AnyCssDeclarationOrAtRule { Self::CssDeclarationWithSemicolon(it) => std::fmt::Debug::fmt(it, f), Self::CssEmptyDeclaration(it) => std::fmt::Debug::fmt(it, f), Self::ScssDeclaration(it) => std::fmt::Debug::fmt(it, f), + Self::ScssNestingDeclaration(it) => std::fmt::Debug::fmt(it, f), } } } @@ -26481,6 +26637,7 @@ impl From for SyntaxNode { AnyCssDeclarationOrAtRule::CssDeclarationWithSemicolon(it) => it.into_syntax(), AnyCssDeclarationOrAtRule::CssEmptyDeclaration(it) => it.into_syntax(), AnyCssDeclarationOrAtRule::ScssDeclaration(it) => it.into_syntax(), + AnyCssDeclarationOrAtRule::ScssNestingDeclaration(it) => it.into_syntax(), } } } @@ -26577,6 +26734,11 @@ impl From for AnyCssDeclarationOrRule { Self::ScssDeclaration(node) } } +impl From for AnyCssDeclarationOrRule { + fn from(node: ScssNestingDeclaration) -> Self { + Self::ScssNestingDeclaration(node) + } +} impl AstNode for AnyCssDeclarationOrRule { type Language = Language; const KIND_SET: SyntaxKindSet = AnyCssRule::KIND_SET @@ -26584,14 +26746,16 @@ impl AstNode for AnyCssDeclarationOrRule { .union(CssDeclarationWithSemicolon::KIND_SET) .union(CssEmptyDeclaration::KIND_SET) .union(CssMetavariable::KIND_SET) - .union(ScssDeclaration::KIND_SET); + .union(ScssDeclaration::KIND_SET) + .union(ScssNestingDeclaration::KIND_SET); fn can_cast(kind: SyntaxKind) -> bool { match kind { CSS_BOGUS | CSS_DECLARATION_WITH_SEMICOLON | CSS_EMPTY_DECLARATION | CSS_METAVARIABLE - | SCSS_DECLARATION => true, + | SCSS_DECLARATION + | SCSS_NESTING_DECLARATION => true, k if AnyCssRule::can_cast(k) => true, _ => false, } @@ -26605,6 +26769,9 @@ impl AstNode for AnyCssDeclarationOrRule { CSS_EMPTY_DECLARATION => Self::CssEmptyDeclaration(CssEmptyDeclaration { syntax }), CSS_METAVARIABLE => Self::CssMetavariable(CssMetavariable { syntax }), SCSS_DECLARATION => Self::ScssDeclaration(ScssDeclaration { syntax }), + SCSS_NESTING_DECLARATION => { + Self::ScssNestingDeclaration(ScssNestingDeclaration { syntax }) + } _ => { if let Some(any_css_rule) = AnyCssRule::cast(syntax) { return Some(Self::AnyCssRule(any_css_rule)); @@ -26621,6 +26788,7 @@ impl AstNode for AnyCssDeclarationOrRule { Self::CssEmptyDeclaration(it) => it.syntax(), Self::CssMetavariable(it) => it.syntax(), Self::ScssDeclaration(it) => it.syntax(), + Self::ScssNestingDeclaration(it) => it.syntax(), Self::AnyCssRule(it) => it.syntax(), } } @@ -26631,6 +26799,7 @@ impl AstNode for AnyCssDeclarationOrRule { Self::CssEmptyDeclaration(it) => it.into_syntax(), Self::CssMetavariable(it) => it.into_syntax(), Self::ScssDeclaration(it) => it.into_syntax(), + Self::ScssNestingDeclaration(it) => it.into_syntax(), Self::AnyCssRule(it) => it.into_syntax(), } } @@ -26644,6 +26813,7 @@ impl std::fmt::Debug for AnyCssDeclarationOrRule { Self::CssEmptyDeclaration(it) => std::fmt::Debug::fmt(it, f), Self::CssMetavariable(it) => std::fmt::Debug::fmt(it, f), Self::ScssDeclaration(it) => std::fmt::Debug::fmt(it, f), + Self::ScssNestingDeclaration(it) => std::fmt::Debug::fmt(it, f), } } } @@ -26656,6 +26826,7 @@ impl From for SyntaxNode { AnyCssDeclarationOrRule::CssEmptyDeclaration(it) => it.into_syntax(), AnyCssDeclarationOrRule::CssMetavariable(it) => it.into_syntax(), AnyCssDeclarationOrRule::ScssDeclaration(it) => it.into_syntax(), + AnyCssDeclarationOrRule::ScssNestingDeclaration(it) => it.into_syntax(), } } } @@ -34550,6 +34721,11 @@ impl std::fmt::Display for ScssNamespacedIdentifier { std::fmt::Display::fmt(self.syntax(), f) } } +impl std::fmt::Display for ScssNestingDeclaration { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} impl std::fmt::Display for ScssQualifiedName { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(self.syntax(), f) diff --git a/crates/biome_css_syntax/src/generated/nodes_mut.rs b/crates/biome_css_syntax/src/generated/nodes_mut.rs index ec21e00ae6d8..8113578a7d48 100644 --- a/crates/biome_css_syntax/src/generated/nodes_mut.rs +++ b/crates/biome_css_syntax/src/generated/nodes_mut.rs @@ -3525,6 +3525,32 @@ impl ScssNamespacedIdentifier { ) } } +impl ScssNestingDeclaration { + pub fn with_name(self, element: CssIdentifier) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(0usize..=0usize, once(Some(element.into_syntax().into()))), + ) + } + pub fn with_colon_token(self, element: SyntaxToken) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(1usize..=1usize, once(Some(element.into()))), + ) + } + pub fn with_value(self, element: CssGenericComponentValueList) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(2usize..=2usize, once(Some(element.into_syntax().into()))), + ) + } + pub fn with_block(self, element: AnyCssDeclarationOrRuleBlock) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(3usize..=3usize, once(Some(element.into_syntax().into()))), + ) + } +} impl ScssQualifiedName { pub fn with_module(self, element: CssIdentifier) -> Self { Self::unwrap_cast( diff --git a/crates/biome_graphql_parser/src/lexer/mod.rs b/crates/biome_graphql_parser/src/lexer/mod.rs index 7df9eba7b60c..7537ac2bfc28 100644 --- a/crates/biome_graphql_parser/src/lexer/mod.rs +++ b/crates/biome_graphql_parser/src/lexer/mod.rs @@ -128,6 +128,7 @@ impl<'src> LexerWithCheckpoint<'src> for GraphqlLexer<'src> { current_flags: self.current_flags, current_kind: self.current_kind, after_line_break: self.has_preceding_line_break(), + after_whitespace: false, unicode_bom_length: self.unicode_bom_length, diagnostics_pos: self.diagnostics.len() as u32, } diff --git a/crates/biome_grit_patterns/src/grit_target_language/css_target_language/generated_mappings.rs b/crates/biome_grit_patterns/src/grit_target_language/css_target_language/generated_mappings.rs index ec48e7c3e273..20ce3fb1bb3f 100644 --- a/crates/biome_grit_patterns/src/grit_target_language/css_target_language/generated_mappings.rs +++ b/crates/biome_grit_patterns/src/grit_target_language/css_target_language/generated_mappings.rs @@ -277,6 +277,7 @@ pub fn kind_by_name(node_name: &str) -> Option { "ScssDeclaration" => lang::ScssDeclaration::KIND_SET.iter().next(), "ScssIdentifier" => lang::ScssIdentifier::KIND_SET.iter().next(), "ScssNamespacedIdentifier" => lang::ScssNamespacedIdentifier::KIND_SET.iter().next(), + "ScssNestingDeclaration" => lang::ScssNestingDeclaration::KIND_SET.iter().next(), "ScssQualifiedName" => lang::ScssQualifiedName::KIND_SET.iter().next(), "ScssVariableModifier" => lang::ScssVariableModifier::KIND_SET.iter().next(), "TwApplyAtRule" => lang::TwApplyAtRule::KIND_SET.iter().next(), diff --git a/crates/biome_html_parser/src/lexer/mod.rs b/crates/biome_html_parser/src/lexer/mod.rs index 60c409f80498..0ce0f65c003e 100644 --- a/crates/biome_html_parser/src/lexer/mod.rs +++ b/crates/biome_html_parser/src/lexer/mod.rs @@ -1235,6 +1235,7 @@ impl<'src> Lexer<'src> for HtmlLexer<'src> { current_flags, current_kind, after_line_break, + after_whitespace: _, unicode_bom_length, diagnostics_pos, } = checkpoint; @@ -1360,6 +1361,7 @@ impl<'src> LexerWithCheckpoint<'src> for HtmlLexer<'src> { current_flags: self.current_flags, current_kind: self.current_kind, after_line_break: self.after_newline, + after_whitespace: false, unicode_bom_length: self.unicode_bom_length, diagnostics_pos: self.diagnostics.len() as u32, } diff --git a/crates/biome_js_parser/src/lexer/mod.rs b/crates/biome_js_parser/src/lexer/mod.rs index 40ea38cae8b0..33019216a753 100644 --- a/crates/biome_js_parser/src/lexer/mod.rs +++ b/crates/biome_js_parser/src/lexer/mod.rs @@ -209,6 +209,7 @@ impl<'src> Lexer<'src> for JsLexer<'src> { current_flags, current_kind, after_line_break, + after_whitespace: _, unicode_bom_length, diagnostics_pos, } = checkpoint; @@ -292,6 +293,7 @@ impl<'src> LexerWithCheckpoint<'src> for JsLexer<'src> { current_flags: self.current_flags, current_kind: self.current_kind, after_line_break: self.after_newline, + after_whitespace: false, unicode_bom_length: self.unicode_bom_length, diagnostics_pos: self.diagnostics.len() as u32, } diff --git a/crates/biome_markdown_parser/src/lexer/mod.rs b/crates/biome_markdown_parser/src/lexer/mod.rs index 0f21aa144a5a..05d9b2b6c3e0 100644 --- a/crates/biome_markdown_parser/src/lexer/mod.rs +++ b/crates/biome_markdown_parser/src/lexer/mod.rs @@ -177,6 +177,7 @@ impl<'src> Lexer<'src> for MarkdownLexer<'src> { current_flags, current_kind, after_line_break, + after_whitespace: _, unicode_bom_length, diagnostics_pos, } = checkpoint; @@ -1268,6 +1269,7 @@ impl<'src> LexerWithCheckpoint<'src> for MarkdownLexer<'src> { current_flags: self.current_flags, current_kind: self.current_kind, after_line_break: self.after_newline, + after_whitespace: false, unicode_bom_length: self.unicode_bom_length, diagnostics_pos: self.diagnostics.len() as u32, } diff --git a/crates/biome_parser/src/lexer.rs b/crates/biome_parser/src/lexer.rs index 0e4ed39f6368..c6c37df10b07 100644 --- a/crates/biome_parser/src/lexer.rs +++ b/crates/biome_parser/src/lexer.rs @@ -655,6 +655,7 @@ where current_kind: Lex::Kind::EOF, current_flags: TokenFlags::empty(), after_line_break: checkpoint.after_line_break, + after_whitespace: checkpoint.after_whitespace, unicode_bom_length: checkpoint.unicode_bom_length, diagnostics_pos: checkpoint.diagnostics_pos, }; @@ -784,6 +785,10 @@ impl LookaheadToken { pub fn has_preceding_line_break(&self) -> bool { self.flags.has_preceding_line_break() } + + pub fn has_preceding_whitespace(&self) -> bool { + self.flags.has_preceding_whitespace() + } } impl From<&LexerCheckpoint> for LookaheadToken { @@ -803,6 +808,7 @@ pub struct LexerCheckpoint { pub current_kind: Kind, pub current_flags: TokenFlags, pub after_line_break: bool, + pub after_whitespace: bool, pub unicode_bom_length: usize, pub diagnostics_pos: u32, } @@ -828,6 +834,7 @@ impl LexerCheckpoint { enum TokenFlag { PrecedingLineBreak = 1 << 0, UnicodeEscape = 1 << 1, + PrecedingWhitespace = 1 << 2, } /// Flags for a lexed token. @@ -841,6 +848,10 @@ impl TokenFlags { /// Indicates that an identifier contains an unicode escape sequence pub const UNICODE_ESCAPE: Self = Self(make_bitflags!(TokenFlag::{UnicodeEscape})); + /// Indicates that there has been a whitespace token between the last + /// non-trivia token and the current token. + pub const PRECEDING_WHITESPACE: Self = Self(make_bitflags!(TokenFlag::{PrecedingWhitespace})); + pub const fn empty() -> Self { Self(BitFlags::EMPTY) } @@ -860,6 +871,10 @@ impl TokenFlags { pub fn has_unicode_escape(&self) -> bool { self.contains(Self::UNICODE_ESCAPE) } + + pub fn has_preceding_whitespace(&self) -> bool { + self.contains(Self::PRECEDING_WHITESPACE) + } } impl BitOr for TokenFlags { diff --git a/crates/biome_tailwind_parser/src/lexer/mod.rs b/crates/biome_tailwind_parser/src/lexer/mod.rs index 75c8f8ffdb9d..f9d6784d2090 100644 --- a/crates/biome_tailwind_parser/src/lexer/mod.rs +++ b/crates/biome_tailwind_parser/src/lexer/mod.rs @@ -330,6 +330,7 @@ impl<'src> Lexer<'src> for TailwindLexer<'src> { current_flags, current_kind, after_line_break, + after_whitespace: _, unicode_bom_length, diagnostics_pos, } = checkpoint; @@ -375,6 +376,7 @@ impl<'src> LexerWithCheckpoint<'src> for TailwindLexer<'src> { current_flags: self.current_flags, current_kind: self.current_kind, after_line_break: self.after_newline, + after_whitespace: false, unicode_bom_length: self.unicode_bom_length, diagnostics_pos: self.diagnostics.len() as u32, } diff --git a/xtask/codegen/css.ungram b/xtask/codegen/css.ungram index 14770630947e..acdf80159072 100644 --- a/xtask/codegen/css.ungram +++ b/xtask/codegen/css.ungram @@ -493,6 +493,7 @@ AnyCssDeclarationOrRule = AnyCssRule | CssDeclarationWithSemicolon | ScssDeclaration + | ScssNestingDeclaration | CssEmptyDeclaration | CssBogus | CssMetavariable @@ -513,6 +514,7 @@ CssDeclarationOrAtRuleList = AnyCssDeclarationOrAtRule* AnyCssDeclarationOrAtRule = CssDeclarationWithSemicolon | ScssDeclaration + | ScssNestingDeclaration | CssEmptyDeclaration | CssAtRule @@ -547,6 +549,7 @@ CssDeclarationList = AnyCssDeclaration* AnyCssDeclaration = CssDeclarationWithSemicolon | ScssDeclaration + | ScssNestingDeclaration | CssEmptyDeclaration AnyCssRuleBlock = @@ -2491,8 +2494,16 @@ ScssNamespacedIdentifier = '.' name: ScssIdentifier -// module.$var -// ^^^^^^^^^^^ +// font: { family: sans-serif; size: 12px; } +// ^^^^ +ScssNestingDeclaration = + name: CssIdentifier + ':' + value: CssGenericComponentValueList + block: AnyCssDeclarationOrRuleBlock + +// color: module.$var; +// ^^^^^^^^^^^ ScssQualifiedName = module: CssIdentifier '.' diff --git a/xtask/codegen/src/css_kinds_src.rs b/xtask/codegen/src/css_kinds_src.rs index 5c09e85ae3ee..cff7fa97592c 100644 --- a/xtask/codegen/src/css_kinds_src.rs +++ b/xtask/codegen/src/css_kinds_src.rs @@ -567,6 +567,7 @@ pub const CSS_KINDS_SRC: KindsSrc = KindsSrc { "CSS_RETURNS_STATEMENT", // SCSS "SCSS_DECLARATION", + "SCSS_NESTING_DECLARATION", "SCSS_NAMESPACED_IDENTIFIER", "SCSS_QUALIFIED_NAME", "SCSS_VARIABLE_MODIFIER_LIST",