From bed0aa11a29076743412ec85f6b9e212db48e565 Mon Sep 17 00:00:00 2001 From: Denis Bezrukov <6227442+denbezrukov@users.noreply.github.com> Date: Wed, 10 Jan 2024 23:23:41 +0200 Subject: [PATCH] feat(css-parser): CSS Parser: parse starting-style at rule #1478 --- .../src/generated/node_factory.rs | 12 + .../src/generated/syntax_factory.rs | 26 ++ .../src/css/any/at_rule.rs | 1 + crates/biome_css_formatter/src/css/any/mod.rs | 1 + .../src/css/any/starting_style_block.rs | 16 ++ .../src/css/statements/mod.rs | 1 + .../css/statements/starting_style_at_rule.rs | 10 + crates/biome_css_formatter/src/generated.rs | 67 +++++ crates/biome_css_parser/src/lexer/mod.rs | 1 + crates/biome_css_parser/src/state.rs | 10 + .../src/syntax/at_rule/keyframes.rs | 11 +- .../src/syntax/at_rule/mod.rs | 6 + .../src/syntax/at_rule/page.rs | 9 +- .../src/syntax/at_rule/starting_style.rs | 65 +++++ crates/biome_css_parser/src/syntax/blocks.rs | 65 +++-- .../ok/at_rule/at_rule_starting_style.css | 15 + .../at_rule/at_rule_starting_style.css.snap | 266 ++++++++++++++++++ crates/biome_css_parser/tests/spec_test.rs | 13 +- crates/biome_css_syntax/src/generated/kind.rs | 6 +- .../biome_css_syntax/src/generated/macros.rs | 4 + .../biome_css_syntax/src/generated/nodes.rs | 221 +++++++++++++++ .../src/generated/nodes_mut.rs | 14 + xtask/codegen/css.ungram | 22 +- xtask/codegen/src/css_kinds_src.rs | 2 + 24 files changed, 826 insertions(+), 38 deletions(-) create mode 100644 crates/biome_css_formatter/src/css/any/starting_style_block.rs create mode 100644 crates/biome_css_formatter/src/css/statements/starting_style_at_rule.rs create mode 100644 crates/biome_css_parser/src/syntax/at_rule/starting_style.rs create mode 100644 crates/biome_css_parser/tests/css_test_suite/ok/at_rule/at_rule_starting_style.css create mode 100644 crates/biome_css_parser/tests/css_test_suite/ok/at_rule/at_rule_starting_style.css.snap diff --git a/crates/biome_css_factory/src/generated/node_factory.rs b/crates/biome_css_factory/src/generated/node_factory.rs index 1a3cf39fd5bf..1debcfed1990 100644 --- a/crates/biome_css_factory/src/generated/node_factory.rs +++ b/crates/biome_css_factory/src/generated/node_factory.rs @@ -1746,6 +1746,18 @@ pub fn css_simple_function( ], )) } +pub fn css_starting_style_at_rule( + starting_style_token: SyntaxToken, + block: AnyCssStartingStyleBlock, +) -> CssStartingStyleAtRule { + CssStartingStyleAtRule::unwrap_cast(SyntaxNode::new_detached( + CssSyntaxKind::CSS_STARTING_STYLE_AT_RULE, + [ + Some(SyntaxElement::Token(starting_style_token)), + Some(SyntaxElement::Node(block.into_syntax())), + ], + )) +} pub fn css_string(value_token: SyntaxToken) -> CssString { CssString::unwrap_cast(SyntaxNode::new_detached( CssSyntaxKind::CSS_STRING, diff --git a/crates/biome_css_factory/src/generated/syntax_factory.rs b/crates/biome_css_factory/src/generated/syntax_factory.rs index b7bbcbe96629..d715c2aca649 100644 --- a/crates/biome_css_factory/src/generated/syntax_factory.rs +++ b/crates/biome_css_factory/src/generated/syntax_factory.rs @@ -3525,6 +3525,32 @@ impl SyntaxFactory for CssSyntaxFactory { } slots.into_node(CSS_SIMPLE_FUNCTION, children) } + CSS_STARTING_STYLE_AT_RULE => { + let mut elements = (&children).into_iter(); + let mut slots: RawNodeSlots<2usize> = RawNodeSlots::default(); + let mut current_element = elements.next(); + if let Some(element) = ¤t_element { + if element.kind() == T![starting_style] { + slots.mark_present(); + current_element = elements.next(); + } + } + slots.next_slot(); + if let Some(element) = ¤t_element { + if AnyCssStartingStyleBlock::can_cast(element.kind()) { + slots.mark_present(); + current_element = elements.next(); + } + } + slots.next_slot(); + if current_element.is_some() { + return RawSyntaxNode::new( + CSS_STARTING_STYLE_AT_RULE.to_bogus(), + children.into_iter().map(Some), + ); + } + slots.into_node(CSS_STARTING_STYLE_AT_RULE, children) + } CSS_STRING => { let mut elements = (&children).into_iter(); let mut slots: RawNodeSlots<1usize> = RawNodeSlots::default(); diff --git a/crates/biome_css_formatter/src/css/any/at_rule.rs b/crates/biome_css_formatter/src/css/any/at_rule.rs index 8bc2986486db..438703a98065 100644 --- a/crates/biome_css_formatter/src/css/any/at_rule.rs +++ b/crates/biome_css_formatter/src/css/any/at_rule.rs @@ -22,6 +22,7 @@ impl FormatRule for FormatAnyCssAtRule { AnyCssAtRule::CssScopeAtRule(node) => node.format().fmt(f), AnyCssAtRule::CssImportAtRule(node) => node.format().fmt(f), AnyCssAtRule::CssNamespaceAtRule(node) => node.format().fmt(f), + AnyCssAtRule::CssStartingStyleAtRule(node) => node.format().fmt(f), AnyCssAtRule::CssBogusAtRule(node) => node.format().fmt(f), } } diff --git a/crates/biome_css_formatter/src/css/any/mod.rs b/crates/biome_css_formatter/src/css/any/mod.rs index ceb588b3988c..c64a142f7ee5 100644 --- a/crates/biome_css_formatter/src/css/any/mod.rs +++ b/crates/biome_css_formatter/src/css/any/mod.rs @@ -56,6 +56,7 @@ pub(crate) mod rule_list_block; pub(crate) mod scope_range; pub(crate) mod selector; pub(crate) mod simple_selector; +pub(crate) mod starting_style_block; pub(crate) mod sub_selector; pub(crate) mod supports_and_combinable_condition; pub(crate) mod supports_condition; diff --git a/crates/biome_css_formatter/src/css/any/starting_style_block.rs b/crates/biome_css_formatter/src/css/any/starting_style_block.rs new file mode 100644 index 000000000000..4659a7af2544 --- /dev/null +++ b/crates/biome_css_formatter/src/css/any/starting_style_block.rs @@ -0,0 +1,16 @@ +//! This is a generated file. Don't modify it by hand! Run 'cargo codegen formatter' to re-generate the file. + +use crate::prelude::*; +use biome_css_syntax::AnyCssStartingStyleBlock; +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatAnyCssStartingStyleBlock; +impl FormatRule for FormatAnyCssStartingStyleBlock { + type Context = CssFormatContext; + fn fmt(&self, node: &AnyCssStartingStyleBlock, f: &mut CssFormatter) -> FormatResult<()> { + match node { + AnyCssStartingStyleBlock::CssRuleListBlock(node) => node.format().fmt(f), + AnyCssStartingStyleBlock::CssDeclarationListBlock(node) => node.format().fmt(f), + AnyCssStartingStyleBlock::CssBogusBlock(node) => node.format().fmt(f), + } + } +} diff --git a/crates/biome_css_formatter/src/css/statements/mod.rs b/crates/biome_css_formatter/src/css/statements/mod.rs index a7dcb680ed5c..e5d9b2ff58b9 100644 --- a/crates/biome_css_formatter/src/css/statements/mod.rs +++ b/crates/biome_css_formatter/src/css/statements/mod.rs @@ -15,4 +15,5 @@ pub(crate) mod media_at_rule; pub(crate) mod namespace_at_rule; pub(crate) mod page_at_rule; pub(crate) mod scope_at_rule; +pub(crate) mod starting_style_at_rule; pub(crate) mod supports_at_rule; diff --git a/crates/biome_css_formatter/src/css/statements/starting_style_at_rule.rs b/crates/biome_css_formatter/src/css/statements/starting_style_at_rule.rs new file mode 100644 index 000000000000..d955b2afc2f2 --- /dev/null +++ b/crates/biome_css_formatter/src/css/statements/starting_style_at_rule.rs @@ -0,0 +1,10 @@ +use crate::prelude::*; +use biome_css_syntax::CssStartingStyleAtRule; +use biome_rowan::AstNode; +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatCssStartingStyleAtRule; +impl FormatNodeRule for FormatCssStartingStyleAtRule { + fn fmt_fields(&self, node: &CssStartingStyleAtRule, f: &mut CssFormatter) -> FormatResult<()> { + format_verbatim_node(node.syntax()).fmt(f) + } +} diff --git a/crates/biome_css_formatter/src/generated.rs b/crates/biome_css_formatter/src/generated.rs index 0d4999f85a49..dd2bb1155ec0 100644 --- a/crates/biome_css_formatter/src/generated.rs +++ b/crates/biome_css_formatter/src/generated.rs @@ -2680,6 +2680,46 @@ impl IntoFormat for biome_css_syntax::CssNamespaceAtRule { ) } } +impl FormatRule + for crate::css::statements::starting_style_at_rule::FormatCssStartingStyleAtRule +{ + type Context = CssFormatContext; + #[inline(always)] + fn fmt( + &self, + node: &biome_css_syntax::CssStartingStyleAtRule, + f: &mut CssFormatter, + ) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl AsFormat for biome_css_syntax::CssStartingStyleAtRule { + type Format<'a> = FormatRefWithRule< + 'a, + biome_css_syntax::CssStartingStyleAtRule, + crate::css::statements::starting_style_at_rule::FormatCssStartingStyleAtRule, + >; + fn format(&self) -> Self::Format<'_> { + #![allow(clippy::default_constructed_unit_structs)] + FormatRefWithRule::new( + self, + crate::css::statements::starting_style_at_rule::FormatCssStartingStyleAtRule::default(), + ) + } +} +impl IntoFormat for biome_css_syntax::CssStartingStyleAtRule { + type Format = FormatOwnedWithRule< + biome_css_syntax::CssStartingStyleAtRule, + crate::css::statements::starting_style_at_rule::FormatCssStartingStyleAtRule, + >; + fn into_format(self) -> Self::Format { + #![allow(clippy::default_constructed_unit_structs)] + FormatOwnedWithRule::new( + self, + crate::css::statements::starting_style_at_rule::FormatCssStartingStyleAtRule::default(), + ) + } +} impl FormatRule for crate::css::auxiliary::container_not_query::FormatCssContainerNotQuery { @@ -7621,6 +7661,33 @@ impl IntoFormat for biome_css_syntax::AnyCssNamespaceUrl { ) } } +impl AsFormat for biome_css_syntax::AnyCssStartingStyleBlock { + type Format<'a> = FormatRefWithRule< + 'a, + biome_css_syntax::AnyCssStartingStyleBlock, + crate::css::any::starting_style_block::FormatAnyCssStartingStyleBlock, + >; + fn format(&self) -> Self::Format<'_> { + #![allow(clippy::default_constructed_unit_structs)] + FormatRefWithRule::new( + self, + crate::css::any::starting_style_block::FormatAnyCssStartingStyleBlock::default(), + ) + } +} +impl IntoFormat for biome_css_syntax::AnyCssStartingStyleBlock { + type Format = FormatOwnedWithRule< + biome_css_syntax::AnyCssStartingStyleBlock, + crate::css::any::starting_style_block::FormatAnyCssStartingStyleBlock, + >; + fn into_format(self) -> Self::Format { + #![allow(clippy::default_constructed_unit_structs)] + FormatOwnedWithRule::new( + self, + crate::css::any::starting_style_block::FormatAnyCssStartingStyleBlock::default(), + ) + } +} impl AsFormat for biome_css_syntax::AnyCssUrlValue { type Format<'a> = FormatRefWithRule< 'a, diff --git a/crates/biome_css_parser/src/lexer/mod.rs b/crates/biome_css_parser/src/lexer/mod.rs index a0be79852968..d83e9e70c44c 100644 --- a/crates/biome_css_parser/src/lexer/mod.rs +++ b/crates/biome_css_parser/src/lexer/mod.rs @@ -997,6 +997,7 @@ impl<'src> CssLexer<'src> { b"scope" => SCOPE_KW, b"import" => IMPORT_KW, b"namespace" => NAMESPACE_KW, + b"starting-style" => STARTING_STYLE_KW, _ => IDENT, } } diff --git a/crates/biome_css_parser/src/state.rs b/crates/biome_css_parser/src/state.rs index 098f104d7061..d47e9120f814 100644 --- a/crates/biome_css_parser/src/state.rs +++ b/crates/biome_css_parser/src/state.rs @@ -11,12 +11,22 @@ pub(crate) struct CssParserState { /// The challenge is, that it isn't possible to tell which of the two kinds it is until the parser /// processed all of `(a, b)`. pub(crate) speculative_parsing: bool, + + /// Indicates whether the parser is currently dealing with a nesting block in the CSS document. + /// + /// This field is essential for understanding the current context of the parser. When set to `true`, + /// it indicates that the parser is inside a nested or inner element, such as rules within media queries + /// or other nested structures. Conversely, when set to `false`, it implies that the parser is at the root level, + /// handling top-level `@rules` or style declarations directly under the stylesheet. + /// This distinction is critical for correctly interpreting and parsing different sections of a CSS document. + pub(crate) is_nesting_block: bool, } impl CssParserState { pub fn new() -> Self { Self { speculative_parsing: false, + is_nesting_block: false, } } } diff --git a/crates/biome_css_parser/src/syntax/at_rule/keyframes.rs b/crates/biome_css_parser/src/syntax/at_rule/keyframes.rs index aa936cafb61e..747d21320f89 100644 --- a/crates/biome_css_parser/src/syntax/at_rule/keyframes.rs +++ b/crates/biome_css_parser/src/syntax/at_rule/keyframes.rs @@ -3,7 +3,7 @@ use crate::parser::CssParser; use crate::syntax::at_rule::parse_error::{ expected_keyframes_item, expected_keyframes_item_selector, }; -use crate::syntax::blocks::parse_declaration_list_block; +use crate::syntax::blocks::{parse_block_body, parse_declaration_list_block}; use crate::syntax::css_dimension::{is_at_percentage_dimension, parse_percentage_dimension}; use crate::syntax::parse_error::{expected_block, expected_non_css_wide_keyword_identifier}; use crate::syntax::{ @@ -74,10 +74,9 @@ fn parse_keyframes_block(p: &mut CssParser) -> ParsedSyntax { return Absent; } - let m = p.start(); - p.bump(T!['{']); - KeyframesItemList.parse_list(p); - p.expect(T!['}']); + let m = parse_block_body(p, |p| { + KeyframesItemList.parse_list(p); + }); Present(m.complete(p, CSS_KEYFRAMES_BLOCK)) } @@ -141,7 +140,7 @@ impl ParseRecovery for KeyframesItemBlockParseRecovery { // color: blue; // } // } - p.at_ts(token_set!(T!['}'])) || is_at_keyframes_item_selector(p) + p.at(T!['}']) || is_at_keyframes_item_selector(p) } } diff --git a/crates/biome_css_parser/src/syntax/at_rule/mod.rs b/crates/biome_css_parser/src/syntax/at_rule/mod.rs index 9ba7b25b862d..dcba616b66df 100644 --- a/crates/biome_css_parser/src/syntax/at_rule/mod.rs +++ b/crates/biome_css_parser/src/syntax/at_rule/mod.rs @@ -13,6 +13,7 @@ mod namespace; mod page; mod parse_error; mod scope; +mod starting_style; mod supports; use crate::parser::CssParser; @@ -35,6 +36,9 @@ use crate::syntax::at_rule::media::{is_at_media_at_rule, parse_media_at_rule}; use crate::syntax::at_rule::namespace::{is_at_namespace_at_rule, parse_namespace_at_rule}; use crate::syntax::at_rule::page::{is_at_page_at_rule, parse_page_at_rule}; use crate::syntax::at_rule::scope::{is_at_scope_at_rule, parse_scope_at_rule}; +use crate::syntax::at_rule::starting_style::{ + is_at_starting_style_at_rule, parse_starting_style_at_rule, +}; use crate::syntax::at_rule::supports::{is_at_supports_at_rule, parse_supports_at_rule}; use crate::syntax::parse_error::expected_any_at_rule; use biome_css_syntax::CssSyntaxKind::*; @@ -99,6 +103,8 @@ pub(crate) fn parse_any_at_rule(p: &mut CssParser) -> ParsedSyntax { parse_import_at_rule(p) } else if is_at_namespace_at_rule(p) { parse_namespace_at_rule(p) + } else if is_at_starting_style_at_rule(p) { + parse_starting_style_at_rule(p) } else { Absent } diff --git a/crates/biome_css_parser/src/syntax/at_rule/page.rs b/crates/biome_css_parser/src/syntax/at_rule/page.rs index 11374805672d..6a1d4b208019 100644 --- a/crates/biome_css_parser/src/syntax/at_rule/page.rs +++ b/crates/biome_css_parser/src/syntax/at_rule/page.rs @@ -4,7 +4,7 @@ use crate::syntax::at_rule::parse_error::{ expected_any_page_at_rule_item, expected_page_selector, expected_page_selector_pseudo, }; use crate::syntax::at_rule::{is_at_at_rule, parse_at_rule}; -use crate::syntax::blocks::parse_or_recover_declaration_or_rule_list_block; +use crate::syntax::blocks::{parse_block_body, parse_or_recover_declaration_or_rule_list_block}; use crate::syntax::parse_error::expected_block; use crate::syntax::{ is_at_identifier, parse_custom_identifier_with_keywords, parse_declaration_with_semicolon, @@ -176,11 +176,10 @@ pub(crate) fn parse_page_block(p: &mut CssParser) -> ParsedSyntax { if !p.at(T!['{']) { return Absent; } - let m = p.start(); - p.expect(T!['{']); - PageAtRuleItemList.parse_list(p); - p.expect(T!['}']); + let m = parse_block_body(p, |p| { + PageAtRuleItemList.parse_list(p); + }); Present(m.complete(p, CSS_PAGE_AT_RULE_BLOCK)) } diff --git a/crates/biome_css_parser/src/syntax/at_rule/starting_style.rs b/crates/biome_css_parser/src/syntax/at_rule/starting_style.rs new file mode 100644 index 000000000000..2c4a889ba661 --- /dev/null +++ b/crates/biome_css_parser/src/syntax/at_rule/starting_style.rs @@ -0,0 +1,65 @@ +use crate::parser::CssParser; +use crate::syntax::blocks::{ + parse_or_recover_declaration_list_block, parse_or_recover_rule_list_block, +}; +use biome_css_syntax::CssSyntaxKind::*; +use biome_css_syntax::T; +use biome_parser::parsed_syntax::ParsedSyntax::Present; +use biome_parser::prelude::ParsedSyntax::Absent; +use biome_parser::prelude::*; + +/// Checks if the current token in the parser is a `@starting-style` at-rule. +/// +/// This function verifies if the current token matches the `@starting-style` rule, +/// which is a custom at-rule used for specific parsing scenarios. +#[inline] +pub(crate) fn is_at_starting_style_at_rule(p: &mut CssParser) -> bool { + p.at(T![starting_style]) +} + +/// Parses a `@starting-style` at-rule in a CSS stylesheet. +/// +/// This function handles the parsing of a `@starting-style` at-rule, which is defined in the +/// CSS Transitions Level 2 specification. It starts by confirming the presence of such a rule and then +/// processes the content depending on whether the parser is currently inside a nesting block or at the root level. +/// It employs different parsing strategies for declarations or rules based on the parser state `is_nesting_block`. +/// +/// Specification: [CSS Transitions Level 2 - @starting-style](https://drafts.csswg.org/css-transitions-2/#at-ruledef-starting-style) +/// # Examples +/// Basic usage in a CSS stylesheet: +/// +/// ```css +/// // At the root level of a stylesheet +/// @starting-style { +/// /* rulesets */ +/// } +/// +/// // Inside a selector +/// selector { +/// @starting-style { +/// /* declarations */ +/// } +/// } +/// ``` +#[inline] +pub(crate) fn parse_starting_style_at_rule(p: &mut CssParser) -> ParsedSyntax { + if !is_at_starting_style_at_rule(p) { + return Absent; + } + + let m = p.start(); + + p.bump(T![starting_style]); + + let block = if p.state().is_nesting_block { + parse_or_recover_declaration_list_block(p) + } else { + parse_or_recover_rule_list_block(p) + }; + + if block.is_err() { + return Present(m.complete(p, CSS_BOGUS_AT_RULE)); + } + + Present(m.complete(p, CSS_STARTING_STYLE_AT_RULE)) +} diff --git a/crates/biome_css_parser/src/syntax/blocks.rs b/crates/biome_css_parser/src/syntax/blocks.rs index aa00755b67fe..c44af41f0a93 100644 --- a/crates/biome_css_parser/src/syntax/blocks.rs +++ b/crates/biome_css_parser/src/syntax/blocks.rs @@ -2,15 +2,16 @@ use crate::parser::CssParser; use crate::syntax::at_rule::{is_at_at_rule, parse_at_rule}; use crate::syntax::parse_error::{expected_any_declaration_or_at_rule, expected_block}; use crate::syntax::{ - parse_declaration_with_semicolon, DeclarationList, RuleList, BODY_RECOVERY_SET, + is_at_declaration, parse_declaration_with_semicolon, DeclarationList, RuleList, + BODY_RECOVERY_SET, }; use biome_css_syntax::CssSyntaxKind::*; use biome_css_syntax::{CssSyntaxKind, T}; use biome_parser::parse_lists::{ParseNodeList, ParseSeparatedList}; -use biome_parser::parse_recovery::{ParseRecoveryTokenSet, RecoveryResult}; +use biome_parser::parse_recovery::{ParseRecovery, ParseRecoveryTokenSet, RecoveryResult}; use biome_parser::parsed_syntax::ParsedSyntax; use biome_parser::parsed_syntax::ParsedSyntax::{Absent, Present}; -use biome_parser::{token_set, Parser, TokenSet}; +use biome_parser::{Marker, Parser}; #[inline] pub(crate) fn parse_or_recover_declaration_list_block(p: &mut CssParser) -> RecoveryResult { @@ -28,11 +29,9 @@ pub(crate) fn parse_declaration_list_block(p: &mut CssParser) -> ParsedSyntax { return Absent; } - let m = p.start(); - - p.bump(T!['{']); - DeclarationList.parse_list(p); - p.expect(T!['}']); + let m = parse_block_body(p, |p| { + DeclarationList.parse_list(p); + }); Present(m.complete(p, CSS_DECLARATION_LIST_BLOCK)) } @@ -53,11 +52,9 @@ pub(crate) fn parse_rule_list_block(p: &mut CssParser) -> ParsedSyntax { return Absent; } - let m = p.start(); - - p.expect(T!['{']); - RuleList::new(T!['}']).parse_list(p); - p.expect(T!['}']); + let m = parse_block_body(p, |p| { + RuleList::new(T!['}']).parse_list(p); + }); Present(m.complete(p, CSS_RULE_LIST_BLOCK)) } @@ -78,17 +75,24 @@ pub(crate) fn parse_declaration_or_rule_list_block(p: &mut CssParser) -> ParsedS return Absent; } - let m = p.start(); - - p.expect(T!['{']); - DeclarationOrAtRuleList.parse_list(p); - p.expect(T!['}']); + let m = parse_block_body(p, |p| { + DeclarationOrAtRuleList.parse_list(p); + }); Present(m.complete(p, CSS_DECLARATION_OR_AT_RULE_BLOCK)) } -const CSS_DECLARATION_OR_AT_RULE_LIST_RECOVERY_SET: TokenSet = - token_set!(T![@], T![ident]); +struct DeclarationOrAtRuleListParseRecovery; +impl ParseRecovery for DeclarationOrAtRuleListParseRecovery { + type Kind = CssSyntaxKind; + type Parser<'source> = CssParser<'source>; + const RECOVERED_KIND: Self::Kind = CSS_BOGUS; + + fn is_at_recovered(&self, p: &mut Self::Parser<'_>) -> bool { + p.at(T!['}']) || is_at_at_rule(p) || is_at_declaration(p) + } +} + struct DeclarationOrAtRuleList; impl ParseNodeList for DeclarationOrAtRuleList { type Kind = CssSyntaxKind; @@ -112,10 +116,27 @@ impl ParseNodeList for DeclarationOrAtRuleList { p: &mut Self::Parser<'_>, parsed_element: ParsedSyntax, ) -> RecoveryResult { - parsed_element.or_recover_with_token_set( + parsed_element.or_recover( p, - &ParseRecoveryTokenSet::new(CSS_BOGUS, CSS_DECLARATION_OR_AT_RULE_LIST_RECOVERY_SET), + &DeclarationOrAtRuleListParseRecovery, expected_any_declaration_or_at_rule, ) } } + +/// Parses the body of a block in CSS. +/// +/// This function handles the parsing of a block's content, delimited by curly braces `{}`. +/// It temporarily sets the parser's state to indicate it is within a nesting block and then +/// processes the content of the block using the provided callback function. +pub(crate) fn parse_block_body(p: &mut CssParser, func: impl FnOnce(&mut CssParser)) -> Marker { + let old_nesting_block = std::mem::replace(&mut p.state_mut().is_nesting_block, true); + + let m = p.start(); + p.bump(T!['{']); + func(p); + p.expect(T!['}']); + + p.state_mut().is_nesting_block = old_nesting_block; + m +} diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/at_rule/at_rule_starting_style.css b/crates/biome_css_parser/tests/css_test_suite/ok/at_rule/at_rule_starting_style.css new file mode 100644 index 000000000000..f3448388d27f --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/at_rule/at_rule_starting_style.css @@ -0,0 +1,15 @@ +@starting-style { + h1 { + background-color: transparent; + } + + @layer foo { + @starting-style { + background-color: transparent; + } + + div { + height: 100px; + } + } +} diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/at_rule/at_rule_starting_style.css.snap b/crates/biome_css_parser/tests/css_test_suite/ok/at_rule/at_rule_starting_style.css.snap new file mode 100644 index 000000000000..9bd15c775fad --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/at_rule/at_rule_starting_style.css.snap @@ -0,0 +1,266 @@ +--- +source: crates/biome_css_parser/tests/spec_test.rs +expression: snapshot +--- + +## Input + +```css +@starting-style { + h1 { + background-color: transparent; + } + + @layer foo { + @starting-style { + background-color: transparent; + } + + div { + height: 100px; + } + } +} + +``` + + +## AST + +``` +CssRoot { + bom_token: missing (optional), + rules: CssRuleList [ + CssAtRule { + at_token: AT@0..1 "@" [] [], + rule: CssStartingStyleAtRule { + starting_style_token: STARTING_STYLE_KW@1..16 "starting-style" [] [Whitespace(" ")], + block: CssRuleListBlock { + l_curly_token: L_CURLY@16..17 "{" [] [], + rules: CssRuleList [ + CssRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selector_token: missing (optional), + simple_selector: CssTypeSelector { + namespace: missing (optional), + ident: CssIdentifier { + value_token: IDENT@17..22 "h1" [Newline("\n"), Whitespace("\t")] [Whitespace(" ")], + }, + }, + sub_selectors: CssSubSelectorList [], + }, + ], + block: CssDeclarationListBlock { + l_curly_token: L_CURLY@22..23 "{" [] [], + declarations: CssDeclarationList [ + CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@23..42 "background-color" [Newline("\n"), Whitespace("\t\t")] [], + }, + colon_token: COLON@42..44 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@44..55 "transparent" [] [], + }, + ], + }, + important: missing (optional), + }, + SEMICOLON@55..56 ";" [] [], + ], + r_curly_token: R_CURLY@56..59 "}" [Newline("\n"), Whitespace("\t")] [], + }, + }, + CssAtRule { + at_token: AT@59..63 "@" [Newline("\n"), Newline("\n"), Whitespace("\t")] [], + rule: CssLayerAtRule { + layer_token: LAYER_KW@63..69 "layer" [] [Whitespace(" ")], + layer: CssLayerDeclaration { + references: CssLayerReferenceList [ + CssLayerNameList [ + CssIdentifier { + value_token: IDENT@69..73 "foo" [] [Whitespace(" ")], + }, + ], + ], + block: CssRuleListBlock { + l_curly_token: L_CURLY@73..74 "{" [] [], + rules: CssRuleList [ + CssAtRule { + at_token: AT@74..78 "@" [Newline("\n"), Whitespace("\t\t")] [], + rule: CssStartingStyleAtRule { + starting_style_token: STARTING_STYLE_KW@78..93 "starting-style" [] [Whitespace(" ")], + block: CssDeclarationListBlock { + l_curly_token: L_CURLY@93..94 "{" [] [], + declarations: CssDeclarationList [ + CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@94..114 "background-color" [Newline("\n"), Whitespace("\t\t\t")] [], + }, + colon_token: COLON@114..116 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@116..127 "transparent" [] [], + }, + ], + }, + important: missing (optional), + }, + SEMICOLON@127..128 ";" [] [], + ], + r_curly_token: R_CURLY@128..132 "}" [Newline("\n"), Whitespace("\t\t")] [], + }, + }, + }, + CssRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selector_token: missing (optional), + simple_selector: CssTypeSelector { + namespace: missing (optional), + ident: CssIdentifier { + value_token: IDENT@132..140 "div" [Newline("\n"), Newline("\n"), Whitespace("\t\t")] [Whitespace(" ")], + }, + }, + sub_selectors: CssSubSelectorList [], + }, + ], + block: CssDeclarationListBlock { + l_curly_token: L_CURLY@140..141 "{" [] [], + declarations: CssDeclarationList [ + CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@141..151 "height" [Newline("\n"), Whitespace("\t\t\t")] [], + }, + colon_token: COLON@151..153 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssRegularDimension { + value_token: CSS_NUMBER_LITERAL@153..156 "100" [] [], + unit_token: IDENT@156..158 "px" [] [], + }, + ], + }, + important: missing (optional), + }, + SEMICOLON@158..159 ";" [] [], + ], + r_curly_token: R_CURLY@159..163 "}" [Newline("\n"), Whitespace("\t\t")] [], + }, + }, + ], + r_curly_token: R_CURLY@163..166 "}" [Newline("\n"), Whitespace("\t")] [], + }, + }, + }, + }, + ], + r_curly_token: R_CURLY@166..168 "}" [Newline("\n")] [], + }, + }, + }, + ], + eof_token: EOF@168..169 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: CSS_ROOT@0..169 + 0: (empty) + 1: CSS_RULE_LIST@0..168 + 0: CSS_AT_RULE@0..168 + 0: AT@0..1 "@" [] [] + 1: CSS_STARTING_STYLE_AT_RULE@1..168 + 0: STARTING_STYLE_KW@1..16 "starting-style" [] [Whitespace(" ")] + 1: CSS_RULE_LIST_BLOCK@16..168 + 0: L_CURLY@16..17 "{" [] [] + 1: CSS_RULE_LIST@17..166 + 0: CSS_RULE@17..59 + 0: CSS_SELECTOR_LIST@17..22 + 0: CSS_COMPOUND_SELECTOR@17..22 + 0: (empty) + 1: CSS_TYPE_SELECTOR@17..22 + 0: (empty) + 1: CSS_IDENTIFIER@17..22 + 0: IDENT@17..22 "h1" [Newline("\n"), Whitespace("\t")] [Whitespace(" ")] + 2: CSS_SUB_SELECTOR_LIST@22..22 + 1: CSS_DECLARATION_LIST_BLOCK@22..59 + 0: L_CURLY@22..23 "{" [] [] + 1: CSS_DECLARATION_LIST@23..56 + 0: CSS_DECLARATION@23..55 + 0: CSS_GENERIC_PROPERTY@23..55 + 0: CSS_IDENTIFIER@23..42 + 0: IDENT@23..42 "background-color" [Newline("\n"), Whitespace("\t\t")] [] + 1: COLON@42..44 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@44..55 + 0: CSS_IDENTIFIER@44..55 + 0: IDENT@44..55 "transparent" [] [] + 1: (empty) + 1: SEMICOLON@55..56 ";" [] [] + 2: R_CURLY@56..59 "}" [Newline("\n"), Whitespace("\t")] [] + 1: CSS_AT_RULE@59..166 + 0: AT@59..63 "@" [Newline("\n"), Newline("\n"), Whitespace("\t")] [] + 1: CSS_LAYER_AT_RULE@63..166 + 0: LAYER_KW@63..69 "layer" [] [Whitespace(" ")] + 1: CSS_LAYER_DECLARATION@69..166 + 0: CSS_LAYER_REFERENCE_LIST@69..73 + 0: CSS_LAYER_NAME_LIST@69..73 + 0: CSS_IDENTIFIER@69..73 + 0: IDENT@69..73 "foo" [] [Whitespace(" ")] + 1: CSS_RULE_LIST_BLOCK@73..166 + 0: L_CURLY@73..74 "{" [] [] + 1: CSS_RULE_LIST@74..163 + 0: CSS_AT_RULE@74..132 + 0: AT@74..78 "@" [Newline("\n"), Whitespace("\t\t")] [] + 1: CSS_STARTING_STYLE_AT_RULE@78..132 + 0: STARTING_STYLE_KW@78..93 "starting-style" [] [Whitespace(" ")] + 1: CSS_DECLARATION_LIST_BLOCK@93..132 + 0: L_CURLY@93..94 "{" [] [] + 1: CSS_DECLARATION_LIST@94..128 + 0: CSS_DECLARATION@94..127 + 0: CSS_GENERIC_PROPERTY@94..127 + 0: CSS_IDENTIFIER@94..114 + 0: IDENT@94..114 "background-color" [Newline("\n"), Whitespace("\t\t\t")] [] + 1: COLON@114..116 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@116..127 + 0: CSS_IDENTIFIER@116..127 + 0: IDENT@116..127 "transparent" [] [] + 1: (empty) + 1: SEMICOLON@127..128 ";" [] [] + 2: R_CURLY@128..132 "}" [Newline("\n"), Whitespace("\t\t")] [] + 1: CSS_RULE@132..163 + 0: CSS_SELECTOR_LIST@132..140 + 0: CSS_COMPOUND_SELECTOR@132..140 + 0: (empty) + 1: CSS_TYPE_SELECTOR@132..140 + 0: (empty) + 1: CSS_IDENTIFIER@132..140 + 0: IDENT@132..140 "div" [Newline("\n"), Newline("\n"), Whitespace("\t\t")] [Whitespace(" ")] + 2: CSS_SUB_SELECTOR_LIST@140..140 + 1: CSS_DECLARATION_LIST_BLOCK@140..163 + 0: L_CURLY@140..141 "{" [] [] + 1: CSS_DECLARATION_LIST@141..159 + 0: CSS_DECLARATION@141..158 + 0: CSS_GENERIC_PROPERTY@141..158 + 0: CSS_IDENTIFIER@141..151 + 0: IDENT@141..151 "height" [Newline("\n"), Whitespace("\t\t\t")] [] + 1: COLON@151..153 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@153..158 + 0: CSS_REGULAR_DIMENSION@153..158 + 0: CSS_NUMBER_LITERAL@153..156 "100" [] [] + 1: IDENT@156..158 "px" [] [] + 1: (empty) + 1: SEMICOLON@158..159 ";" [] [] + 2: R_CURLY@159..163 "}" [Newline("\n"), Whitespace("\t\t")] [] + 2: R_CURLY@163..166 "}" [Newline("\n"), Whitespace("\t")] [] + 2: R_CURLY@166..168 "}" [Newline("\n")] [] + 2: EOF@168..169 "" [Newline("\n")] [] + +``` + + diff --git a/crates/biome_css_parser/tests/spec_test.rs b/crates/biome_css_parser/tests/spec_test.rs index 779c6f0ef44d..bfdc68a1e4ca 100644 --- a/crates/biome_css_parser/tests/spec_test.rs +++ b/crates/biome_css_parser/tests/spec_test.rs @@ -135,9 +135,16 @@ pub fn run(test_case: &str, _snapshot_name: &str, test_directory: &str, outcome_ pub fn quick_test() { let code = r#" - -@namespace url(http://www.w3.org/1999/xhtml); - +@starting-style { + h1 { + background-color: transparent; + } + @layer foo { + @starting-style { + height: 100px; + } + } +} "#; let root = parse_css( diff --git a/crates/biome_css_syntax/src/generated/kind.rs b/crates/biome_css_syntax/src/generated/kind.rs index 4c9eb3e047d8..e246840a8215 100644 --- a/crates/biome_css_syntax/src/generated/kind.rs +++ b/crates/biome_css_syntax/src/generated/kind.rs @@ -212,6 +212,7 @@ pub enum CssSyntaxKind { SELECTOR_KW, IMPORT_KW, NAMESPACE_KW, + STARTING_STYLE_KW, FONT_FACE_KW, CSS_STRING_LITERAL, CSS_NUMBER_LITERAL, @@ -388,6 +389,7 @@ pub enum CssSyntaxKind { CSS_IMPORT_NAMED_LAYER, CSS_IMPORT_SUPPORTS, CSS_NAMESPACE_AT_RULE, + CSS_STARTING_STYLE_AT_RULE, CSS_BOGUS, CSS_BOGUS_BLOCK, CSS_BOGUS_KEYFRAMES_ITEM, @@ -627,6 +629,7 @@ impl CssSyntaxKind { "selector" => SELECTOR_KW, "import" => IMPORT_KW, "namespace" => NAMESPACE_KW, + "starting-style" => STARTING_STYLE_KW, "font-face" => FONT_FACE_KW, _ => return None, }; @@ -833,6 +836,7 @@ impl CssSyntaxKind { SELECTOR_KW => "selector", IMPORT_KW => "import", NAMESPACE_KW => "namespace", + STARTING_STYLE_KW => "starting-style", FONT_FACE_KW => "font-face", CSS_STRING_LITERAL => "string literal", _ => return None, @@ -842,4 +846,4 @@ impl CssSyntaxKind { } #[doc = r" Utility macro for creating a SyntaxKind through simple macro syntax"] #[macro_export] -macro_rules ! T { [;] => { $ crate :: CssSyntaxKind :: SEMICOLON } ; [,] => { $ crate :: CssSyntaxKind :: COMMA } ; ['('] => { $ crate :: CssSyntaxKind :: L_PAREN } ; [')'] => { $ crate :: CssSyntaxKind :: R_PAREN } ; ['{'] => { $ crate :: CssSyntaxKind :: L_CURLY } ; ['}'] => { $ crate :: CssSyntaxKind :: R_CURLY } ; ['['] => { $ crate :: CssSyntaxKind :: L_BRACK } ; [']'] => { $ crate :: CssSyntaxKind :: R_BRACK } ; [<] => { $ crate :: CssSyntaxKind :: L_ANGLE } ; [>] => { $ crate :: CssSyntaxKind :: R_ANGLE } ; [~] => { $ crate :: CssSyntaxKind :: TILDE } ; [#] => { $ crate :: CssSyntaxKind :: HASH } ; [&] => { $ crate :: CssSyntaxKind :: AMP } ; [|] => { $ crate :: CssSyntaxKind :: PIPE } ; [||] => { $ crate :: CssSyntaxKind :: PIPE2 } ; [+] => { $ crate :: CssSyntaxKind :: PLUS } ; [*] => { $ crate :: CssSyntaxKind :: STAR } ; [/] => { $ crate :: CssSyntaxKind :: SLASH } ; [^] => { $ crate :: CssSyntaxKind :: CARET } ; [%] => { $ crate :: CssSyntaxKind :: PERCENT } ; [.] => { $ crate :: CssSyntaxKind :: DOT } ; [:] => { $ crate :: CssSyntaxKind :: COLON } ; [::] => { $ crate :: CssSyntaxKind :: COLON2 } ; [=] => { $ crate :: CssSyntaxKind :: EQ } ; [!] => { $ crate :: CssSyntaxKind :: BANG } ; [!=] => { $ crate :: CssSyntaxKind :: NEQ } ; [-] => { $ crate :: CssSyntaxKind :: MINUS } ; [<=] => { $ crate :: CssSyntaxKind :: LTEQ } ; [>=] => { $ crate :: CssSyntaxKind :: GTEQ } ; [+=] => { $ crate :: CssSyntaxKind :: PLUSEQ } ; [|=] => { $ crate :: CssSyntaxKind :: PIPEEQ } ; [&=] => { $ crate :: CssSyntaxKind :: AMPEQ } ; [^=] => { $ crate :: CssSyntaxKind :: CARETEQ } ; [/=] => { $ crate :: CssSyntaxKind :: SLASHEQ } ; [*=] => { $ crate :: CssSyntaxKind :: STAREQ } ; [%=] => { $ crate :: CssSyntaxKind :: PERCENTEQ } ; [@] => { $ crate :: CssSyntaxKind :: AT } ; ["$="] => { $ crate :: CssSyntaxKind :: DOLLAR_EQ } ; [~=] => { $ crate :: CssSyntaxKind :: TILDE_EQ } ; [-->] => { $ crate :: CssSyntaxKind :: CDC } ; [] => { $ crate :: CssSyntaxKind :: CDC } ; [