diff --git a/crates/biome_css_analyze/src/lint/nursery/use_baseline.rs b/crates/biome_css_analyze/src/lint/nursery/use_baseline.rs index 8a3aadcbe979..ad1576a73718 100644 --- a/crates/biome_css_analyze/src/lint/nursery/use_baseline.rs +++ b/crates/biome_css_analyze/src/lint/nursery/use_baseline.rs @@ -616,10 +616,12 @@ fn at_rule_name(rule: &AnyCssAtRule) -> Option<&'static str> { | AnyCssAtRule::ScssDebugAtRule(_) | AnyCssAtRule::ScssEachAtRule(_) | AnyCssAtRule::ScssErrorAtRule(_) + | AnyCssAtRule::ScssExtendAtRule(_) | AnyCssAtRule::ScssForAtRule(_) | AnyCssAtRule::ScssForwardAtRule(_) | AnyCssAtRule::ScssFunctionAtRule(_) | AnyCssAtRule::ScssIfAtRule(_) + | AnyCssAtRule::ScssImportAtRule(_) | AnyCssAtRule::ScssIncludeAtRule(_) | AnyCssAtRule::ScssMixinAtRule(_) | AnyCssAtRule::ScssReturnAtRule(_) diff --git a/crates/biome_css_factory/src/generated/node_factory.rs b/crates/biome_css_factory/src/generated/node_factory.rs index 80b1a97912a6..ca27feef48f8 100644 --- a/crates/biome_css_factory/src/generated/node_factory.rs +++ b/crates/biome_css_factory/src/generated/node_factory.rs @@ -3220,6 +3220,54 @@ pub fn scss_expression(items: ScssExpressionItemList) -> ScssExpression { [Some(SyntaxElement::Node(items.into_syntax()))], )) } +pub fn scss_extend_at_rule( + extend_token: SyntaxToken, + css_selector_list: CssSelectorList, + semicolon_token: SyntaxToken, +) -> ScssExtendAtRuleBuilder { + ScssExtendAtRuleBuilder { + extend_token, + css_selector_list, + semicolon_token, + optional_modifier: None, + } +} +pub struct ScssExtendAtRuleBuilder { + extend_token: SyntaxToken, + css_selector_list: CssSelectorList, + semicolon_token: SyntaxToken, + optional_modifier: Option, +} +impl ScssExtendAtRuleBuilder { + pub fn with_optional_modifier(mut self, optional_modifier: ScssExtendOptionalModifier) -> Self { + self.optional_modifier = Some(optional_modifier); + self + } + pub fn build(self) -> ScssExtendAtRule { + ScssExtendAtRule::unwrap_cast(SyntaxNode::new_detached( + CssSyntaxKind::SCSS_EXTEND_AT_RULE, + [ + Some(SyntaxElement::Token(self.extend_token)), + Some(SyntaxElement::Node(self.css_selector_list.into_syntax())), + self.optional_modifier + .map(|token| SyntaxElement::Node(token.into_syntax())), + Some(SyntaxElement::Token(self.semicolon_token)), + ], + )) + } +} +pub fn scss_extend_optional_modifier( + excl_token: SyntaxToken, + optional_token: SyntaxToken, +) -> ScssExtendOptionalModifier { + ScssExtendOptionalModifier::unwrap_cast(SyntaxNode::new_detached( + CssSyntaxKind::SCSS_EXTEND_OPTIONAL_MODIFIER, + [ + Some(SyntaxElement::Token(excl_token)), + Some(SyntaxElement::Token(optional_token)), + ], + )) +} pub fn scss_for_at_rule( for_token: SyntaxToken, variable: ScssIdentifier, @@ -3401,6 +3449,20 @@ impl ScssIfAtRuleBuilder { )) } } +pub fn scss_import_at_rule( + import_token: SyntaxToken, + imports: ScssImportItemList, + semicolon_token: SyntaxToken, +) -> ScssImportAtRule { + ScssImportAtRule::unwrap_cast(SyntaxNode::new_detached( + CssSyntaxKind::SCSS_IMPORT_AT_RULE, + [ + Some(SyntaxElement::Token(import_token)), + Some(SyntaxElement::Node(imports.into_syntax())), + Some(SyntaxElement::Token(semicolon_token)), + ], + )) +} pub fn scss_include_argument_list( l_paren_token: SyntaxToken, items: CssParameterList, @@ -3712,6 +3774,55 @@ pub fn scss_parenthesized_expression( ], )) } +pub fn scss_placeholder_selector( + percent_token: SyntaxToken, + name: CssCustomIdentifier, +) -> ScssPlaceholderSelector { + ScssPlaceholderSelector::unwrap_cast(SyntaxNode::new_detached( + CssSyntaxKind::SCSS_PLACEHOLDER_SELECTOR, + [ + Some(SyntaxElement::Token(percent_token)), + Some(SyntaxElement::Node(name.into_syntax())), + ], + )) +} +pub fn scss_plain_import(url: AnyCssImportUrl, media: CssMediaQueryList) -> ScssPlainImportBuilder { + ScssPlainImportBuilder { + url, + media, + layer: None, + supports: None, + } +} +pub struct ScssPlainImportBuilder { + url: AnyCssImportUrl, + media: CssMediaQueryList, + layer: Option, + supports: Option, +} +impl ScssPlainImportBuilder { + pub fn with_layer(mut self, layer: AnyCssImportLayer) -> Self { + self.layer = Some(layer); + self + } + pub fn with_supports(mut self, supports: CssImportSupports) -> Self { + self.supports = Some(supports); + self + } + pub fn build(self) -> ScssPlainImport { + ScssPlainImport::unwrap_cast(SyntaxNode::new_detached( + CssSyntaxKind::SCSS_PLAIN_IMPORT, + [ + Some(SyntaxElement::Node(self.url.into_syntax())), + self.layer + .map(|token| SyntaxElement::Node(token.into_syntax())), + self.supports + .map(|token| SyntaxElement::Node(token.into_syntax())), + Some(SyntaxElement::Node(self.media.into_syntax())), + ], + )) + } +} pub fn scss_qualified_name( module: CssIdentifier, dot_token: SyntaxToken, @@ -4807,6 +4918,27 @@ where .map(|item| Some(item.into_syntax().into())), )) } +pub fn scss_import_item_list(items: I, separators: S) -> ScssImportItemList +where + I: IntoIterator, + I::IntoIter: ExactSizeIterator, + S: IntoIterator, + S::IntoIter: ExactSizeIterator, +{ + let mut items = items.into_iter(); + let mut separators = separators.into_iter(); + let length = items.len() + separators.len(); + ScssImportItemList::unwrap_cast(SyntaxNode::new_detached( + CssSyntaxKind::SCSS_IMPORT_ITEM_LIST, + (0..length).map(|index| { + if index % 2 == 0 { + Some(items.next()?.into_syntax().into()) + } else { + Some(separators.next()?.into()) + } + }), + )) +} pub fn scss_list_expression_element_list( items: I, separators: S, diff --git a/crates/biome_css_factory/src/generated/syntax_factory.rs b/crates/biome_css_factory/src/generated/syntax_factory.rs index 8bd4278b1976..21d30c732933 100644 --- a/crates/biome_css_factory/src/generated/syntax_factory.rs +++ b/crates/biome_css_factory/src/generated/syntax_factory.rs @@ -6623,6 +6623,72 @@ impl SyntaxFactory for CssSyntaxFactory { } slots.into_node(SCSS_EXPRESSION, children) } + SCSS_EXTEND_AT_RULE => { + 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 + && element.kind() == T![extend] + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && CssSelectorList::can_cast(element.kind()) + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && ScssExtendOptionalModifier::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 current_element.is_some() { + return RawSyntaxNode::new( + SCSS_EXTEND_AT_RULE.to_bogus(), + children.into_iter().map(Some), + ); + } + slots.into_node(SCSS_EXTEND_AT_RULE, children) + } + SCSS_EXTEND_OPTIONAL_MODIFIER => { + 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 + && element.kind() == T![!] + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && element.kind() == T![optional] + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if current_element.is_some() { + return RawSyntaxNode::new( + SCSS_EXTEND_OPTIONAL_MODIFIER.to_bogus(), + children.into_iter().map(Some), + ); + } + slots.into_node(SCSS_EXTEND_OPTIONAL_MODIFIER, children) + } SCSS_FOR_AT_RULE => { let mut elements = (&children).into_iter(); let mut slots: RawNodeSlots<7usize> = RawNodeSlots::default(); @@ -6903,6 +6969,39 @@ impl SyntaxFactory for CssSyntaxFactory { } slots.into_node(SCSS_IF_AT_RULE, children) } + SCSS_IMPORT_AT_RULE => { + let mut elements = (&children).into_iter(); + let mut slots: RawNodeSlots<3usize> = RawNodeSlots::default(); + let mut current_element = elements.next(); + if let Some(element) = ¤t_element + && element.kind() == T![import] + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && ScssImportItemList::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 current_element.is_some() { + return RawSyntaxNode::new( + SCSS_IMPORT_AT_RULE.to_bogus(), + children.into_iter().map(Some), + ); + } + slots.into_node(SCSS_IMPORT_AT_RULE, children) + } SCSS_INCLUDE_ARGUMENT_LIST => { let mut elements = (&children).into_iter(); let mut slots: RawNodeSlots<3usize> = RawNodeSlots::default(); @@ -7450,6 +7549,72 @@ impl SyntaxFactory for CssSyntaxFactory { } slots.into_node(SCSS_PARENTHESIZED_EXPRESSION, children) } + SCSS_PLACEHOLDER_SELECTOR => { + 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 + && element.kind() == T ! [%] + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && CssCustomIdentifier::can_cast(element.kind()) + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if current_element.is_some() { + return RawSyntaxNode::new( + SCSS_PLACEHOLDER_SELECTOR.to_bogus(), + children.into_iter().map(Some), + ); + } + slots.into_node(SCSS_PLACEHOLDER_SELECTOR, children) + } + SCSS_PLAIN_IMPORT => { + 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 + && AnyCssImportUrl::can_cast(element.kind()) + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && AnyCssImportLayer::can_cast(element.kind()) + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && CssImportSupports::can_cast(element.kind()) + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && CssMediaQueryList::can_cast(element.kind()) + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if current_element.is_some() { + return RawSyntaxNode::new( + SCSS_PLAIN_IMPORT.to_bogus(), + children.into_iter().map(Some), + ); + } + slots.into_node(SCSS_PLAIN_IMPORT, children) + } SCSS_QUALIFIED_NAME => { let mut elements = (&children).into_iter(); let mut slots: RawNodeSlots<3usize> = RawNodeSlots::default(); @@ -8456,6 +8621,13 @@ impl SyntaxFactory for CssSyntaxFactory { SCSS_EXPRESSION_ITEM_LIST => { Self::make_node_list_syntax(kind, children, AnyScssExpressionItem::can_cast) } + SCSS_IMPORT_ITEM_LIST => Self::make_separated_list_syntax( + kind, + children, + AnyScssImportItem::can_cast, + T ! [,], + false, + ), SCSS_LIST_EXPRESSION_ELEMENT_LIST => Self::make_separated_list_syntax( kind, children, 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 8bce1f37f1bf..409393f2b49d 100644 --- a/crates/biome_css_formatter/src/css/any/at_rule.rs +++ b/crates/biome_css_formatter/src/css/any/at_rule.rs @@ -37,10 +37,12 @@ impl FormatRule for FormatAnyCssAtRule { AnyCssAtRule::ScssDebugAtRule(node) => node.format().fmt(f), AnyCssAtRule::ScssEachAtRule(node) => node.format().fmt(f), AnyCssAtRule::ScssErrorAtRule(node) => node.format().fmt(f), + AnyCssAtRule::ScssExtendAtRule(node) => node.format().fmt(f), AnyCssAtRule::ScssForAtRule(node) => node.format().fmt(f), AnyCssAtRule::ScssForwardAtRule(node) => node.format().fmt(f), AnyCssAtRule::ScssFunctionAtRule(node) => node.format().fmt(f), AnyCssAtRule::ScssIfAtRule(node) => node.format().fmt(f), + AnyCssAtRule::ScssImportAtRule(node) => node.format().fmt(f), AnyCssAtRule::ScssIncludeAtRule(node) => node.format().fmt(f), AnyCssAtRule::ScssMixinAtRule(node) => node.format().fmt(f), AnyCssAtRule::ScssReturnAtRule(node) => node.format().fmt(f), diff --git a/crates/biome_css_formatter/src/css/any/simple_selector.rs b/crates/biome_css_formatter/src/css/any/simple_selector.rs index 14cfd1db414a..d830d24ead3b 100644 --- a/crates/biome_css_formatter/src/css/any/simple_selector.rs +++ b/crates/biome_css_formatter/src/css/any/simple_selector.rs @@ -10,6 +10,7 @@ impl FormatRule for FormatAnyCssSimpleSelector { match node { AnyCssSimpleSelector::CssTypeSelector(node) => node.format().fmt(f), AnyCssSimpleSelector::CssUniversalSelector(node) => node.format().fmt(f), + AnyCssSimpleSelector::ScssPlaceholderSelector(node) => node.format().fmt(f), } } } diff --git a/crates/biome_css_formatter/src/css/statements/import_at_rule.rs b/crates/biome_css_formatter/src/css/statements/import_at_rule.rs index 06365a4b61c4..c545a6e44c88 100644 --- a/crates/biome_css_formatter/src/css/statements/import_at_rule.rs +++ b/crates/biome_css_formatter/src/css/statements/import_at_rule.rs @@ -1,4 +1,5 @@ use crate::prelude::*; +use crate::utils::import::write_import_payload; use biome_css_syntax::{CssImportAtRule, CssImportAtRuleFields}; use biome_formatter::write; @@ -16,37 +17,7 @@ impl FormatNodeRule for FormatCssImportAtRule { } = node.as_fields(); write!(f, [import_token.format(), space()])?; - - // Determine if there are any modifiers present. - let has_any_modifiers = layer.is_some() || supports.is_some() || !media.is_empty(); - - if has_any_modifiers { - // If there are, we need to group them together and try to fill them. - let modifiers = format_once(|f| { - let separator = soft_line_break_or_space(); - let mut fill = f.fill(); - - fill.entry(&separator, &url.format()); - - if let Some(layer) = layer { - fill.entry(&separator, &layer.format()); - } - - if let Some(supports) = supports { - fill.entry(&separator, &supports.format()); - } - - if media.len() > 0 { - fill.entry(&separator, &media.format()); - } - - fill.finish() - }); - write!(f, [group(&indent(&modifiers))])?; - } else { - // If there are no modifiers, simply write the formatted `url` to the formatter `f`. - write!(f, [url.format()])?; - } + write_import_payload(f, &url, layer.as_ref(), supports.as_ref(), &media)?; write!(f, [semicolon_token.format()]) } diff --git a/crates/biome_css_formatter/src/generated.rs b/crates/biome_css_formatter/src/generated.rs index 616b92a20141..502470280168 100644 --- a/crates/biome_css_formatter/src/generated.rs +++ b/crates/biome_css_formatter/src/generated.rs @@ -7419,6 +7419,76 @@ impl IntoFormat for biome_css_syntax::ScssExpression { ) } } +impl FormatRule + for crate::scss::statements::extend_at_rule::FormatScssExtendAtRule +{ + type Context = CssFormatContext; + #[inline(always)] + fn fmt( + &self, + node: &biome_css_syntax::ScssExtendAtRule, + f: &mut CssFormatter, + ) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl AsFormat for biome_css_syntax::ScssExtendAtRule { + type Format<'a> = FormatRefWithRule< + 'a, + biome_css_syntax::ScssExtendAtRule, + crate::scss::statements::extend_at_rule::FormatScssExtendAtRule, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::scss::statements::extend_at_rule::FormatScssExtendAtRule::default(), + ) + } +} +impl IntoFormat for biome_css_syntax::ScssExtendAtRule { + type Format = FormatOwnedWithRule< + biome_css_syntax::ScssExtendAtRule, + crate::scss::statements::extend_at_rule::FormatScssExtendAtRule, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::scss::statements::extend_at_rule::FormatScssExtendAtRule::default(), + ) + } +} +impl FormatRule + for crate::scss::auxiliary::extend_optional_modifier::FormatScssExtendOptionalModifier +{ + type Context = CssFormatContext; + #[inline(always)] + fn fmt( + &self, + node: &biome_css_syntax::ScssExtendOptionalModifier, + f: &mut CssFormatter, + ) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl AsFormat for biome_css_syntax::ScssExtendOptionalModifier { + type Format<'a> = FormatRefWithRule< + 'a, + biome_css_syntax::ScssExtendOptionalModifier, + crate::scss::auxiliary::extend_optional_modifier::FormatScssExtendOptionalModifier, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule :: new (self , crate :: scss :: auxiliary :: extend_optional_modifier :: FormatScssExtendOptionalModifier :: default ()) + } +} +impl IntoFormat for biome_css_syntax::ScssExtendOptionalModifier { + type Format = FormatOwnedWithRule< + biome_css_syntax::ScssExtendOptionalModifier, + crate::scss::auxiliary::extend_optional_modifier::FormatScssExtendOptionalModifier, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule :: new (self , crate :: scss :: auxiliary :: extend_optional_modifier :: FormatScssExtendOptionalModifier :: default ()) + } +} impl FormatRule for crate::scss::statements::for_at_rule::FormatScssForAtRule { @@ -7681,6 +7751,44 @@ impl IntoFormat for biome_css_syntax::ScssIfAtRule { ) } } +impl FormatRule + for crate::scss::statements::import_at_rule::FormatScssImportAtRule +{ + type Context = CssFormatContext; + #[inline(always)] + fn fmt( + &self, + node: &biome_css_syntax::ScssImportAtRule, + f: &mut CssFormatter, + ) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl AsFormat for biome_css_syntax::ScssImportAtRule { + type Format<'a> = FormatRefWithRule< + 'a, + biome_css_syntax::ScssImportAtRule, + crate::scss::statements::import_at_rule::FormatScssImportAtRule, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::scss::statements::import_at_rule::FormatScssImportAtRule::default(), + ) + } +} +impl IntoFormat for biome_css_syntax::ScssImportAtRule { + type Format = FormatOwnedWithRule< + biome_css_syntax::ScssImportAtRule, + crate::scss::statements::import_at_rule::FormatScssImportAtRule, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::scss::statements::import_at_rule::FormatScssImportAtRule::default(), + ) + } +} impl FormatRule for crate::scss::auxiliary::include_argument_list::FormatScssIncludeArgumentList { @@ -8303,6 +8411,82 @@ impl IntoFormat for biome_css_syntax::ScssParenthesizedExpress FormatOwnedWithRule :: new (self , crate :: scss :: auxiliary :: parenthesized_expression :: FormatScssParenthesizedExpression :: default ()) } } +impl FormatRule + for crate::scss::selectors::placeholder_selector::FormatScssPlaceholderSelector +{ + type Context = CssFormatContext; + #[inline(always)] + fn fmt( + &self, + node: &biome_css_syntax::ScssPlaceholderSelector, + f: &mut CssFormatter, + ) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl AsFormat for biome_css_syntax::ScssPlaceholderSelector { + type Format<'a> = FormatRefWithRule< + 'a, + biome_css_syntax::ScssPlaceholderSelector, + crate::scss::selectors::placeholder_selector::FormatScssPlaceholderSelector, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::scss::selectors::placeholder_selector::FormatScssPlaceholderSelector::default(), + ) + } +} +impl IntoFormat for biome_css_syntax::ScssPlaceholderSelector { + type Format = FormatOwnedWithRule< + biome_css_syntax::ScssPlaceholderSelector, + crate::scss::selectors::placeholder_selector::FormatScssPlaceholderSelector, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::scss::selectors::placeholder_selector::FormatScssPlaceholderSelector::default(), + ) + } +} +impl FormatRule + for crate::scss::auxiliary::plain_import::FormatScssPlainImport +{ + type Context = CssFormatContext; + #[inline(always)] + fn fmt( + &self, + node: &biome_css_syntax::ScssPlainImport, + f: &mut CssFormatter, + ) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl AsFormat for biome_css_syntax::ScssPlainImport { + type Format<'a> = FormatRefWithRule< + 'a, + biome_css_syntax::ScssPlainImport, + crate::scss::auxiliary::plain_import::FormatScssPlainImport, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::scss::auxiliary::plain_import::FormatScssPlainImport::default(), + ) + } +} +impl IntoFormat for biome_css_syntax::ScssPlainImport { + type Format = FormatOwnedWithRule< + biome_css_syntax::ScssPlainImport, + crate::scss::auxiliary::plain_import::FormatScssPlainImport, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::scss::auxiliary::plain_import::FormatScssPlainImport::default(), + ) + } +} impl FormatRule for crate::scss::auxiliary::qualified_name::FormatScssQualifiedName { @@ -10134,6 +10318,31 @@ impl IntoFormat for biome_css_syntax::ScssExpressionItemList { ) } } +impl AsFormat for biome_css_syntax::ScssImportItemList { + type Format<'a> = FormatRefWithRule< + 'a, + biome_css_syntax::ScssImportItemList, + crate::scss::lists::import_item_list::FormatScssImportItemList, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::scss::lists::import_item_list::FormatScssImportItemList::default(), + ) + } +} +impl IntoFormat for biome_css_syntax::ScssImportItemList { + type Format = FormatOwnedWithRule< + biome_css_syntax::ScssImportItemList, + crate::scss::lists::import_item_list::FormatScssImportItemList, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::scss::lists::import_item_list::FormatScssImportItemList::default(), + ) + } +} impl AsFormat for biome_css_syntax::ScssListExpressionElementList { type Format<'a> = FormatRefWithRule< 'a, @@ -14085,6 +14294,31 @@ impl IntoFormat for biome_css_syntax::AnyScssForwardVisibility FormatOwnedWithRule :: new (self , crate :: scss :: any :: forward_visibility_clause :: FormatAnyScssForwardVisibilityClause :: default ()) } } +impl AsFormat for biome_css_syntax::AnyScssImportItem { + type Format<'a> = FormatRefWithRule< + 'a, + biome_css_syntax::AnyScssImportItem, + crate::scss::any::import_item::FormatAnyScssImportItem, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::scss::any::import_item::FormatAnyScssImportItem::default(), + ) + } +} +impl IntoFormat for biome_css_syntax::AnyScssImportItem { + type Format = FormatOwnedWithRule< + biome_css_syntax::AnyScssImportItem, + crate::scss::any::import_item::FormatAnyScssImportItem, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::scss::any::import_item::FormatAnyScssImportItem::default(), + ) + } +} impl AsFormat for biome_css_syntax::AnyScssIncludeTarget { type Format<'a> = FormatRefWithRule< 'a, diff --git a/crates/biome_css_formatter/src/scss/any/import_item.rs b/crates/biome_css_formatter/src/scss/any/import_item.rs new file mode 100644 index 000000000000..93b628772519 --- /dev/null +++ b/crates/biome_css_formatter/src/scss/any/import_item.rs @@ -0,0 +1,15 @@ +//! 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::AnyScssImportItem; +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatAnyScssImportItem; +impl FormatRule for FormatAnyScssImportItem { + type Context = CssFormatContext; + fn fmt(&self, node: &AnyScssImportItem, f: &mut CssFormatter) -> FormatResult<()> { + match node { + AnyScssImportItem::CssString(node) => node.format().fmt(f), + AnyScssImportItem::ScssPlainImport(node) => node.format().fmt(f), + } + } +} diff --git a/crates/biome_css_formatter/src/scss/any/mod.rs b/crates/biome_css_formatter/src/scss/any/mod.rs index 42898abf78c0..c05590167896 100644 --- a/crates/biome_css_formatter/src/scss/any/mod.rs +++ b/crates/biome_css_formatter/src/scss/any/mod.rs @@ -5,6 +5,7 @@ pub(crate) mod else_clause_body; pub(crate) mod expression; pub(crate) mod expression_item; pub(crate) mod forward_visibility_clause; +pub(crate) mod import_item; pub(crate) mod include_target; pub(crate) mod module_configuration; pub(crate) mod module_member; diff --git a/crates/biome_css_formatter/src/scss/auxiliary/extend_optional_modifier.rs b/crates/biome_css_formatter/src/scss/auxiliary/extend_optional_modifier.rs new file mode 100644 index 000000000000..bbbfb43ec4a4 --- /dev/null +++ b/crates/biome_css_formatter/src/scss/auxiliary/extend_optional_modifier.rs @@ -0,0 +1,20 @@ +use crate::prelude::*; +use biome_css_syntax::{ScssExtendOptionalModifier, ScssExtendOptionalModifierFields}; +use biome_formatter::write; + +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatScssExtendOptionalModifier; +impl FormatNodeRule for FormatScssExtendOptionalModifier { + fn fmt_fields( + &self, + node: &ScssExtendOptionalModifier, + f: &mut CssFormatter, + ) -> FormatResult<()> { + let ScssExtendOptionalModifierFields { + excl_token, + optional_token, + } = node.as_fields(); + + write!(f, [excl_token.format(), optional_token.format()]) + } +} diff --git a/crates/biome_css_formatter/src/scss/auxiliary/mod.rs b/crates/biome_css_formatter/src/scss/auxiliary/mod.rs index 2e5ac24a847d..5c4b8c4a2ee6 100644 --- a/crates/biome_css_formatter/src/scss/auxiliary/mod.rs +++ b/crates/biome_css_formatter/src/scss/auxiliary/mod.rs @@ -5,6 +5,7 @@ pub(crate) mod binary_expression; pub(crate) mod declaration; pub(crate) mod else_clause; pub(crate) mod expression; +pub(crate) mod extend_optional_modifier; pub(crate) mod forward_as_clause; pub(crate) mod hide_clause; pub(crate) mod include_argument_list; @@ -21,6 +22,7 @@ pub(crate) mod parameter_default_value; pub(crate) mod parameter_list; pub(crate) mod parent_selector_value; pub(crate) mod parenthesized_expression; +pub(crate) mod plain_import; pub(crate) mod qualified_name; pub(crate) mod show_clause; pub(crate) mod unary_expression; diff --git a/crates/biome_css_formatter/src/scss/auxiliary/plain_import.rs b/crates/biome_css_formatter/src/scss/auxiliary/plain_import.rs new file mode 100644 index 000000000000..3b8153cac29c --- /dev/null +++ b/crates/biome_css_formatter/src/scss/auxiliary/plain_import.rs @@ -0,0 +1,18 @@ +use crate::prelude::*; +use crate::utils::import::write_import_payload; +use biome_css_syntax::{ScssPlainImport, ScssPlainImportFields}; + +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatScssPlainImport; +impl FormatNodeRule for FormatScssPlainImport { + fn fmt_fields(&self, node: &ScssPlainImport, f: &mut CssFormatter) -> FormatResult<()> { + let ScssPlainImportFields { + url, + layer, + supports, + media, + } = node.as_fields(); + + write_import_payload(f, &url, layer.as_ref(), supports.as_ref(), &media) + } +} diff --git a/crates/biome_css_formatter/src/scss/lists/import_item_list.rs b/crates/biome_css_formatter/src/scss/lists/import_item_list.rs new file mode 100644 index 000000000000..9da2c99b3de8 --- /dev/null +++ b/crates/biome_css_formatter/src/scss/lists/import_item_list.rs @@ -0,0 +1,19 @@ +use crate::prelude::*; +use biome_css_syntax::ScssImportItemList; + +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatScssImportItemList; +impl FormatRule for FormatScssImportItemList { + type Context = CssFormatContext; + + fn fmt(&self, node: &ScssImportItemList, f: &mut CssFormatter) -> FormatResult<()> { + let separator = soft_line_break_or_space(); + let mut joiner = f.join_with(&separator); + + for formatted in node.format_separated(",") { + joiner.entry(&formatted); + } + + joiner.finish() + } +} diff --git a/crates/biome_css_formatter/src/scss/lists/mod.rs b/crates/biome_css_formatter/src/scss/lists/mod.rs index 1ee4619594cc..52e10fb2d8fb 100644 --- a/crates/biome_css_formatter/src/scss/lists/mod.rs +++ b/crates/biome_css_formatter/src/scss/lists/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod each_binding_list; pub(crate) mod expression_item_list; +pub(crate) mod import_item_list; pub(crate) mod list_expression_element_list; pub(crate) mod map_expression_pair_list; pub(crate) mod module_configuration_item_list; diff --git a/crates/biome_css_formatter/src/scss/mod.rs b/crates/biome_css_formatter/src/scss/mod.rs index 5d416ce44723..13683845ee03 100644 --- a/crates/biome_css_formatter/src/scss/mod.rs +++ b/crates/biome_css_formatter/src/scss/mod.rs @@ -3,5 +3,6 @@ pub(crate) mod any; pub(crate) mod auxiliary; pub(crate) mod lists; +pub(crate) mod selectors; pub(crate) mod statements; pub(crate) mod value; diff --git a/crates/biome_css_formatter/src/scss/selectors/mod.rs b/crates/biome_css_formatter/src/scss/selectors/mod.rs new file mode 100644 index 000000000000..927620962bc4 --- /dev/null +++ b/crates/biome_css_formatter/src/scss/selectors/mod.rs @@ -0,0 +1,3 @@ +//! This is a generated file. Don't modify it by hand! Run 'cargo codegen formatter' to re-generate the file. + +pub(crate) mod placeholder_selector; diff --git a/crates/biome_css_formatter/src/scss/selectors/placeholder_selector.rs b/crates/biome_css_formatter/src/scss/selectors/placeholder_selector.rs new file mode 100644 index 000000000000..a5a5c2e8bb23 --- /dev/null +++ b/crates/biome_css_formatter/src/scss/selectors/placeholder_selector.rs @@ -0,0 +1,16 @@ +use crate::prelude::*; +use biome_css_syntax::{ScssPlaceholderSelector, ScssPlaceholderSelectorFields}; +use biome_formatter::write; + +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatScssPlaceholderSelector; +impl FormatNodeRule for FormatScssPlaceholderSelector { + fn fmt_fields(&self, node: &ScssPlaceholderSelector, f: &mut CssFormatter) -> FormatResult<()> { + let ScssPlaceholderSelectorFields { + percent_token, + name, + } = node.as_fields(); + + write!(f, [percent_token.format(), name.format()]) + } +} diff --git a/crates/biome_css_formatter/src/scss/statements/extend_at_rule.rs b/crates/biome_css_formatter/src/scss/statements/extend_at_rule.rs new file mode 100644 index 000000000000..3ed42a9a278a --- /dev/null +++ b/crates/biome_css_formatter/src/scss/statements/extend_at_rule.rs @@ -0,0 +1,36 @@ +use crate::prelude::*; +use biome_css_syntax::{ScssExtendAtRule, ScssExtendAtRuleFields}; +use biome_formatter::write; + +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatScssExtendAtRule; +impl FormatNodeRule for FormatScssExtendAtRule { + fn fmt_fields(&self, node: &ScssExtendAtRule, f: &mut CssFormatter) -> FormatResult<()> { + let ScssExtendAtRuleFields { + extend_token, + css_selector_list, + optional_modifier, + semicolon_token, + } = node.as_fields(); + + let target = format_once(|f| { + write!(f, [group(&css_selector_list.format())])?; + + if let Some(optional_modifier) = optional_modifier { + write!(f, [space(), optional_modifier.format()])?; + } + + Ok(()) + }); + + write!( + f, + [ + extend_token.format(), + space(), + group(&target), + semicolon_token.format() + ] + ) + } +} diff --git a/crates/biome_css_formatter/src/scss/statements/import_at_rule.rs b/crates/biome_css_formatter/src/scss/statements/import_at_rule.rs new file mode 100644 index 000000000000..a8f55af0c88e --- /dev/null +++ b/crates/biome_css_formatter/src/scss/statements/import_at_rule.rs @@ -0,0 +1,26 @@ +use crate::prelude::*; +use biome_css_syntax::{ScssImportAtRule, ScssImportAtRuleFields}; +use biome_formatter::write; + +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatScssImportAtRule; + +impl FormatNodeRule for FormatScssImportAtRule { + fn fmt_fields(&self, node: &ScssImportAtRule, f: &mut CssFormatter) -> FormatResult<()> { + let ScssImportAtRuleFields { + import_token, + imports, + semicolon_token, + } = node.as_fields(); + + write!( + f, + [ + import_token.format(), + space(), + group(&indent(&imports.format())), + semicolon_token.format() + ] + ) + } +} diff --git a/crates/biome_css_formatter/src/scss/statements/mod.rs b/crates/biome_css_formatter/src/scss/statements/mod.rs index df4754e82a66..51350e56d86d 100644 --- a/crates/biome_css_formatter/src/scss/statements/mod.rs +++ b/crates/biome_css_formatter/src/scss/statements/mod.rs @@ -4,10 +4,12 @@ pub(crate) mod content_at_rule; pub(crate) mod debug_at_rule; pub(crate) mod each_at_rule; pub(crate) mod error_at_rule; +pub(crate) mod extend_at_rule; pub(crate) mod for_at_rule; pub(crate) mod forward_at_rule; pub(crate) mod function_at_rule; pub(crate) mod if_at_rule; +pub(crate) mod import_at_rule; pub(crate) mod include_at_rule; pub(crate) mod mixin_at_rule; pub(crate) mod return_at_rule; diff --git a/crates/biome_css_formatter/src/utils/import.rs b/crates/biome_css_formatter/src/utils/import.rs new file mode 100644 index 000000000000..4830c0320ed8 --- /dev/null +++ b/crates/biome_css_formatter/src/utils/import.rs @@ -0,0 +1,43 @@ +use crate::CssFormatter; +use crate::prelude::*; +use biome_css_syntax::{AnyCssImportLayer, AnyCssImportUrl, CssImportSupports, CssMediaQueryList}; +use biome_formatter::{FormatResult, write}; +use biome_rowan::SyntaxResult; + +/// Formats the shared `@import` payload used by CSS imports and SCSS plain imports. +pub(crate) fn write_import_payload( + f: &mut CssFormatter, + url: &SyntaxResult, + layer: Option<&AnyCssImportLayer>, + supports: Option<&CssImportSupports>, + media: &CssMediaQueryList, +) -> FormatResult<()> { + let has_any_modifiers = layer.is_some() || supports.is_some() || !media.is_empty(); + + if has_any_modifiers { + let modifiers = format_once(|f| { + let separator = soft_line_break_or_space(); + let mut fill = f.fill(); + + fill.entry(&separator, &url.format()); + + if let Some(layer) = layer { + fill.entry(&separator, &layer.format()); + } + + if let Some(supports) = supports { + fill.entry(&separator, &supports.format()); + } + + if !media.is_empty() { + fill.entry(&separator, &media.format()); + } + + fill.finish() + }); + + write!(f, [group(&indent(&modifiers))]) + } else { + write!(f, [url.format()]) + } +} diff --git a/crates/biome_css_formatter/src/utils/mod.rs b/crates/biome_css_formatter/src/utils/mod.rs index 6ea8c496f913..40657ef69359 100644 --- a/crates/biome_css_formatter/src/utils/mod.rs +++ b/crates/biome_css_formatter/src/utils/mod.rs @@ -1,3 +1,4 @@ pub(crate) mod block_like; pub(crate) mod component_value_list; +pub(crate) mod import; pub(crate) mod string_utils; diff --git a/crates/biome_css_formatter/tests/specs/css/scss/at-rule/extend.scss b/crates/biome_css_formatter/tests/specs/css/scss/at-rule/extend.scss new file mode 100644 index 000000000000..629dab28622a --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/scss/at-rule/extend.scss @@ -0,0 +1,16 @@ +%toolbelt{ +border:1px solid black; +} + +.action-buttons{ + @extend +%toolbelt +!optional; +} + +.alert{ + @extend +.message,.info; + @extend +a:hover; +} diff --git a/crates/biome_css_formatter/tests/specs/css/scss/at-rule/extend.scss.snap b/crates/biome_css_formatter/tests/specs/css/scss/at-rule/extend.scss.snap new file mode 100644 index 000000000000..94438273a0a0 --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/scss/at-rule/extend.scss.snap @@ -0,0 +1,58 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +info: css/scss/at-rule/extend.scss +--- + +# Input + +```scss +%toolbelt{ +border:1px solid black; +} + +.action-buttons{ + @extend +%toolbelt +!optional; +} + +.alert{ + @extend +.message,.info; + @extend +a:hover; +} + +``` + + +============================= + +# Outputs + +## Output 1 + +----- +Indent style: Tab +Indent width: 2 +Line ending: LF +Line width: 80 +Quote style: Double Quotes +Trailing newline: true +----- + +```scss +%toolbelt { + border: 1px solid black; +} + +.action-buttons { + @extend %toolbelt !optional; +} + +.alert { + @extend .message, .info; + @extend a:hover; +} + +``` diff --git a/crates/biome_css_formatter/tests/specs/css/scss/at-rule/import.scss b/crates/biome_css_formatter/tests/specs/css/scss/at-rule/import.scss new file mode 100644 index 000000000000..91d656f70bb5 --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/scss/at-rule/import.scss @@ -0,0 +1,22 @@ +@import"theme"; + +@import "rounded-corners", +"text-shadow"; + +@import +"theme.scss" +, +"spacing"; + +@import "theme.css"print; + +@import "print.css" +print, +"spacing"; + +@import "print.css" +print +, +screen; + +@import url("theme.css"); diff --git a/crates/biome_css_formatter/tests/specs/css/scss/at-rule/import.scss.snap b/crates/biome_css_formatter/tests/specs/css/scss/at-rule/import.scss.snap new file mode 100644 index 000000000000..893f97798ea6 --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/scss/at-rule/import.scss.snap @@ -0,0 +1,66 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +assertion_line: 212 +info: css/scss/at-rule/import.scss +--- + +# Input + +```scss +@import"theme"; + +@import "rounded-corners", +"text-shadow"; + +@import +"theme.scss" +, +"spacing"; + +@import "theme.css"print; + +@import "print.css" +print, +"spacing"; + +@import "print.css" +print +, +screen; + +@import url("theme.css"); + +``` + + +============================= + +# Outputs + +## Output 1 + +----- +Indent style: Tab +Indent width: 2 +Line ending: LF +Line width: 80 +Quote style: Double Quotes +Trailing newline: true +----- + +```scss +@import "theme"; + +@import "rounded-corners", "text-shadow"; + +@import "theme.scss", "spacing"; + +@import "theme.css" print; + +@import "print.css" print, "spacing"; + +@import "print.css" print, screen; + +@import url("theme.css"); + +``` diff --git a/crates/biome_css_parser/src/lexer/mod.rs b/crates/biome_css_parser/src/lexer/mod.rs index df4a5a491347..d8676bd1c37b 100644 --- a/crates/biome_css_parser/src/lexer/mod.rs +++ b/crates/biome_css_parser/src/lexer/mod.rs @@ -851,11 +851,13 @@ impl<'src> CssLexer<'src> { b"warn" => WARN_KW, b"error" => ERROR_KW, b"content" => CONTENT_KW, + b"extend" => EXTEND_KW, b"for" => FOR_KW, b"forward" => FORWARD_KW, b"hide" => HIDE_KW, b"include" => INCLUDE_KW, b"mixin" => MIXIN_KW, + b"optional" => OPTIONAL_KW, b"while" => WHILE_KW, b"show" => SHOW_KW, b"sass" => SASS_KW, diff --git a/crates/biome_css_parser/src/syntax/at_rule/import.rs b/crates/biome_css_parser/src/syntax/at_rule/import.rs index 54aa8ee6bbe1..fd15e591dd8f 100644 --- a/crates/biome_css_parser/src/syntax/at_rule/import.rs +++ b/crates/biome_css_parser/src/syntax/at_rule/import.rs @@ -6,7 +6,7 @@ use crate::syntax::at_rule::supports::{ is_at_supports_property, parse_any_supports_condition, parse_supports_declaration, }; use crate::syntax::util::skip_possible_tailwind_syntax; -use crate::syntax::value::url::{is_at_url_function, parse_url_function}; +use crate::syntax::value::url::{is_at_url_function, is_nth_at_url_function, parse_url_function}; use crate::syntax::{is_at_string, parse_string}; use biome_css_syntax::CssSyntaxKind::*; use biome_css_syntax::{CssSyntaxKind, T}; @@ -47,27 +47,7 @@ pub(crate) fn parse_import_at_rule(p: &mut CssParser) -> ParsedSyntax { CSS_BOGUS_AT_RULE }; - skip_possible_tailwind_syntax(p); - - // An optional cascade layer name, or for an anonymous layer. - if is_at_import_named_layer(p) { - parse_import_named_layer(p).ok(); - } else if is_at_import_anonymous_layer(p) { - parse_import_anonymous_layer(p).ok(); - } - - skip_possible_tailwind_syntax(p); - - if is_at_import_supports(p) { - // An optional supports condition, we don't have an error here - // is_at_import_supports validates the supports condition - parse_import_supports(p).ok(); - } - - skip_possible_tailwind_syntax(p); - - MediaQueryList::new(T![;]).parse_list(p); - + parse_import_modifiers(p, T![;]); p.expect(T![;]); Present(m.complete(p, kind)) @@ -83,6 +63,12 @@ pub(crate) fn is_at_import_url(p: &mut CssParser) -> bool { is_at_url_function(p) || is_at_string(p) } +/// Checks if the token at offset `n` begins a URL or string import target. +#[inline] +pub(crate) fn is_nth_at_import_url(p: &mut CssParser, n: usize) -> bool { + p.nth_at(n, CSS_STRING_LITERAL) || is_nth_at_url_function(p, n) +} + /// Parses the URL component of an `@import` rule in CSS. /// /// This function checks if the current token is a valid URL or string format for an `@import` rule. @@ -100,6 +86,53 @@ pub(crate) fn parse_import_url(p: &mut CssParser) -> ParsedSyntax { } } +/// Parses the optional modifier clauses that may follow a CSS import URL. +/// +/// This includes cascade layers, `supports(...)`, and trailing media queries. +/// +/// # Example +/// +/// ```css +/// @import "theme.css" layer(base) supports(display: grid) screen; +/// ``` +#[inline] +pub(crate) fn parse_import_modifiers(p: &mut CssParser, end_kind: CssSyntaxKind) { + parse_import_non_media_modifiers(p); + MediaQueryList::new(end_kind).parse_list(p); +} + +/// Parses the shared non-media modifiers that may follow an import URL. +/// +/// Both CSS `@import` rules and SCSS plain import items allow optional cascade +/// layer clauses and `supports(...)` clauses before any trailing media queries. +/// +/// # Example +/// +/// ```css +/// @import "theme.css" layer(base) supports(display: grid); +/// ``` +#[inline] +pub(crate) fn parse_import_non_media_modifiers(p: &mut CssParser) { + skip_possible_tailwind_syntax(p); + + // An optional cascade layer name, or for an anonymous layer. + if is_at_import_named_layer(p) { + parse_import_named_layer(p).ok(); + } else if is_at_import_anonymous_layer(p) { + parse_import_anonymous_layer(p).ok(); + } + + skip_possible_tailwind_syntax(p); + + if is_at_import_supports(p) { + // An optional supports condition, we don't have an error here + // is_at_import_supports validates the supports condition + parse_import_supports(p).ok(); + } + + skip_possible_tailwind_syntax(p); +} + /// Determines if the current parsing position is at an anonymous layer within an `@import` rule. /// /// This function checks whether the current token in the `CssParser` matches the `layer` token, diff --git a/crates/biome_css_parser/src/syntax/at_rule/media.rs b/crates/biome_css_parser/src/syntax/at_rule/media.rs index b77a5757f41a..2fea0cabdbad 100644 --- a/crates/biome_css_parser/src/syntax/at_rule/media.rs +++ b/crates/biome_css_parser/src/syntax/at_rule/media.rs @@ -96,7 +96,12 @@ impl ParseSeparatedList for MediaQueryList { } #[inline] -fn parse_any_media_query(p: &mut CssParser) -> ParsedSyntax { +pub(crate) fn is_at_any_media_query(p: &mut CssParser) -> bool { + is_at_media_type_query(p) || is_at_metavariable(p) || is_at_any_media_condition(p) +} + +#[inline] +pub(crate) fn parse_any_media_query(p: &mut CssParser) -> ParsedSyntax { if is_at_media_type_query(p) { parse_any_media_type_query(p) } else if is_at_metavariable(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 fb9e55d7948b..50f03715fa3a 100644 --- a/crates/biome_css_parser/src/syntax/at_rule/mod.rs +++ b/crates/biome_css_parser/src/syntax/at_rule/mod.rs @@ -76,10 +76,11 @@ use crate::syntax::CssSyntaxFeatures; use crate::syntax::parse_error::{expected_any_at_rule, tailwind_disabled}; use crate::syntax::scss::{ parse_bogus_scss_else_at_rule, parse_scss_content_at_rule, parse_scss_debug_at_rule, - parse_scss_each_at_rule, parse_scss_error_at_rule, parse_scss_for_at_rule, - parse_scss_forward_at_rule, parse_scss_function_at_rule, parse_scss_if_at_rule, - parse_scss_include_at_rule, parse_scss_mixin_at_rule, parse_scss_return_at_rule, - parse_scss_use_at_rule, parse_scss_warn_at_rule, parse_scss_while_at_rule, + parse_scss_each_at_rule, parse_scss_error_at_rule, parse_scss_extend_at_rule, + parse_scss_for_at_rule, parse_scss_forward_at_rule, parse_scss_function_at_rule, + parse_scss_if_at_rule, parse_scss_import_at_rule, parse_scss_include_at_rule, + parse_scss_mixin_at_rule, parse_scss_return_at_rule, parse_scss_use_at_rule, + parse_scss_warn_at_rule, parse_scss_while_at_rule, }; use biome_css_syntax::CssSyntaxKind::*; use biome_css_syntax::T; @@ -87,6 +88,11 @@ use biome_css_syntax::T; use biome_parser::prelude::ParsedSyntax::{Absent, Present}; use biome_parser::prelude::*; +pub(crate) use import::{ + is_at_import_url, is_nth_at_import_url, parse_import_non_media_modifiers, parse_import_url, +}; +pub(crate) use parse_error::expected_media_query; + #[inline] pub(crate) fn is_at_at_rule(p: &mut CssParser) -> bool { p.at(T![@]) @@ -139,7 +145,9 @@ pub(crate) fn parse_any_at_rule(p: &mut CssParser) -> ParsedSyntax { T![layer] => parse_layer_at_rule(p), T![scope] => parse_scope_at_rule(p), T![supports] => parse_supports_at_rule(p), - T![import] => parse_import_at_rule(p), + T![import] => CssSyntaxFeatures::Scss + .parse_supported_syntax(p, parse_scss_import_at_rule) + .or_else(|| parse_import_at_rule(p)), T![namespace] => parse_namespace_at_rule(p), T![starting_style] => parse_starting_style_at_rule(p), T![document] => parse_document_at_rule(p), @@ -150,6 +158,9 @@ pub(crate) fn parse_any_at_rule(p: &mut CssParser) -> ParsedSyntax { T![each] => CssSyntaxFeatures::Scss .parse_supported_syntax(p, parse_scss_each_at_rule) .or_else(|| parse_unknown_at_rule(p)), + T![extend] => CssSyntaxFeatures::Scss + .parse_supported_syntax(p, parse_scss_extend_at_rule) + .or_else(|| parse_unknown_at_rule(p)), T![for] => CssSyntaxFeatures::Scss .parse_supported_syntax(p, parse_scss_for_at_rule) .or_else(|| parse_unknown_at_rule(p)), diff --git a/crates/biome_css_parser/src/syntax/scss/at_rule/extend_at_rule.rs b/crates/biome_css_parser/src/syntax/scss/at_rule/extend_at_rule.rs new file mode 100644 index 000000000000..64afea97c035 --- /dev/null +++ b/crates/biome_css_parser/src/syntax/scss/at_rule/extend_at_rule.rs @@ -0,0 +1,74 @@ +use crate::parser::CssParser; +use crate::syntax::selector::SelectorList; +use biome_css_syntax::CssSyntaxKind::{self, SCSS_EXTEND_AT_RULE, SCSS_EXTEND_OPTIONAL_MODIFIER}; +use biome_css_syntax::T; +use biome_parser::parse_lists::ParseSeparatedList; +use biome_parser::prelude::ParsedSyntax::{Absent, Present}; +use biome_parser::prelude::*; +use biome_parser::{TokenSet, token_set}; + +const SCSS_EXTEND_SELECTOR_LIST_END_SET: TokenSet = + token_set![T![!], T![;], T!['{'], T!['}']]; + +/// Parses the SCSS `@extend` at-rule. +/// +/// # Example +/// +/// ```scss +/// @extend %toolbelt !optional; +/// @extend .message; +/// ``` +/// +/// Docs: https://sass-lang.com/documentation/at-rules/extend/ +#[inline] +pub(crate) fn parse_scss_extend_at_rule(p: &mut CssParser) -> ParsedSyntax { + if !is_at_scss_extend_at_rule(p) { + return Absent; + } + + let m = p.start(); + + p.bump(T![extend]); + + parse_scss_extend_selector_list(p).ok(); + + // The `!optional` modifier is optional in the grammar, so `Absent` is valid here. + parse_scss_extend_optional_modifier(p).ok(); + p.expect(T![;]); + + Present(m.complete(p, SCSS_EXTEND_AT_RULE)) +} + +#[inline] +fn is_at_scss_extend_at_rule(p: &mut CssParser) -> bool { + p.at(T![extend]) +} + +#[inline] +fn parse_scss_extend_selector_list(p: &mut CssParser) -> ParsedSyntax { + Present( + SelectorList::default() + .with_end_kind_ts(SCSS_EXTEND_SELECTOR_LIST_END_SET) + .with_recovery_ts(SCSS_EXTEND_SELECTOR_LIST_END_SET) + .parse_list(p), + ) +} + +#[inline] +fn parse_scss_extend_optional_modifier(p: &mut CssParser) -> ParsedSyntax { + if !is_at_scss_extend_optional_modifier(p) { + return Absent; + } + + let m = p.start(); + + p.bump(T![!]); + p.expect(T![optional]); + + Present(m.complete(p, SCSS_EXTEND_OPTIONAL_MODIFIER)) +} + +#[inline] +fn is_at_scss_extend_optional_modifier(p: &mut CssParser) -> bool { + p.at(T![!]) +} diff --git a/crates/biome_css_parser/src/syntax/scss/at_rule/import_at_rule.rs b/crates/biome_css_parser/src/syntax/scss/at_rule/import_at_rule.rs new file mode 100644 index 000000000000..aeec0879c31f --- /dev/null +++ b/crates/biome_css_parser/src/syntax/scss/at_rule/import_at_rule.rs @@ -0,0 +1,232 @@ +use crate::parser::CssParser; +use crate::syntax::at_rule::media::{is_at_any_media_query, parse_any_media_query}; +use crate::syntax::at_rule::{ + expected_media_query, is_at_import_url, is_nth_at_import_url, parse_import_non_media_modifiers, + parse_import_url, +}; +use crate::syntax::parse_error::expected_string; +use crate::syntax::value::url::is_at_url_function; +use crate::syntax::{is_at_string, parse_string}; +use biome_css_syntax::CssSyntaxKind::*; +use biome_css_syntax::{CssSyntaxKind, T}; +use biome_parser::parse_lists::ParseSeparatedList; +use biome_parser::parse_recovery::{ParseRecoveryTokenSet, RecoveryResult}; +use biome_parser::parsed_syntax::ParsedSyntax::Present; +use biome_parser::prelude::ParsedSyntax::Absent; +use biome_parser::prelude::*; +use biome_parser::{TokenSet, token_set}; + +const SCSS_IMPORT_ITEM_LIST_END_SET: TokenSet = token_set![T![;]]; +const SCSS_IMPORT_ITEM_LIST_RECOVERY_SET: TokenSet = token_set![T![,], T![;]]; + +#[inline] +pub(crate) fn is_at_scss_import_at_rule(p: &mut CssParser) -> bool { + p.at(T![import]) +} + +/// Parses the SCSS `@import` at-rule. +/// +/// # Example +/// +/// ```scss +/// @import "theme", "rounded-corners"; +/// ``` +#[inline] +pub(crate) fn parse_scss_import_at_rule(p: &mut CssParser) -> ParsedSyntax { + if !is_at_scss_import_at_rule(p) { + return Absent; + } + + let m = p.start(); + + p.bump(T![import]); + parse_scss_import_item_list(p); + p.expect(T![;]); + + Present(m.complete(p, SCSS_IMPORT_AT_RULE)) +} + +/// Parses the comma-separated SCSS `@import` item list. +/// +/// # Example +/// +/// ```scss +/// @import "theme", "rounded-corners"; +/// ``` +#[inline] +fn parse_scss_import_item_list(p: &mut CssParser) -> CompletedMarker { + ScssImportItemList.parse_list(p) +} + +/// Parses a quoted SCSS `@import` item and classifies it after consuming the string. +/// +/// A quoted import becomes a plain-CSS import if its target is classified as +/// plain CSS or if the following token starts CSS import modifiers such as +/// `layer(...)`, `supports(...)`, or media queries. +#[inline] +fn parse_scss_string_import_item(p: &mut CssParser) -> ParsedSyntax { + if !is_at_string(p) { + return Absent; + } + + let is_plain_target = is_at_plain_css_import_target(p.cur_text()); + // Guarded by `is_at_string` above. + let Some(import_string) = parse_string(p).ok() else { + return Absent; + }; + + if is_plain_target || is_at_import_modifier(p) { + let m = import_string.precede(p); + parse_scss_plain_import_modifiers(p); + Present(m.complete(p, SCSS_PLAIN_IMPORT)) + } else { + Present(import_string) + } +} + +/// Parses a single plain-CSS import item inside an SCSS `@import` list. +/// +/// # Example +/// +/// ```scss +/// @import "theme.css", url("fonts.css"); +/// ``` +#[inline] +fn parse_scss_plain_import(p: &mut CssParser) -> ParsedSyntax { + if !is_at_import_url(p) { + return Absent; + } + + let m = p.start(); + parse_import_url(p).ok(); + parse_scss_plain_import_modifiers(p); + + Present(m.complete(p, SCSS_PLAIN_IMPORT)) +} + +/// Parses the optional modifier clauses for a plain-CSS SCSS import item. +/// +/// This mirrors CSS `@import` modifier parsing, but the media-query list stops +/// at a comma when that comma begins the next SCSS import item. +#[inline] +fn parse_scss_plain_import_modifiers(p: &mut CssParser) { + parse_import_non_media_modifiers(p); + ScssImportMediaQueryList.parse_list(p); +} + +/// Returns `true` when the current token begins CSS import modifiers. +#[inline] +fn is_at_import_modifier(p: &mut CssParser) -> bool { + p.at(T![layer]) || p.at(T![supports]) || is_at_any_media_query(p) +} + +/// Returns `true` when a quoted SCSS import target is classified as plain CSS. +/// +/// This mirrors Sass's plain-CSS import classification for quoted URLs: +/// absolute HTTP(S) URLs, protocol-relative URLs, and paths ending in `.css` +/// are treated as CSS imports rather than Sass module imports. +/// +/// Docs: https://sass-lang.com/documentation/at-rules/import/#plain-css-imports +#[inline] +fn is_at_plain_css_import_target(text: &str) -> bool { + let Some(unquoted) = text + .strip_prefix('"') + .and_then(|text| text.strip_suffix('"')) + .or_else(|| { + text.strip_prefix('\'') + .and_then(|text| text.strip_suffix('\'')) + }) + else { + return false; + }; + + if unquoted.starts_with("http://") + || unquoted.starts_with("https://") + || unquoted.starts_with("//") + { + return true; + } + + unquoted.ends_with(".css") +} + +struct ScssImportItemList; + +impl ParseSeparatedList for ScssImportItemList { + type Kind = CssSyntaxKind; + type Parser<'source> = CssParser<'source>; + const LIST_KIND: Self::Kind = SCSS_IMPORT_ITEM_LIST; + + fn parse_element(&mut self, p: &mut Self::Parser<'_>) -> ParsedSyntax { + parse_scss_import_item(p) + } + + fn is_at_list_end(&self, p: &mut Self::Parser<'_>) -> bool { + p.at_ts(SCSS_IMPORT_ITEM_LIST_END_SET) + } + + fn recover( + &mut self, + p: &mut Self::Parser<'_>, + parsed_element: ParsedSyntax, + ) -> RecoveryResult { + parsed_element.or_recover_with_token_set( + p, + &ParseRecoveryTokenSet::new(CSS_BOGUS, SCSS_IMPORT_ITEM_LIST_RECOVERY_SET), + expected_string, + ) + } + + fn allow_empty(&self) -> bool { + false + } + + fn separating_element_kind(&mut self) -> Self::Kind { + T![,] + } +} + +#[inline] +fn parse_scss_import_item(p: &mut CssParser) -> ParsedSyntax { + if !is_at_import_url(p) { + return Absent; + } + + if is_at_url_function(p) { + parse_scss_plain_import(p) + } else { + parse_scss_string_import_item(p) + } +} + +struct ScssImportMediaQueryList; + +impl ParseSeparatedList for ScssImportMediaQueryList { + type Kind = CssSyntaxKind; + type Parser<'source> = CssParser<'source>; + const LIST_KIND: Self::Kind = CSS_MEDIA_QUERY_LIST; + + fn parse_element(&mut self, p: &mut Self::Parser<'_>) -> ParsedSyntax { + parse_any_media_query(p) + } + + fn is_at_list_end(&self, p: &mut Self::Parser<'_>) -> bool { + p.at(T![;]) || (p.at(T![,]) && is_nth_at_import_url(p, 1)) + } + + fn recover( + &mut self, + p: &mut Self::Parser<'_>, + parsed_element: ParsedSyntax, + ) -> RecoveryResult { + parsed_element.or_recover_with_token_set( + p, + &ParseRecoveryTokenSet::new(CSS_BOGUS_MEDIA_QUERY, token_set![T![,], T![;]]), + expected_media_query, + ) + } + + fn separating_element_kind(&mut self) -> Self::Kind { + T![,] + } +} diff --git a/crates/biome_css_parser/src/syntax/scss/at_rule/mod.rs b/crates/biome_css_parser/src/syntax/scss/at_rule/mod.rs index 9ff9ba673c2b..781bb171f3f3 100644 --- a/crates/biome_css_parser/src/syntax/scss/at_rule/mod.rs +++ b/crates/biome_css_parser/src/syntax/scss/at_rule/mod.rs @@ -3,10 +3,12 @@ mod debug; mod each_at_rule; mod else_clause; mod error; +mod extend_at_rule; mod for_at_rule; mod forward_at_rule; mod function_at_rule; mod if_at_rule; +mod import_at_rule; mod include_at_rule; mod mixin_at_rule; mod module_clauses; @@ -28,10 +30,12 @@ pub(crate) use debug::parse_scss_debug_at_rule; pub(crate) use each_at_rule::parse_scss_each_at_rule; pub(crate) use else_clause::parse_bogus_scss_else_at_rule; pub(crate) use error::parse_scss_error_at_rule; +pub(crate) use extend_at_rule::parse_scss_extend_at_rule; pub(crate) use for_at_rule::parse_scss_for_at_rule; pub(crate) use forward_at_rule::parse_scss_forward_at_rule; pub(crate) use function_at_rule::parse_scss_function_at_rule; pub(crate) use if_at_rule::parse_scss_if_at_rule; +pub(crate) use import_at_rule::parse_scss_import_at_rule; pub(crate) use include_at_rule::parse_scss_include_at_rule; pub(crate) use mixin_at_rule::parse_scss_mixin_at_rule; pub(crate) use return_at_rule::parse_scss_return_at_rule; diff --git a/crates/biome_css_parser/src/syntax/scss/mod.rs b/crates/biome_css_parser/src/syntax/scss/mod.rs index 3b4848c82ec0..80223579e0d6 100644 --- a/crates/biome_css_parser/src/syntax/scss/mod.rs +++ b/crates/biome_css_parser/src/syntax/scss/mod.rs @@ -4,15 +4,17 @@ mod expression; mod function_name; mod identifiers; mod parse_error; +mod selector; mod token_sets; mod value; pub(crate) use at_rule::{ parse_bogus_scss_else_at_rule, parse_scss_content_at_rule, parse_scss_debug_at_rule, - parse_scss_each_at_rule, parse_scss_error_at_rule, parse_scss_for_at_rule, - parse_scss_forward_at_rule, parse_scss_function_at_rule, parse_scss_if_at_rule, - parse_scss_include_at_rule, parse_scss_mixin_at_rule, parse_scss_return_at_rule, - parse_scss_use_at_rule, parse_scss_warn_at_rule, parse_scss_while_at_rule, + parse_scss_each_at_rule, parse_scss_error_at_rule, parse_scss_extend_at_rule, + parse_scss_for_at_rule, parse_scss_forward_at_rule, parse_scss_function_at_rule, + parse_scss_if_at_rule, parse_scss_import_at_rule, parse_scss_include_at_rule, + parse_scss_mixin_at_rule, parse_scss_return_at_rule, parse_scss_use_at_rule, + parse_scss_warn_at_rule, parse_scss_while_at_rule, }; pub(crate) use declaration::{ is_at_scss_declaration, is_at_scss_nesting_declaration, is_at_scss_variable_modifier_start, @@ -32,6 +34,7 @@ pub(crate) use identifiers::{ pub(crate) use parse_error::{ expected_scss_expression, expected_scss_variable_modifier, scss_ellipsis_not_allowed, }; +pub(crate) use selector::{is_nth_at_scss_placeholder_selector, parse_scss_placeholder_selector}; pub(crate) use token_sets::{ END_OF_SCSS_EXPRESSION_TOKEN_SET, SCSS_IDENT_CONTINUATION_SET, SCSS_NESTING_VALUE_END_SET, SCSS_STATEMENT_START_SET, SCSS_VARIABLE_MODIFIER_LIST_END_SET, diff --git a/crates/biome_css_parser/src/syntax/scss/selector.rs b/crates/biome_css_parser/src/syntax/scss/selector.rs new file mode 100644 index 000000000000..8b5e8dfba4f5 --- /dev/null +++ b/crates/biome_css_parser/src/syntax/scss/selector.rs @@ -0,0 +1,29 @@ +use crate::parser::CssParser; +use crate::syntax::parse_error::expected_identifier; +use crate::syntax::selector::parse_selector_custom_identifier; +use crate::syntax::{CssSyntaxFeatures, is_nth_at_identifier}; +use biome_css_syntax::CssSyntaxKind::SCSS_PLACEHOLDER_SELECTOR; +use biome_css_syntax::T; +use biome_parser::SyntaxFeature; +use biome_parser::prelude::ParsedSyntax::{Absent, Present}; +use biome_parser::prelude::*; + +#[inline] +pub(crate) fn is_nth_at_scss_placeholder_selector(p: &mut CssParser, n: usize) -> bool { + CssSyntaxFeatures::Scss.is_supported(p) && p.nth_at(n, T![%]) && is_nth_at_identifier(p, n + 1) +} + +/// Parses an SCSS placeholder selector such as `%toolbelt`. +#[inline] +pub(crate) fn parse_scss_placeholder_selector(p: &mut CssParser) -> ParsedSyntax { + if !is_nth_at_scss_placeholder_selector(p, 0) { + return Absent; + } + + let m = p.start(); + + p.bump(T![%]); + parse_selector_custom_identifier(p).or_add_diagnostic(p, expected_identifier); + + Present(m.complete(p, SCSS_PLACEHOLDER_SELECTOR)) +} diff --git a/crates/biome_css_parser/src/syntax/selector/mod.rs b/crates/biome_css_parser/src/syntax/selector/mod.rs index da6ab27e2894..946a9674e489 100644 --- a/crates/biome_css_parser/src/syntax/selector/mod.rs +++ b/crates/biome_css_parser/src/syntax/selector/mod.rs @@ -4,11 +4,13 @@ mod pseudo_class; mod pseudo_element; pub(crate) mod relative_selector; +use super::{is_nth_at_metavariable, parse_metavariable}; use crate::lexer::CssLexContext; use crate::parser::CssParser; use crate::syntax::parse_error::{ expected_any_sub_selector, expected_compound_selector, expected_identifier, expected_selector, }; +use crate::syntax::scss::{is_nth_at_scss_placeholder_selector, parse_scss_placeholder_selector}; use crate::syntax::selector::attribute::parse_attribute_selector; use crate::syntax::selector::nested_selector::NestedSelectorList; use crate::syntax::selector::pseudo_class::parse_pseudo_class_selector; @@ -28,8 +30,6 @@ use biome_parser::prelude::ParsedSyntax; use biome_parser::prelude::ParsedSyntax::{Absent, Present}; use biome_parser::{CompletedMarker, Parser, ParserProgress, TokenSet, token_set}; -use super::{is_nth_at_metavariable, parse_metavariable}; - /// Determines the lexical context for parsing CSS selectors. /// /// This function is applied when lexing CSS selectors. It decides whether the @@ -37,7 +37,7 @@ use super::{is_nth_at_metavariable, parse_metavariable}; /// context. The distinction is important for handling whitespaces, especially /// around combinators in CSS selectors. const SELECTOR_LEX_SET: TokenSet = - COMPLEX_SELECTOR_COMBINATOR_SET.union(token_set![T!['{'], T![,], T![')']]); + COMPLEX_SELECTOR_COMBINATOR_SET.union(token_set![T!['{'], T![,], T![')'], T![!], T![;], EOF]); #[inline] fn selector_lex_context(p: &mut CssParser) -> CssLexContext { // It's an inverted logic for `is_nth_at_selector(p, 1)`. @@ -107,7 +107,7 @@ impl ParseSeparatedList for SelectorList { } fn is_at_list_end(&self, p: &mut Self::Parser<'_>) -> bool { - p.at_ts(self.end_kind_ts) + p.at_ts(self.end_kind_ts) || p.at(CSS_SPACE_LITERAL) && p.nth_at_ts(1, self.end_kind_ts) } fn recover( @@ -306,7 +306,10 @@ fn parse_compound_selector(p: &mut CssParser) -> ParsedSyntax { /// including type selectors, universal selectors, and attribute selectors. #[inline] fn is_nth_at_simple_selector(p: &mut CssParser, n: usize) -> bool { - is_nth_at_namespace(p, n) || p.nth_at(n, T![*]) || is_nth_at_identifier(p, n) + is_nth_at_namespace(p, n) + || p.nth_at(n, T![*]) + || is_nth_at_identifier(p, n) + || is_nth_at_scss_placeholder_selector(p, n) } /// Parses a simple selector in CSS. @@ -320,6 +323,10 @@ fn parse_simple_selector(p: &mut CssParser) -> ParsedSyntax { return Absent; } + if is_nth_at_scss_placeholder_selector(p, 0) { + return parse_scss_placeholder_selector(p); + } + let namespace = parse_namespace(p); if p.at(T![*]) { @@ -528,7 +535,7 @@ fn parse_selector_identifier(p: &mut CssParser) -> ParsedSyntax { /// case-sensitive. These are distinguished from regular identifiers in /// selectors that are case-insensitive for safety in preserving the casing. #[inline] -fn parse_selector_custom_identifier(p: &mut CssParser) -> ParsedSyntax { +pub(crate) fn parse_selector_custom_identifier(p: &mut CssParser) -> ParsedSyntax { let context = selector_lex_context(p); // Class and ID selectors are technically `` _and_ case-sensitive. // To handle this, we use `` instead, but also have to allow diff --git a/crates/biome_css_parser/src/syntax/value/url.rs b/crates/biome_css_parser/src/syntax/value/url.rs index 3250c43f3511..04f28d638510 100644 --- a/crates/biome_css_parser/src/syntax/value/url.rs +++ b/crates/biome_css_parser/src/syntax/value/url.rs @@ -20,7 +20,13 @@ const URL_SET: TokenSet = token_set![T![url], T![src]]; /// Determines if the current position of the parser is at the beginning of a URL function. pub(crate) fn is_at_url_function(p: &mut CssParser) -> bool { - p.at_ts(URL_SET) && p.nth_at(1, T!['(']) + is_nth_at_url_function(p, 0) +} + +/// Determines if the token at offset `n` begins a URL function. +#[inline] +pub(crate) fn is_nth_at_url_function(p: &mut CssParser, n: usize) -> bool { + p.nth_at_ts(n, URL_SET) && p.nth_at(n + 1, T!['(']) } /// Parses a URL function from the current position of the CSS parser. diff --git a/crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/extend.scss b/crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/extend.scss new file mode 100644 index 000000000000..be6f50818c5f --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/extend.scss @@ -0,0 +1,4 @@ +@extend; +@extend !optional; +@extend .message !; +@extend .message diff --git a/crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/extend.scss.snap b/crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/extend.scss.snap new file mode 100644 index 000000000000..dfae8ed443de --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/extend.scss.snap @@ -0,0 +1,187 @@ +--- +source: crates/biome_css_parser/tests/spec_test.rs +assertion_line: 208 +expression: snapshot +--- + +## Input + +```css +@extend; +@extend !optional; +@extend .message !; +@extend .message + +``` + + +## AST + +``` +CssRoot { + bom_token: missing (optional), + items: CssRootItemList [ + CssAtRule { + at_token: AT@0..1 "@" [] [], + rule: ScssExtendAtRule { + extend_token: EXTEND_KW@1..7 "extend" [] [], + css_selector_list: CssSelectorList [], + optional_modifier: missing (optional), + semicolon_token: SEMICOLON@7..8 ";" [] [], + }, + }, + CssAtRule { + at_token: AT@8..10 "@" [Newline("\n")] [], + rule: ScssExtendAtRule { + extend_token: EXTEND_KW@10..17 "extend" [] [Whitespace(" ")], + css_selector_list: CssSelectorList [], + optional_modifier: ScssExtendOptionalModifier { + excl_token: BANG@17..18 "!" [] [], + optional_token: OPTIONAL_KW@18..26 "optional" [] [], + }, + semicolon_token: SEMICOLON@26..27 ";" [] [], + }, + }, + CssAtRule { + at_token: AT@27..29 "@" [Newline("\n")] [], + rule: ScssExtendAtRule { + extend_token: EXTEND_KW@29..36 "extend" [] [Whitespace(" ")], + css_selector_list: CssSelectorList [ + CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@36..37 "." [] [], + name: CssCustomIdentifier { + value_token: IDENT@37..45 "message" [] [Whitespace(" ")], + }, + }, + ], + }, + ], + optional_modifier: ScssExtendOptionalModifier { + excl_token: BANG@45..46 "!" [] [], + optional_token: missing (required), + }, + semicolon_token: SEMICOLON@46..47 ";" [] [], + }, + }, + CssAtRule { + at_token: AT@47..49 "@" [Newline("\n")] [], + rule: ScssExtendAtRule { + extend_token: EXTEND_KW@49..56 "extend" [] [Whitespace(" ")], + css_selector_list: CssSelectorList [ + CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@56..57 "." [] [], + name: CssCustomIdentifier { + value_token: IDENT@57..64 "message" [] [], + }, + }, + ], + }, + ], + optional_modifier: missing (optional), + semicolon_token: missing (required), + }, + }, + ], + eof_token: EOF@64..65 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: CSS_ROOT@0..65 + 0: (empty) + 1: CSS_ROOT_ITEM_LIST@0..64 + 0: CSS_AT_RULE@0..8 + 0: AT@0..1 "@" [] [] + 1: SCSS_EXTEND_AT_RULE@1..8 + 0: EXTEND_KW@1..7 "extend" [] [] + 1: CSS_SELECTOR_LIST@7..7 + 2: (empty) + 3: SEMICOLON@7..8 ";" [] [] + 1: CSS_AT_RULE@8..27 + 0: AT@8..10 "@" [Newline("\n")] [] + 1: SCSS_EXTEND_AT_RULE@10..27 + 0: EXTEND_KW@10..17 "extend" [] [Whitespace(" ")] + 1: CSS_SELECTOR_LIST@17..17 + 2: SCSS_EXTEND_OPTIONAL_MODIFIER@17..26 + 0: BANG@17..18 "!" [] [] + 1: OPTIONAL_KW@18..26 "optional" [] [] + 3: SEMICOLON@26..27 ";" [] [] + 2: CSS_AT_RULE@27..47 + 0: AT@27..29 "@" [Newline("\n")] [] + 1: SCSS_EXTEND_AT_RULE@29..47 + 0: EXTEND_KW@29..36 "extend" [] [Whitespace(" ")] + 1: CSS_SELECTOR_LIST@36..45 + 0: CSS_COMPOUND_SELECTOR@36..45 + 0: CSS_NESTED_SELECTOR_LIST@36..36 + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@36..45 + 0: CSS_CLASS_SELECTOR@36..45 + 0: DOT@36..37 "." [] [] + 1: CSS_CUSTOM_IDENTIFIER@37..45 + 0: IDENT@37..45 "message" [] [Whitespace(" ")] + 2: SCSS_EXTEND_OPTIONAL_MODIFIER@45..46 + 0: BANG@45..46 "!" [] [] + 1: (empty) + 3: SEMICOLON@46..47 ";" [] [] + 3: CSS_AT_RULE@47..64 + 0: AT@47..49 "@" [Newline("\n")] [] + 1: SCSS_EXTEND_AT_RULE@49..64 + 0: EXTEND_KW@49..56 "extend" [] [Whitespace(" ")] + 1: CSS_SELECTOR_LIST@56..64 + 0: CSS_COMPOUND_SELECTOR@56..64 + 0: CSS_NESTED_SELECTOR_LIST@56..56 + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@56..64 + 0: CSS_CLASS_SELECTOR@56..64 + 0: DOT@56..57 "." [] [] + 1: CSS_CUSTOM_IDENTIFIER@57..64 + 0: IDENT@57..64 "message" [] [] + 2: (empty) + 3: (empty) + 2: EOF@64..65 "" [Newline("\n")] [] + +``` + +## Diagnostics + +``` +extend.scss:3:19 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `optional` but instead found `;` + + 1 │ @extend; + 2 │ @extend !optional; + > 3 │ @extend .message !; + │ ^ + 4 │ @extend .message + 5 │ + + i Remove ; + +extend.scss:5:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `;` but instead the file ends + + 3 │ @extend .message !; + 4 │ @extend .message + > 5 │ + │ + + i the file ends here + + 3 │ @extend .message !; + 4 │ @extend .message + > 5 │ + │ + +``` diff --git a/crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/import.scss b/crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/import.scss new file mode 100644 index 000000000000..b5dbf73ffbea --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/import.scss @@ -0,0 +1,5 @@ +@import; + +@import "theme",; + +@import "theme", "spacing",; diff --git a/crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/import.scss.snap b/crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/import.scss.snap new file mode 100644 index 000000000000..5bbb0608d810 --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/error/scss/at-rule/import.scss.snap @@ -0,0 +1,166 @@ +--- +source: crates/biome_css_parser/tests/spec_test.rs +assertion_line: 208 +expression: snapshot +--- + +## Input + +```css +@import; + +@import "theme",; + +@import "theme", "spacing",; + +``` + + +## AST + +``` +CssRoot { + bom_token: missing (optional), + items: CssRootItemList [ + CssAtRule { + at_token: AT@0..1 "@" [] [], + rule: ScssImportAtRule { + import_token: IMPORT_KW@1..7 "import" [] [], + imports: ScssImportItemList [], + semicolon_token: SEMICOLON@7..8 ";" [] [], + }, + }, + CssAtRule { + at_token: AT@8..11 "@" [Newline("\n"), Newline("\n")] [], + rule: ScssImportAtRule { + import_token: IMPORT_KW@11..18 "import" [] [Whitespace(" ")], + imports: ScssImportItemList [ + CssString { + value_token: CSS_STRING_LITERAL@18..25 "\"theme\"" [] [], + }, + COMMA@25..26 "," [] [], + missing element, + ], + semicolon_token: SEMICOLON@26..27 ";" [] [], + }, + }, + CssAtRule { + at_token: AT@27..30 "@" [Newline("\n"), Newline("\n")] [], + rule: ScssImportAtRule { + import_token: IMPORT_KW@30..37 "import" [] [Whitespace(" ")], + imports: ScssImportItemList [ + CssString { + value_token: CSS_STRING_LITERAL@37..44 "\"theme\"" [] [], + }, + COMMA@44..46 "," [] [Whitespace(" ")], + CssString { + value_token: CSS_STRING_LITERAL@46..55 "\"spacing\"" [] [], + }, + COMMA@55..56 "," [] [], + missing element, + ], + semicolon_token: SEMICOLON@56..57 ";" [] [], + }, + }, + ], + eof_token: EOF@57..58 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: CSS_ROOT@0..58 + 0: (empty) + 1: CSS_ROOT_ITEM_LIST@0..57 + 0: CSS_AT_RULE@0..8 + 0: AT@0..1 "@" [] [] + 1: SCSS_IMPORT_AT_RULE@1..8 + 0: IMPORT_KW@1..7 "import" [] [] + 1: SCSS_IMPORT_ITEM_LIST@7..7 + 2: SEMICOLON@7..8 ";" [] [] + 1: CSS_AT_RULE@8..27 + 0: AT@8..11 "@" [Newline("\n"), Newline("\n")] [] + 1: SCSS_IMPORT_AT_RULE@11..27 + 0: IMPORT_KW@11..18 "import" [] [Whitespace(" ")] + 1: SCSS_IMPORT_ITEM_LIST@18..26 + 0: CSS_STRING@18..25 + 0: CSS_STRING_LITERAL@18..25 "\"theme\"" [] [] + 1: COMMA@25..26 "," [] [] + 2: (empty) + 2: SEMICOLON@26..27 ";" [] [] + 2: CSS_AT_RULE@27..57 + 0: AT@27..30 "@" [Newline("\n"), Newline("\n")] [] + 1: SCSS_IMPORT_AT_RULE@30..57 + 0: IMPORT_KW@30..37 "import" [] [Whitespace(" ")] + 1: SCSS_IMPORT_ITEM_LIST@37..56 + 0: CSS_STRING@37..44 + 0: CSS_STRING_LITERAL@37..44 "\"theme\"" [] [] + 1: COMMA@44..46 "," [] [Whitespace(" ")] + 2: CSS_STRING@46..55 + 0: CSS_STRING_LITERAL@46..55 "\"spacing\"" [] [] + 3: COMMA@55..56 "," [] [] + 4: (empty) + 2: SEMICOLON@56..57 ";" [] [] + 2: EOF@57..58 "" [Newline("\n")] [] + +``` + +## Diagnostics + +``` +import.scss:1:8 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a string but instead found ';'. + + > 1 │ @import; + │ ^ + 2 │ + 3 │ @import "theme",; + + i Expected a string here. + + > 1 │ @import; + │ ^ + 2 │ + 3 │ @import "theme",; + +import.scss:3:17 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a string but instead found ';'. + + 1 │ @import; + 2 │ + > 3 │ @import "theme",; + │ ^ + 4 │ + 5 │ @import "theme", "spacing",; + + i Expected a string here. + + 1 │ @import; + 2 │ + > 3 │ @import "theme",; + │ ^ + 4 │ + 5 │ @import "theme", "spacing",; + +import.scss:5:28 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a string but instead found ';'. + + 3 │ @import "theme",; + 4 │ + > 5 │ @import "theme", "spacing",; + │ ^ + 6 │ + + i Expected a string here. + + 3 │ @import "theme",; + 4 │ + > 5 │ @import "theme", "spacing",; + │ ^ + 6 │ + +``` diff --git a/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_compound_selector.css.snap b/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_compound_selector.css.snap index 9991de6fe615..42a15238d45e 100644 --- a/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_compound_selector.css.snap +++ b/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_compound_selector.css.snap @@ -323,11 +323,6 @@ CssRoot { }, ], }, - CssBogus { - items: [ - CSS_SPACE_LITERAL@133..134 "\n" [] [], - ], - }, ], }, }, @@ -339,7 +334,7 @@ CssRoot { }, }, ], - eof_token: EOF@134..134 "" [] [], + eof_token: EOF@133..134 "" [Newline("\n")] [], } ``` @@ -348,7 +343,7 @@ CssRoot { ``` 0: CSS_ROOT@0..134 0: (empty) - 1: CSS_ROOT_ITEM_LIST@0..134 + 1: CSS_ROOT_ITEM_LIST@0..133 0: CSS_QUALIFIED_RULE@0..9 0: CSS_SELECTOR_LIST@0..7 0: CSS_COMPOUND_SELECTOR@0..7 @@ -520,15 +515,15 @@ CssRoot { 0: L_CURLY@120..121 "{" [] [] 1: CSS_DECLARATION_OR_RULE_LIST@121..121 2: R_CURLY@121..122 "}" [] [] - 7: CSS_QUALIFIED_RULE@122..134 - 0: CSS_SELECTOR_LIST@122..134 - 0: CSS_COMPOUND_SELECTOR@122..134 + 7: CSS_QUALIFIED_RULE@122..133 + 0: CSS_SELECTOR_LIST@122..133 + 0: CSS_COMPOUND_SELECTOR@122..133 0: CSS_NESTED_SELECTOR_LIST@122..122 1: (empty) - 2: CSS_SUB_SELECTOR_LIST@122..134 - 0: CSS_PSEUDO_CLASS_SELECTOR@122..134 + 2: CSS_SUB_SELECTOR_LIST@122..133 + 0: CSS_PSEUDO_CLASS_SELECTOR@122..133 0: COLON@122..124 ":" [Newline("\n")] [] - 1: CSS_BOGUS_PSEUDO_CLASS@124..134 + 1: CSS_BOGUS_PSEUDO_CLASS@124..133 0: CSS_IDENTIFIER@124..128 0: IDENT@124..128 "host" [] [] 1: L_PAREN@128..129 "(" [] [] @@ -540,10 +535,8 @@ CssRoot { 0: DOT@129..130 "." [] [] 1: CSS_CUSTOM_IDENTIFIER@130..133 0: IDENT@130..133 "div" [] [] - 3: CSS_BOGUS@133..134 - 0: CSS_SPACE_LITERAL@133..134 "\n" [] [] - 1: CSS_BOGUS_BLOCK@134..134 - 2: EOF@134..134 "" [] [] + 1: CSS_BOGUS_BLOCK@133..133 + 2: EOF@133..134 "" [Newline("\n")] [] ``` @@ -703,27 +696,6 @@ pseudo_class_function_compound_selector.css:7:12 parse ━━━━━━━━ i Remove { -pseudo_class_function_compound_selector.css:8:7 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - × Expected a compound selector but instead found '.div - '. - - 6 │ :host(.div .class {} - 7 │ :host(.div {} - > 8 │ :host(.div - │ ^^^^ - > 9 │ - │ - - i Expected a compound selector here. - - 6 │ :host(.div .class {} - 7 │ :host(.div {} - > 8 │ :host(.div - │ ^^^^ - > 9 │ - │ - pseudo_class_function_compound_selector.css:9:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × expected `)` but instead the file ends diff --git a/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_compound_selector_list.css.snap b/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_compound_selector_list.css.snap index 14e87cc08e32..8c49635e5017 100644 --- a/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_compound_selector_list.css.snap +++ b/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_compound_selector_list.css.snap @@ -1,5 +1,6 @@ --- source: crates/biome_css_parser/tests/spec_test.rs +assertion_line: 208 expression: snapshot --- @@ -359,11 +360,6 @@ CssRoot { ], }, ], - CssBogus { - items: [ - CSS_SPACE_LITERAL@148..149 "\n" [] [], - ], - }, ], }, }, @@ -375,7 +371,7 @@ CssRoot { }, }, ], - eof_token: EOF@149..149 "" [] [], + eof_token: EOF@148..149 "" [Newline("\n")] [], } ``` @@ -384,7 +380,7 @@ CssRoot { ``` 0: CSS_ROOT@0..149 0: (empty) - 1: CSS_ROOT_ITEM_LIST@0..149 + 1: CSS_ROOT_ITEM_LIST@0..148 0: CSS_QUALIFIED_RULE@0..9 0: CSS_SELECTOR_LIST@0..7 0: CSS_COMPOUND_SELECTOR@0..7 @@ -576,15 +572,15 @@ CssRoot { 0: L_CURLY@135..136 "{" [] [] 1: CSS_DECLARATION_OR_RULE_LIST@136..136 2: R_CURLY@136..137 "}" [] [] - 8: CSS_QUALIFIED_RULE@137..149 - 0: CSS_SELECTOR_LIST@137..149 - 0: CSS_COMPOUND_SELECTOR@137..149 + 8: CSS_QUALIFIED_RULE@137..148 + 0: CSS_SELECTOR_LIST@137..148 + 0: CSS_COMPOUND_SELECTOR@137..148 0: CSS_NESTED_SELECTOR_LIST@137..137 1: (empty) - 2: CSS_SUB_SELECTOR_LIST@137..149 - 0: CSS_PSEUDO_CLASS_SELECTOR@137..149 + 2: CSS_SUB_SELECTOR_LIST@137..148 + 0: CSS_PSEUDO_CLASS_SELECTOR@137..148 0: COLON@137..139 ":" [Newline("\n")] [] - 1: CSS_BOGUS_PSEUDO_CLASS@139..149 + 1: CSS_BOGUS_PSEUDO_CLASS@139..148 0: CSS_IDENTIFIER@139..143 0: IDENT@139..143 "past" [] [] 1: L_PAREN@143..144 "(" [] [] @@ -597,10 +593,8 @@ CssRoot { 0: DOT@144..145 "." [] [] 1: CSS_CUSTOM_IDENTIFIER@145..148 0: IDENT@145..148 "div" [] [] - 3: CSS_BOGUS@148..149 - 0: CSS_SPACE_LITERAL@148..149 "\n" [] [] - 1: CSS_BOGUS_BLOCK@149..149 - 2: EOF@149..149 "" [] [] + 1: CSS_BOGUS_BLOCK@148..148 + 2: EOF@148..149 "" [Newline("\n")] [] ``` @@ -819,42 +813,6 @@ pseudo_class_function_compound_selector_list.css:8:12 parse ━━━━━━ i Remove { -pseudo_class_function_compound_selector_list.css:9:11 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - × expected `,` but instead found ` - ` - - 7 │ :past(.div .class {} - 8 │ :past(.div {} - > 9 │ :past(.div - │ - > 10 │ - │ - - i Remove - - -pseudo_class_function_compound_selector_list.css:9:7 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - × Expected a compound selector but instead found '.div - '. - - 7 │ :past(.div .class {} - 8 │ :past(.div {} - > 9 │ :past(.div - │ ^^^^ - > 10 │ - │ - - i Expected a compound selector here. - - 7 │ :past(.div .class {} - 8 │ :past(.div {} - > 9 │ :past(.div - │ ^^^^ - > 10 │ - │ - pseudo_class_function_compound_selector_list.css:10:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × expected `)` but instead the file ends diff --git a/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_relative_selector_list.css.snap b/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_relative_selector_list.css.snap index e8dbbfb99463..bcc5910d6801 100644 --- a/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_relative_selector_list.css.snap +++ b/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_relative_selector_list.css.snap @@ -1,5 +1,6 @@ --- source: crates/biome_css_parser/tests/spec_test.rs +assertion_line: 208 expression: snapshot --- @@ -368,21 +369,17 @@ CssRoot { CssRelativeSelectorList [ CssRelativeSelector { combinator: missing (optional), - selector: CssComplexSelector { - left: CssCompoundSelector { - nesting_selectors: CssNestedSelectorList [], - simple_selector: missing (optional), - sub_selectors: CssSubSelectorList [ - CssClassSelector { - dot_token: DOT@147..148 "." [] [], - name: CssCustomIdentifier { - value_token: IDENT@148..151 "div" [] [], - }, + selector: CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@147..148 "." [] [], + name: CssCustomIdentifier { + value_token: IDENT@148..151 "div" [] [], }, - ], - }, - combinator: CSS_SPACE_LITERAL@151..152 "\n" [] [], - right: missing (required), + }, + ], }, }, ], @@ -397,7 +394,7 @@ CssRoot { }, }, ], - eof_token: EOF@152..152 "" [] [], + eof_token: EOF@151..152 "" [Newline("\n")] [], } ``` @@ -406,7 +403,7 @@ CssRoot { ``` 0: CSS_ROOT@0..152 0: (empty) - 1: CSS_ROOT_ITEM_LIST@0..152 + 1: CSS_ROOT_ITEM_LIST@0..151 0: CSS_QUALIFIED_RULE@0..8 0: CSS_SELECTOR_LIST@0..6 0: CSS_COMPOUND_SELECTOR@0..6 @@ -613,34 +610,31 @@ CssRoot { 0: L_CURLY@139..140 "{" [] [] 1: CSS_DECLARATION_OR_RULE_LIST@140..140 2: R_CURLY@140..141 "}" [] [] - 8: CSS_QUALIFIED_RULE@141..152 - 0: CSS_SELECTOR_LIST@141..152 - 0: CSS_COMPOUND_SELECTOR@141..152 + 8: CSS_QUALIFIED_RULE@141..151 + 0: CSS_SELECTOR_LIST@141..151 + 0: CSS_COMPOUND_SELECTOR@141..151 0: CSS_NESTED_SELECTOR_LIST@141..141 1: (empty) - 2: CSS_SUB_SELECTOR_LIST@141..152 - 0: CSS_PSEUDO_CLASS_SELECTOR@141..152 + 2: CSS_SUB_SELECTOR_LIST@141..151 + 0: CSS_PSEUDO_CLASS_SELECTOR@141..151 0: COLON@141..143 ":" [Newline("\n")] [] - 1: CSS_BOGUS_PSEUDO_CLASS@143..152 + 1: CSS_BOGUS_PSEUDO_CLASS@143..151 0: CSS_IDENTIFIER@143..146 0: IDENT@143..146 "has" [] [] 1: L_PAREN@146..147 "(" [] [] - 2: CSS_RELATIVE_SELECTOR_LIST@147..152 - 0: CSS_RELATIVE_SELECTOR@147..152 + 2: CSS_RELATIVE_SELECTOR_LIST@147..151 + 0: CSS_RELATIVE_SELECTOR@147..151 0: (empty) - 1: CSS_COMPLEX_SELECTOR@147..152 - 0: CSS_COMPOUND_SELECTOR@147..151 - 0: CSS_NESTED_SELECTOR_LIST@147..147 - 1: (empty) - 2: CSS_SUB_SELECTOR_LIST@147..151 - 0: CSS_CLASS_SELECTOR@147..151 - 0: DOT@147..148 "." [] [] - 1: CSS_CUSTOM_IDENTIFIER@148..151 - 0: IDENT@148..151 "div" [] [] - 1: CSS_SPACE_LITERAL@151..152 "\n" [] [] - 2: (empty) - 1: CSS_BOGUS_BLOCK@152..152 - 2: EOF@152..152 "" [] [] + 1: CSS_COMPOUND_SELECTOR@147..151 + 0: CSS_NESTED_SELECTOR_LIST@147..147 + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@147..151 + 0: CSS_CLASS_SELECTOR@147..151 + 0: DOT@147..148 "." [] [] + 1: CSS_CUSTOM_IDENTIFIER@148..151 + 0: IDENT@148..151 "div" [] [] + 1: CSS_BOGUS_BLOCK@151..151 + 2: EOF@151..152 "" [Newline("\n")] [] ``` @@ -797,14 +791,14 @@ pseudo_class_function_relative_selector_list.css:8:11 parse ━━━━━━ pseudo_class_function_relative_selector_list.css:10:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × Expected a compound selector but instead found the end of the file. + × expected `)` but instead the file ends 8 │ :has(.div {} 9 │ :has(.div > 10 │ │ - i Expected a compound selector here. + i the file ends here 8 │ :has(.div {} 9 │ :has(.div diff --git a/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_selector/disabled/pseudo_class_function_selector_disabled.css.snap b/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_selector/disabled/pseudo_class_function_selector_disabled.css.snap index 322a302a00ba..e792e387fe15 100644 --- a/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_selector/disabled/pseudo_class_function_selector_disabled.css.snap +++ b/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_selector/disabled/pseudo_class_function_selector_disabled.css.snap @@ -1,5 +1,6 @@ --- source: crates/biome_css_parser/tests/spec_test.rs +assertion_line: 208 expression: snapshot --- @@ -423,35 +424,31 @@ CssRoot { }, L_PAREN@183..184 "(" [] [], CssComplexSelector { - left: CssComplexSelector { - left: CssCompoundSelector { - nesting_selectors: CssNestedSelectorList [], - simple_selector: missing (optional), - sub_selectors: CssSubSelectorList [ - CssClassSelector { - dot_token: DOT@184..185 "." [] [], - name: CssCustomIdentifier { - value_token: IDENT@185..188 "div" [] [], - }, + left: CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@184..185 "." [] [], + name: CssCustomIdentifier { + value_token: IDENT@185..188 "div" [] [], }, - ], - }, - combinator: CSS_SPACE_LITERAL@188..189 " " [] [], - right: CssCompoundSelector { - nesting_selectors: CssNestedSelectorList [], - simple_selector: missing (optional), - sub_selectors: CssSubSelectorList [ - CssClassSelector { - dot_token: DOT@189..190 "." [] [], - name: CssCustomIdentifier { - value_token: IDENT@190..195 "class" [] [], - }, + }, + ], + }, + combinator: CSS_SPACE_LITERAL@188..189 " " [] [], + right: CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@189..190 "." [] [], + name: CssCustomIdentifier { + value_token: IDENT@190..195 "class" [] [], }, - ], - }, + }, + ], }, - combinator: CSS_SPACE_LITERAL@195..196 "\n" [] [], - right: missing (required), }, ], }, @@ -464,7 +461,7 @@ CssRoot { }, }, ], - eof_token: EOF@196..196 "" [] [], + eof_token: EOF@195..196 "" [Newline("\n")] [], } ``` @@ -473,7 +470,7 @@ CssRoot { ``` 0: CSS_ROOT@0..196 0: (empty) - 1: CSS_ROOT_ITEM_LIST@0..196 + 1: CSS_ROOT_ITEM_LIST@0..195 0: CSS_QUALIFIED_RULE@0..22 0: CSS_SELECTOR_LIST@0..20 0: CSS_COMPOUND_SELECTOR@0..20 @@ -720,41 +717,38 @@ CssRoot { 0: L_CURLY@173..174 "{" [] [] 1: CSS_DECLARATION_OR_RULE_LIST@174..174 2: R_CURLY@174..175 "}" [] [] - 8: CSS_QUALIFIED_RULE@175..196 - 0: CSS_SELECTOR_LIST@175..196 - 0: CSS_COMPOUND_SELECTOR@175..196 + 8: CSS_QUALIFIED_RULE@175..195 + 0: CSS_SELECTOR_LIST@175..195 + 0: CSS_COMPOUND_SELECTOR@175..195 0: CSS_NESTED_SELECTOR_LIST@175..175 1: (empty) - 2: CSS_SUB_SELECTOR_LIST@175..196 - 0: CSS_PSEUDO_CLASS_SELECTOR@175..196 + 2: CSS_SUB_SELECTOR_LIST@175..195 + 0: CSS_PSEUDO_CLASS_SELECTOR@175..195 0: COLON@175..177 ":" [Newline("\n")] [] - 1: CSS_BOGUS_PSEUDO_CLASS@177..196 + 1: CSS_BOGUS_PSEUDO_CLASS@177..195 0: CSS_IDENTIFIER@177..183 0: IDENT@177..183 "global" [] [] 1: L_PAREN@183..184 "(" [] [] - 2: CSS_COMPLEX_SELECTOR@184..196 - 0: CSS_COMPLEX_SELECTOR@184..195 - 0: CSS_COMPOUND_SELECTOR@184..188 - 0: CSS_NESTED_SELECTOR_LIST@184..184 - 1: (empty) - 2: CSS_SUB_SELECTOR_LIST@184..188 - 0: CSS_CLASS_SELECTOR@184..188 - 0: DOT@184..185 "." [] [] - 1: CSS_CUSTOM_IDENTIFIER@185..188 - 0: IDENT@185..188 "div" [] [] - 1: CSS_SPACE_LITERAL@188..189 " " [] [] - 2: CSS_COMPOUND_SELECTOR@189..195 - 0: CSS_NESTED_SELECTOR_LIST@189..189 - 1: (empty) - 2: CSS_SUB_SELECTOR_LIST@189..195 - 0: CSS_CLASS_SELECTOR@189..195 - 0: DOT@189..190 "." [] [] - 1: CSS_CUSTOM_IDENTIFIER@190..195 - 0: IDENT@190..195 "class" [] [] - 1: CSS_SPACE_LITERAL@195..196 "\n" [] [] - 2: (empty) - 1: CSS_BOGUS_BLOCK@196..196 - 2: EOF@196..196 "" [] [] + 2: CSS_COMPLEX_SELECTOR@184..195 + 0: CSS_COMPOUND_SELECTOR@184..188 + 0: CSS_NESTED_SELECTOR_LIST@184..184 + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@184..188 + 0: CSS_CLASS_SELECTOR@184..188 + 0: DOT@184..185 "." [] [] + 1: CSS_CUSTOM_IDENTIFIER@185..188 + 0: IDENT@185..188 "div" [] [] + 1: CSS_SPACE_LITERAL@188..189 " " [] [] + 2: CSS_COMPOUND_SELECTOR@189..195 + 0: CSS_NESTED_SELECTOR_LIST@189..189 + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@189..195 + 0: CSS_CLASS_SELECTOR@189..195 + 0: DOT@189..190 "." [] [] + 1: CSS_CUSTOM_IDENTIFIER@190..195 + 0: IDENT@190..195 "class" [] [] + 1: CSS_BOGUS_BLOCK@195..195 + 2: EOF@195..196 "" [Newline("\n")] [] ``` @@ -870,8 +864,7 @@ pseudo_class_function_selector_disabled.css:9:2 parse ━━━━━━━━ 8 │ :global(.div .class {} > 9 │ :global(.div .class │ ^^^^^^^^^^^^^^^^^^ - > 10 │ - │ + 10 │ i You can enable `:local` and `:global` pseudo-class parsing by setting the `css.parser.cssModules` option to `true` in your configuration file. diff --git a/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_selector/enabled/pseudo_class_function_selector_enabled.css.snap b/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_selector/enabled/pseudo_class_function_selector_enabled.css.snap index 75ae0554a78f..41e73bd4be61 100644 --- a/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_selector/enabled/pseudo_class_function_selector_enabled.css.snap +++ b/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_selector/enabled/pseudo_class_function_selector_enabled.css.snap @@ -1,5 +1,6 @@ --- source: crates/biome_css_parser/tests/spec_test.rs +assertion_line: 208 expression: snapshot --- @@ -234,35 +235,31 @@ CssRoot { }, L_PAREN@104..105 "(" [] [], CssComplexSelector { - left: CssComplexSelector { - left: CssCompoundSelector { - nesting_selectors: CssNestedSelectorList [], - simple_selector: missing (optional), - sub_selectors: CssSubSelectorList [ - CssClassSelector { - dot_token: DOT@105..106 "." [] [], - name: CssCustomIdentifier { - value_token: IDENT@106..109 "div" [] [], - }, + left: CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@105..106 "." [] [], + name: CssCustomIdentifier { + value_token: IDENT@106..109 "div" [] [], }, - ], - }, - combinator: CSS_SPACE_LITERAL@109..110 " " [] [], - right: CssCompoundSelector { - nesting_selectors: CssNestedSelectorList [], - simple_selector: missing (optional), - sub_selectors: CssSubSelectorList [ - CssClassSelector { - dot_token: DOT@110..111 "." [] [], - name: CssCustomIdentifier { - value_token: IDENT@111..116 "class" [] [], - }, + }, + ], + }, + combinator: CSS_SPACE_LITERAL@109..110 " " [] [], + right: CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@110..111 "." [] [], + name: CssCustomIdentifier { + value_token: IDENT@111..116 "class" [] [], }, - ], - }, + }, + ], }, - combinator: CSS_SPACE_LITERAL@116..117 "\n" [] [], - right: missing (required), }, ], }, @@ -275,7 +272,7 @@ CssRoot { }, }, ], - eof_token: EOF@117..117 "" [] [], + eof_token: EOF@116..117 "" [Newline("\n")] [], } ``` @@ -284,7 +281,7 @@ CssRoot { ``` 0: CSS_ROOT@0..117 0: (empty) - 1: CSS_ROOT_ITEM_LIST@0..117 + 1: CSS_ROOT_ITEM_LIST@0..116 0: CSS_QUALIFIED_RULE@0..11 0: CSS_SELECTOR_LIST@0..9 0: CSS_COMPOUND_SELECTOR@0..9 @@ -409,41 +406,38 @@ CssRoot { 0: L_CURLY@94..95 "{" [] [] 1: CSS_DECLARATION_OR_RULE_LIST@95..95 2: R_CURLY@95..96 "}" [] [] - 5: CSS_QUALIFIED_RULE@96..117 - 0: CSS_SELECTOR_LIST@96..117 - 0: CSS_COMPOUND_SELECTOR@96..117 + 5: CSS_QUALIFIED_RULE@96..116 + 0: CSS_SELECTOR_LIST@96..116 + 0: CSS_COMPOUND_SELECTOR@96..116 0: CSS_NESTED_SELECTOR_LIST@96..96 1: (empty) - 2: CSS_SUB_SELECTOR_LIST@96..117 - 0: CSS_PSEUDO_CLASS_SELECTOR@96..117 + 2: CSS_SUB_SELECTOR_LIST@96..116 + 0: CSS_PSEUDO_CLASS_SELECTOR@96..116 0: COLON@96..98 ":" [Newline("\n")] [] - 1: CSS_BOGUS_PSEUDO_CLASS@98..117 + 1: CSS_BOGUS_PSEUDO_CLASS@98..116 0: CSS_IDENTIFIER@98..104 0: IDENT@98..104 "global" [] [] 1: L_PAREN@104..105 "(" [] [] - 2: CSS_COMPLEX_SELECTOR@105..117 - 0: CSS_COMPLEX_SELECTOR@105..116 - 0: CSS_COMPOUND_SELECTOR@105..109 - 0: CSS_NESTED_SELECTOR_LIST@105..105 - 1: (empty) - 2: CSS_SUB_SELECTOR_LIST@105..109 - 0: CSS_CLASS_SELECTOR@105..109 - 0: DOT@105..106 "." [] [] - 1: CSS_CUSTOM_IDENTIFIER@106..109 - 0: IDENT@106..109 "div" [] [] - 1: CSS_SPACE_LITERAL@109..110 " " [] [] - 2: CSS_COMPOUND_SELECTOR@110..116 - 0: CSS_NESTED_SELECTOR_LIST@110..110 - 1: (empty) - 2: CSS_SUB_SELECTOR_LIST@110..116 - 0: CSS_CLASS_SELECTOR@110..116 - 0: DOT@110..111 "." [] [] - 1: CSS_CUSTOM_IDENTIFIER@111..116 - 0: IDENT@111..116 "class" [] [] - 1: CSS_SPACE_LITERAL@116..117 "\n" [] [] - 2: (empty) - 1: CSS_BOGUS_BLOCK@117..117 - 2: EOF@117..117 "" [] [] + 2: CSS_COMPLEX_SELECTOR@105..116 + 0: CSS_COMPOUND_SELECTOR@105..109 + 0: CSS_NESTED_SELECTOR_LIST@105..105 + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@105..109 + 0: CSS_CLASS_SELECTOR@105..109 + 0: DOT@105..106 "." [] [] + 1: CSS_CUSTOM_IDENTIFIER@106..109 + 0: IDENT@106..109 "div" [] [] + 1: CSS_SPACE_LITERAL@109..110 " " [] [] + 2: CSS_COMPOUND_SELECTOR@110..116 + 0: CSS_NESTED_SELECTOR_LIST@110..110 + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@110..116 + 0: CSS_CLASS_SELECTOR@110..116 + 0: DOT@110..111 "." [] [] + 1: CSS_CUSTOM_IDENTIFIER@111..116 + 0: IDENT@111..116 "class" [] [] + 1: CSS_BOGUS_BLOCK@116..116 + 2: EOF@116..117 "" [Newline("\n")] [] ``` @@ -552,14 +546,14 @@ pseudo_class_function_selector_enabled.css:5:21 parse ━━━━━━━━ pseudo_class_function_selector_enabled.css:7:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × Expected a compound selector but instead found the end of the file. + × expected `)` but instead the file ends 5 │ :global(.div .class {} 6 │ :global(.div .class > 7 │ │ - i Expected a compound selector here. + i the file ends here 5 │ :global(.div .class {} 6 │ :global(.div .class diff --git a/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_selector_list.css.snap b/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_selector_list.css.snap index e62b165c3f4d..b87ee7096435 100644 --- a/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_selector_list.css.snap +++ b/crates/biome_css_parser/tests/css_test_suite/error/selector/pseudo_class/pseudo_class_function_selector_list.css.snap @@ -1,5 +1,6 @@ --- source: crates/biome_css_parser/tests/spec_test.rs +assertion_line: 208 expression: snapshot --- @@ -357,21 +358,17 @@ CssRoot { }, L_PAREN@164..165 "(" [] [], CssSelectorList [ - CssComplexSelector { - left: CssCompoundSelector { - nesting_selectors: CssNestedSelectorList [], - simple_selector: missing (optional), - sub_selectors: CssSubSelectorList [ - CssClassSelector { - dot_token: DOT@165..166 "." [] [], - name: CssCustomIdentifier { - value_token: IDENT@166..169 "div" [] [], - }, + CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@165..166 "." [] [], + name: CssCustomIdentifier { + value_token: IDENT@166..169 "div" [] [], }, - ], - }, - combinator: CSS_SPACE_LITERAL@169..170 "\n" [] [], - right: missing (required), + }, + ], }, ], ], @@ -385,7 +382,7 @@ CssRoot { }, }, ], - eof_token: EOF@170..170 "" [] [], + eof_token: EOF@169..170 "" [Newline("\n")] [], } ``` @@ -394,7 +391,7 @@ CssRoot { ``` 0: CSS_ROOT@0..170 0: (empty) - 1: CSS_ROOT_ITEM_LIST@0..170 + 1: CSS_ROOT_ITEM_LIST@0..169 0: CSS_QUALIFIED_RULE@0..10 0: CSS_SELECTOR_LIST@0..8 0: CSS_COMPOUND_SELECTOR@0..8 @@ -595,32 +592,29 @@ CssRoot { 0: L_CURLY@155..156 "{" [] [] 1: CSS_DECLARATION_OR_RULE_LIST@156..156 2: R_CURLY@156..157 "}" [] [] - 8: CSS_QUALIFIED_RULE@157..170 - 0: CSS_SELECTOR_LIST@157..170 - 0: CSS_COMPOUND_SELECTOR@157..170 + 8: CSS_QUALIFIED_RULE@157..169 + 0: CSS_SELECTOR_LIST@157..169 + 0: CSS_COMPOUND_SELECTOR@157..169 0: CSS_NESTED_SELECTOR_LIST@157..157 1: (empty) - 2: CSS_SUB_SELECTOR_LIST@157..170 - 0: CSS_PSEUDO_CLASS_SELECTOR@157..170 + 2: CSS_SUB_SELECTOR_LIST@157..169 + 0: CSS_PSEUDO_CLASS_SELECTOR@157..169 0: COLON@157..159 ":" [Newline("\n")] [] - 1: CSS_BOGUS_PSEUDO_CLASS@159..170 + 1: CSS_BOGUS_PSEUDO_CLASS@159..169 0: CSS_IDENTIFIER@159..164 0: IDENT@159..164 "where" [] [] 1: L_PAREN@164..165 "(" [] [] - 2: CSS_SELECTOR_LIST@165..170 - 0: CSS_COMPLEX_SELECTOR@165..170 - 0: CSS_COMPOUND_SELECTOR@165..169 - 0: CSS_NESTED_SELECTOR_LIST@165..165 - 1: (empty) - 2: CSS_SUB_SELECTOR_LIST@165..169 - 0: CSS_CLASS_SELECTOR@165..169 - 0: DOT@165..166 "." [] [] - 1: CSS_CUSTOM_IDENTIFIER@166..169 - 0: IDENT@166..169 "div" [] [] - 1: CSS_SPACE_LITERAL@169..170 "\n" [] [] - 2: (empty) - 1: CSS_BOGUS_BLOCK@170..170 - 2: EOF@170..170 "" [] [] + 2: CSS_SELECTOR_LIST@165..169 + 0: CSS_COMPOUND_SELECTOR@165..169 + 0: CSS_NESTED_SELECTOR_LIST@165..165 + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@165..169 + 0: CSS_CLASS_SELECTOR@165..169 + 0: DOT@165..166 "." [] [] + 1: CSS_CUSTOM_IDENTIFIER@166..169 + 0: IDENT@166..169 "div" [] [] + 1: CSS_BOGUS_BLOCK@169..169 + 2: EOF@169..170 "" [Newline("\n")] [] ``` @@ -802,14 +796,14 @@ pseudo_class_function_selector_list.css:8:13 parse ━━━━━━━━━ pseudo_class_function_selector_list.css:10:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × Expected a compound selector but instead found the end of the file. + × expected `)` but instead the file ends 8 │ :where(.div {} 9 │ :where(.div > 10 │ │ - i Expected a compound selector here. + i the file ends here 8 │ :where(.div {} 9 │ :where(.div diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/extend.scss b/crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/extend.scss new file mode 100644 index 000000000000..395e86ad5352 --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/extend.scss @@ -0,0 +1,26 @@ +%toolbelt { + border: 1px solid black; +} + +.message { + color: red; +} + +.action-buttons { + @extend %toolbelt !optional; +} + +.alert { + @extend .message; + @extend .message, .info; + @extend a:hover; +} + +@extend .message; + +// TODO: enable @extend interpolation coverage when interpolation work resumes. +// @extend .icon-#{$name}; + +@include themed-button { + @extend .message; +} diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/extend.scss.snap b/crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/extend.scss.snap new file mode 100644 index 000000000000..05ab99da8723 --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/extend.scss.snap @@ -0,0 +1,559 @@ +--- +source: crates/biome_css_parser/tests/spec_test.rs +expression: snapshot +--- + +## Input + +```css +%toolbelt { + border: 1px solid black; +} + +.message { + color: red; +} + +.action-buttons { + @extend %toolbelt !optional; +} + +.alert { + @extend .message; + @extend .message, .info; + @extend a:hover; +} + +@extend .message; + +// TODO: enable @extend interpolation coverage when interpolation work resumes. +// @extend .icon-#{$name}; + +@include themed-button { + @extend .message; +} + +``` + + +## AST + +``` +CssRoot { + bom_token: missing (optional), + items: CssRootItemList [ + CssQualifiedRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: ScssPlaceholderSelector { + percent_token: PERCENT@0..1 "%" [] [], + name: CssCustomIdentifier { + value_token: IDENT@1..10 "toolbelt" [] [Whitespace(" ")], + }, + }, + sub_selectors: CssSubSelectorList [], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@10..11 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@11..20 "border" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@20..22 ":" [] [Whitespace(" ")], + value: ScssExpression { + items: ScssExpressionItemList [ + CssRegularDimension { + value_token: CSS_NUMBER_LITERAL@22..23 "1" [] [], + unit_token: IDENT@23..26 "px" [] [Whitespace(" ")], + }, + CssIdentifier { + value_token: IDENT@26..32 "solid" [] [Whitespace(" ")], + }, + CssIdentifier { + value_token: IDENT@32..37 "black" [] [], + }, + ], + }, + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@37..38 ";" [] [], + }, + ], + r_curly_token: R_CURLY@38..40 "}" [Newline("\n")] [], + }, + }, + CssQualifiedRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@40..43 "." [Newline("\n"), Newline("\n")] [], + name: CssCustomIdentifier { + value_token: IDENT@43..51 "message" [] [Whitespace(" ")], + }, + }, + ], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@51..52 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@52..60 "color" [Newline("\n"), Whitespace(" ")] [], + }, + colon_token: COLON@60..62 ":" [] [Whitespace(" ")], + value: ScssExpression { + items: ScssExpressionItemList [ + CssIdentifier { + value_token: IDENT@62..65 "red" [] [], + }, + ], + }, + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@65..66 ";" [] [], + }, + ], + r_curly_token: R_CURLY@66..68 "}" [Newline("\n")] [], + }, + }, + CssQualifiedRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@68..71 "." [Newline("\n"), Newline("\n")] [], + name: CssCustomIdentifier { + value_token: IDENT@71..86 "action-buttons" [] [Whitespace(" ")], + }, + }, + ], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@86..87 "{" [] [], + items: CssDeclarationOrRuleList [ + CssAtRule { + at_token: AT@87..91 "@" [Newline("\n"), Whitespace(" ")] [], + rule: ScssExtendAtRule { + extend_token: EXTEND_KW@91..98 "extend" [] [Whitespace(" ")], + css_selector_list: CssSelectorList [ + CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: ScssPlaceholderSelector { + percent_token: PERCENT@98..99 "%" [] [], + name: CssCustomIdentifier { + value_token: IDENT@99..108 "toolbelt" [] [Whitespace(" ")], + }, + }, + sub_selectors: CssSubSelectorList [], + }, + ], + optional_modifier: ScssExtendOptionalModifier { + excl_token: BANG@108..109 "!" [] [], + optional_token: OPTIONAL_KW@109..117 "optional" [] [], + }, + semicolon_token: SEMICOLON@117..118 ";" [] [], + }, + }, + ], + r_curly_token: R_CURLY@118..120 "}" [Newline("\n")] [], + }, + }, + CssQualifiedRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@120..123 "." [Newline("\n"), Newline("\n")] [], + name: CssCustomIdentifier { + value_token: IDENT@123..129 "alert" [] [Whitespace(" ")], + }, + }, + ], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@129..130 "{" [] [], + items: CssDeclarationOrRuleList [ + CssAtRule { + at_token: AT@130..134 "@" [Newline("\n"), Whitespace(" ")] [], + rule: ScssExtendAtRule { + extend_token: EXTEND_KW@134..141 "extend" [] [Whitespace(" ")], + css_selector_list: CssSelectorList [ + CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@141..142 "." [] [], + name: CssCustomIdentifier { + value_token: IDENT@142..149 "message" [] [], + }, + }, + ], + }, + ], + optional_modifier: missing (optional), + semicolon_token: SEMICOLON@149..150 ";" [] [], + }, + }, + CssAtRule { + at_token: AT@150..154 "@" [Newline("\n"), Whitespace(" ")] [], + rule: ScssExtendAtRule { + extend_token: EXTEND_KW@154..161 "extend" [] [Whitespace(" ")], + css_selector_list: CssSelectorList [ + CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@161..162 "." [] [], + name: CssCustomIdentifier { + value_token: IDENT@162..169 "message" [] [], + }, + }, + ], + }, + COMMA@169..171 "," [] [Whitespace(" ")], + CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@171..172 "." [] [], + name: CssCustomIdentifier { + value_token: IDENT@172..176 "info" [] [], + }, + }, + ], + }, + ], + optional_modifier: missing (optional), + semicolon_token: SEMICOLON@176..177 ";" [] [], + }, + }, + CssAtRule { + at_token: AT@177..181 "@" [Newline("\n"), Whitespace(" ")] [], + rule: ScssExtendAtRule { + extend_token: EXTEND_KW@181..188 "extend" [] [Whitespace(" ")], + css_selector_list: CssSelectorList [ + CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: CssTypeSelector { + namespace: missing (optional), + ident: CssIdentifier { + value_token: IDENT@188..189 "a" [] [], + }, + }, + sub_selectors: CssSubSelectorList [ + CssPseudoClassSelector { + colon_token: COLON@189..190 ":" [] [], + class: CssPseudoClassIdentifier { + name: CssIdentifier { + value_token: IDENT@190..195 "hover" [] [], + }, + }, + }, + ], + }, + ], + optional_modifier: missing (optional), + semicolon_token: SEMICOLON@195..196 ";" [] [], + }, + }, + ], + r_curly_token: R_CURLY@196..198 "}" [Newline("\n")] [], + }, + }, + CssAtRule { + at_token: AT@198..201 "@" [Newline("\n"), Newline("\n")] [], + rule: ScssExtendAtRule { + extend_token: EXTEND_KW@201..208 "extend" [] [Whitespace(" ")], + css_selector_list: CssSelectorList [ + CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@208..209 "." [] [], + name: CssCustomIdentifier { + value_token: IDENT@209..216 "message" [] [], + }, + }, + ], + }, + ], + optional_modifier: missing (optional), + semicolon_token: SEMICOLON@216..217 ";" [] [], + }, + }, + CssAtRule { + at_token: AT@217..328 "@" [Newline("\n"), Newline("\n"), Comments("// TODO: enable @exte ..."), Newline("\n"), Comments("// @extend .icon-#{$n ..."), Newline("\n"), Newline("\n")] [], + rule: ScssIncludeAtRule { + include_token: INCLUDE_KW@328..336 "include" [] [Whitespace(" ")], + name: CssIdentifier { + value_token: IDENT@336..350 "themed-button" [] [Whitespace(" ")], + }, + arguments: missing (optional), + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@350..351 "{" [] [], + items: CssDeclarationOrRuleList [ + CssAtRule { + at_token: AT@351..355 "@" [Newline("\n"), Whitespace(" ")] [], + rule: ScssExtendAtRule { + extend_token: EXTEND_KW@355..362 "extend" [] [Whitespace(" ")], + css_selector_list: CssSelectorList [ + CssCompoundSelector { + nesting_selectors: CssNestedSelectorList [], + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@362..363 "." [] [], + name: CssCustomIdentifier { + value_token: IDENT@363..370 "message" [] [], + }, + }, + ], + }, + ], + optional_modifier: missing (optional), + semicolon_token: SEMICOLON@370..371 ";" [] [], + }, + }, + ], + r_curly_token: R_CURLY@371..373 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + }, + ], + eof_token: EOF@373..374 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: CSS_ROOT@0..374 + 0: (empty) + 1: CSS_ROOT_ITEM_LIST@0..373 + 0: CSS_QUALIFIED_RULE@0..40 + 0: CSS_SELECTOR_LIST@0..10 + 0: CSS_COMPOUND_SELECTOR@0..10 + 0: CSS_NESTED_SELECTOR_LIST@0..0 + 1: SCSS_PLACEHOLDER_SELECTOR@0..10 + 0: PERCENT@0..1 "%" [] [] + 1: CSS_CUSTOM_IDENTIFIER@1..10 + 0: IDENT@1..10 "toolbelt" [] [Whitespace(" ")] + 2: CSS_SUB_SELECTOR_LIST@10..10 + 1: CSS_DECLARATION_OR_RULE_BLOCK@10..40 + 0: L_CURLY@10..11 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@11..38 + 0: CSS_DECLARATION_WITH_SEMICOLON@11..38 + 0: CSS_DECLARATION@11..37 + 0: CSS_GENERIC_PROPERTY@11..37 + 0: CSS_IDENTIFIER@11..20 + 0: IDENT@11..20 "border" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@20..22 ":" [] [Whitespace(" ")] + 2: SCSS_EXPRESSION@22..37 + 0: SCSS_EXPRESSION_ITEM_LIST@22..37 + 0: CSS_REGULAR_DIMENSION@22..26 + 0: CSS_NUMBER_LITERAL@22..23 "1" [] [] + 1: IDENT@23..26 "px" [] [Whitespace(" ")] + 1: CSS_IDENTIFIER@26..32 + 0: IDENT@26..32 "solid" [] [Whitespace(" ")] + 2: CSS_IDENTIFIER@32..37 + 0: IDENT@32..37 "black" [] [] + 1: (empty) + 1: SEMICOLON@37..38 ";" [] [] + 2: R_CURLY@38..40 "}" [Newline("\n")] [] + 1: CSS_QUALIFIED_RULE@40..68 + 0: CSS_SELECTOR_LIST@40..51 + 0: CSS_COMPOUND_SELECTOR@40..51 + 0: CSS_NESTED_SELECTOR_LIST@40..40 + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@40..51 + 0: CSS_CLASS_SELECTOR@40..51 + 0: DOT@40..43 "." [Newline("\n"), Newline("\n")] [] + 1: CSS_CUSTOM_IDENTIFIER@43..51 + 0: IDENT@43..51 "message" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@51..68 + 0: L_CURLY@51..52 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@52..66 + 0: CSS_DECLARATION_WITH_SEMICOLON@52..66 + 0: CSS_DECLARATION@52..65 + 0: CSS_GENERIC_PROPERTY@52..65 + 0: CSS_IDENTIFIER@52..60 + 0: IDENT@52..60 "color" [Newline("\n"), Whitespace(" ")] [] + 1: COLON@60..62 ":" [] [Whitespace(" ")] + 2: SCSS_EXPRESSION@62..65 + 0: SCSS_EXPRESSION_ITEM_LIST@62..65 + 0: CSS_IDENTIFIER@62..65 + 0: IDENT@62..65 "red" [] [] + 1: (empty) + 1: SEMICOLON@65..66 ";" [] [] + 2: R_CURLY@66..68 "}" [Newline("\n")] [] + 2: CSS_QUALIFIED_RULE@68..120 + 0: CSS_SELECTOR_LIST@68..86 + 0: CSS_COMPOUND_SELECTOR@68..86 + 0: CSS_NESTED_SELECTOR_LIST@68..68 + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@68..86 + 0: CSS_CLASS_SELECTOR@68..86 + 0: DOT@68..71 "." [Newline("\n"), Newline("\n")] [] + 1: CSS_CUSTOM_IDENTIFIER@71..86 + 0: IDENT@71..86 "action-buttons" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@86..120 + 0: L_CURLY@86..87 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@87..118 + 0: CSS_AT_RULE@87..118 + 0: AT@87..91 "@" [Newline("\n"), Whitespace(" ")] [] + 1: SCSS_EXTEND_AT_RULE@91..118 + 0: EXTEND_KW@91..98 "extend" [] [Whitespace(" ")] + 1: CSS_SELECTOR_LIST@98..108 + 0: CSS_COMPOUND_SELECTOR@98..108 + 0: CSS_NESTED_SELECTOR_LIST@98..98 + 1: SCSS_PLACEHOLDER_SELECTOR@98..108 + 0: PERCENT@98..99 "%" [] [] + 1: CSS_CUSTOM_IDENTIFIER@99..108 + 0: IDENT@99..108 "toolbelt" [] [Whitespace(" ")] + 2: CSS_SUB_SELECTOR_LIST@108..108 + 2: SCSS_EXTEND_OPTIONAL_MODIFIER@108..117 + 0: BANG@108..109 "!" [] [] + 1: OPTIONAL_KW@109..117 "optional" [] [] + 3: SEMICOLON@117..118 ";" [] [] + 2: R_CURLY@118..120 "}" [Newline("\n")] [] + 3: CSS_QUALIFIED_RULE@120..198 + 0: CSS_SELECTOR_LIST@120..129 + 0: CSS_COMPOUND_SELECTOR@120..129 + 0: CSS_NESTED_SELECTOR_LIST@120..120 + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@120..129 + 0: CSS_CLASS_SELECTOR@120..129 + 0: DOT@120..123 "." [Newline("\n"), Newline("\n")] [] + 1: CSS_CUSTOM_IDENTIFIER@123..129 + 0: IDENT@123..129 "alert" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@129..198 + 0: L_CURLY@129..130 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@130..196 + 0: CSS_AT_RULE@130..150 + 0: AT@130..134 "@" [Newline("\n"), Whitespace(" ")] [] + 1: SCSS_EXTEND_AT_RULE@134..150 + 0: EXTEND_KW@134..141 "extend" [] [Whitespace(" ")] + 1: CSS_SELECTOR_LIST@141..149 + 0: CSS_COMPOUND_SELECTOR@141..149 + 0: CSS_NESTED_SELECTOR_LIST@141..141 + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@141..149 + 0: CSS_CLASS_SELECTOR@141..149 + 0: DOT@141..142 "." [] [] + 1: CSS_CUSTOM_IDENTIFIER@142..149 + 0: IDENT@142..149 "message" [] [] + 2: (empty) + 3: SEMICOLON@149..150 ";" [] [] + 1: CSS_AT_RULE@150..177 + 0: AT@150..154 "@" [Newline("\n"), Whitespace(" ")] [] + 1: SCSS_EXTEND_AT_RULE@154..177 + 0: EXTEND_KW@154..161 "extend" [] [Whitespace(" ")] + 1: CSS_SELECTOR_LIST@161..176 + 0: CSS_COMPOUND_SELECTOR@161..169 + 0: CSS_NESTED_SELECTOR_LIST@161..161 + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@161..169 + 0: CSS_CLASS_SELECTOR@161..169 + 0: DOT@161..162 "." [] [] + 1: CSS_CUSTOM_IDENTIFIER@162..169 + 0: IDENT@162..169 "message" [] [] + 1: COMMA@169..171 "," [] [Whitespace(" ")] + 2: CSS_COMPOUND_SELECTOR@171..176 + 0: CSS_NESTED_SELECTOR_LIST@171..171 + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@171..176 + 0: CSS_CLASS_SELECTOR@171..176 + 0: DOT@171..172 "." [] [] + 1: CSS_CUSTOM_IDENTIFIER@172..176 + 0: IDENT@172..176 "info" [] [] + 2: (empty) + 3: SEMICOLON@176..177 ";" [] [] + 2: CSS_AT_RULE@177..196 + 0: AT@177..181 "@" [Newline("\n"), Whitespace(" ")] [] + 1: SCSS_EXTEND_AT_RULE@181..196 + 0: EXTEND_KW@181..188 "extend" [] [Whitespace(" ")] + 1: CSS_SELECTOR_LIST@188..195 + 0: CSS_COMPOUND_SELECTOR@188..195 + 0: CSS_NESTED_SELECTOR_LIST@188..188 + 1: CSS_TYPE_SELECTOR@188..189 + 0: (empty) + 1: CSS_IDENTIFIER@188..189 + 0: IDENT@188..189 "a" [] [] + 2: CSS_SUB_SELECTOR_LIST@189..195 + 0: CSS_PSEUDO_CLASS_SELECTOR@189..195 + 0: COLON@189..190 ":" [] [] + 1: CSS_PSEUDO_CLASS_IDENTIFIER@190..195 + 0: CSS_IDENTIFIER@190..195 + 0: IDENT@190..195 "hover" [] [] + 2: (empty) + 3: SEMICOLON@195..196 ";" [] [] + 2: R_CURLY@196..198 "}" [Newline("\n")] [] + 4: CSS_AT_RULE@198..217 + 0: AT@198..201 "@" [Newline("\n"), Newline("\n")] [] + 1: SCSS_EXTEND_AT_RULE@201..217 + 0: EXTEND_KW@201..208 "extend" [] [Whitespace(" ")] + 1: CSS_SELECTOR_LIST@208..216 + 0: CSS_COMPOUND_SELECTOR@208..216 + 0: CSS_NESTED_SELECTOR_LIST@208..208 + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@208..216 + 0: CSS_CLASS_SELECTOR@208..216 + 0: DOT@208..209 "." [] [] + 1: CSS_CUSTOM_IDENTIFIER@209..216 + 0: IDENT@209..216 "message" [] [] + 2: (empty) + 3: SEMICOLON@216..217 ";" [] [] + 5: CSS_AT_RULE@217..373 + 0: AT@217..328 "@" [Newline("\n"), Newline("\n"), Comments("// TODO: enable @exte ..."), Newline("\n"), Comments("// @extend .icon-#{$n ..."), Newline("\n"), Newline("\n")] [] + 1: SCSS_INCLUDE_AT_RULE@328..373 + 0: INCLUDE_KW@328..336 "include" [] [Whitespace(" ")] + 1: CSS_IDENTIFIER@336..350 + 0: IDENT@336..350 "themed-button" [] [Whitespace(" ")] + 2: (empty) + 3: CSS_DECLARATION_OR_RULE_BLOCK@350..373 + 0: L_CURLY@350..351 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@351..371 + 0: CSS_AT_RULE@351..371 + 0: AT@351..355 "@" [Newline("\n"), Whitespace(" ")] [] + 1: SCSS_EXTEND_AT_RULE@355..371 + 0: EXTEND_KW@355..362 "extend" [] [Whitespace(" ")] + 1: CSS_SELECTOR_LIST@362..370 + 0: CSS_COMPOUND_SELECTOR@362..370 + 0: CSS_NESTED_SELECTOR_LIST@362..362 + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@362..370 + 0: CSS_CLASS_SELECTOR@362..370 + 0: DOT@362..363 "." [] [] + 1: CSS_CUSTOM_IDENTIFIER@363..370 + 0: IDENT@363..370 "message" [] [] + 2: (empty) + 3: SEMICOLON@370..371 ";" [] [] + 2: R_CURLY@371..373 "}" [Newline("\n")] [] + 4: (empty) + 2: EOF@373..374 "" [Newline("\n")] [] + +``` diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/import.scss b/crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/import.scss new file mode 100644 index 000000000000..8117bf1bbfb3 --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/import.scss @@ -0,0 +1,37 @@ +@import "theme"; + +@import "rounded-corners", "text-shadow"; + +@import "theme.scss"; + +@import "theme.css", "spacing"; + +@import "theme.css"; + +@import "theme.CSS?cache=1#section"; + +@import "print.css" print; + +@import "print.css" print, screen; + +@import "print.css" print, "spacing"; + +@import "http://example.com/theme"; + +@import url("theme.css"); + +@import "//example.com/theme"; + +@import "https://example.com/theme.css"; + +@mixin plain-import { + @import "theme.css"; +} + +@mixin dynamic-import { + @import "theme"; +} + +@if true { + @import "theme"; +} diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/import.scss.snap b/crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/import.scss.snap new file mode 100644 index 000000000000..665b5f44a3bc --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/import.scss.snap @@ -0,0 +1,656 @@ +--- +source: crates/biome_css_parser/tests/spec_test.rs +assertion_line: 208 +expression: snapshot +--- + +## Input + +```css +@import "theme"; + +@import "rounded-corners", "text-shadow"; + +@import "theme.scss"; + +@import "theme.css", "spacing"; + +@import "theme.css"; + +@import "theme.CSS?cache=1#section"; + +@import "print.css" print; + +@import "print.css" print, screen; + +@import "print.css" print, "spacing"; + +@import "http://example.com/theme"; + +@import url("theme.css"); + +@import "//example.com/theme"; + +@import "https://example.com/theme.css"; + +@mixin plain-import { + @import "theme.css"; +} + +@mixin dynamic-import { + @import "theme"; +} + +@if true { + @import "theme"; +} + +``` + + +## AST + +``` +CssRoot { + bom_token: missing (optional), + items: CssRootItemList [ + CssAtRule { + at_token: AT@0..1 "@" [] [], + rule: ScssImportAtRule { + import_token: IMPORT_KW@1..8 "import" [] [Whitespace(" ")], + imports: ScssImportItemList [ + CssString { + value_token: CSS_STRING_LITERAL@8..15 "\"theme\"" [] [], + }, + ], + semicolon_token: SEMICOLON@15..16 ";" [] [], + }, + }, + CssAtRule { + at_token: AT@16..19 "@" [Newline("\n"), Newline("\n")] [], + rule: ScssImportAtRule { + import_token: IMPORT_KW@19..26 "import" [] [Whitespace(" ")], + imports: ScssImportItemList [ + CssString { + value_token: CSS_STRING_LITERAL@26..43 "\"rounded-corners\"" [] [], + }, + COMMA@43..45 "," [] [Whitespace(" ")], + CssString { + value_token: CSS_STRING_LITERAL@45..58 "\"text-shadow\"" [] [], + }, + ], + semicolon_token: SEMICOLON@58..59 ";" [] [], + }, + }, + CssAtRule { + at_token: AT@59..62 "@" [Newline("\n"), Newline("\n")] [], + rule: ScssImportAtRule { + import_token: IMPORT_KW@62..69 "import" [] [Whitespace(" ")], + imports: ScssImportItemList [ + CssString { + value_token: CSS_STRING_LITERAL@69..81 "\"theme.scss\"" [] [], + }, + ], + semicolon_token: SEMICOLON@81..82 ";" [] [], + }, + }, + CssAtRule { + at_token: AT@82..85 "@" [Newline("\n"), Newline("\n")] [], + rule: ScssImportAtRule { + import_token: IMPORT_KW@85..92 "import" [] [Whitespace(" ")], + imports: ScssImportItemList [ + ScssPlainImport { + url: CssString { + value_token: CSS_STRING_LITERAL@92..103 "\"theme.css\"" [] [], + }, + layer: missing (optional), + supports: missing (optional), + media: CssMediaQueryList [], + }, + COMMA@103..105 "," [] [Whitespace(" ")], + CssString { + value_token: CSS_STRING_LITERAL@105..114 "\"spacing\"" [] [], + }, + ], + semicolon_token: SEMICOLON@114..115 ";" [] [], + }, + }, + CssAtRule { + at_token: AT@115..118 "@" [Newline("\n"), Newline("\n")] [], + rule: ScssImportAtRule { + import_token: IMPORT_KW@118..125 "import" [] [Whitespace(" ")], + imports: ScssImportItemList [ + ScssPlainImport { + url: CssString { + value_token: CSS_STRING_LITERAL@125..136 "\"theme.css\"" [] [], + }, + layer: missing (optional), + supports: missing (optional), + media: CssMediaQueryList [], + }, + ], + semicolon_token: SEMICOLON@136..137 ";" [] [], + }, + }, + CssAtRule { + at_token: AT@137..140 "@" [Newline("\n"), Newline("\n")] [], + rule: ScssImportAtRule { + import_token: IMPORT_KW@140..147 "import" [] [Whitespace(" ")], + imports: ScssImportItemList [ + CssString { + value_token: CSS_STRING_LITERAL@147..174 "\"theme.CSS?cache=1#section\"" [] [], + }, + ], + semicolon_token: SEMICOLON@174..175 ";" [] [], + }, + }, + CssAtRule { + at_token: AT@175..178 "@" [Newline("\n"), Newline("\n")] [], + rule: ScssImportAtRule { + import_token: IMPORT_KW@178..185 "import" [] [Whitespace(" ")], + imports: ScssImportItemList [ + ScssPlainImport { + url: CssString { + value_token: CSS_STRING_LITERAL@185..197 "\"print.css\"" [] [Whitespace(" ")], + }, + layer: missing (optional), + supports: missing (optional), + media: CssMediaQueryList [ + CssMediaTypeQuery { + modifier: missing (optional), + ty: CssMediaType { + value: CssIdentifier { + value_token: IDENT@197..202 "print" [] [], + }, + }, + }, + ], + }, + ], + semicolon_token: SEMICOLON@202..203 ";" [] [], + }, + }, + CssAtRule { + at_token: AT@203..206 "@" [Newline("\n"), Newline("\n")] [], + rule: ScssImportAtRule { + import_token: IMPORT_KW@206..213 "import" [] [Whitespace(" ")], + imports: ScssImportItemList [ + ScssPlainImport { + url: CssString { + value_token: CSS_STRING_LITERAL@213..225 "\"print.css\"" [] [Whitespace(" ")], + }, + layer: missing (optional), + supports: missing (optional), + media: CssMediaQueryList [ + CssMediaTypeQuery { + modifier: missing (optional), + ty: CssMediaType { + value: CssIdentifier { + value_token: IDENT@225..230 "print" [] [], + }, + }, + }, + COMMA@230..232 "," [] [Whitespace(" ")], + CssMediaTypeQuery { + modifier: missing (optional), + ty: CssMediaType { + value: CssIdentifier { + value_token: IDENT@232..238 "screen" [] [], + }, + }, + }, + ], + }, + ], + semicolon_token: SEMICOLON@238..239 ";" [] [], + }, + }, + CssAtRule { + at_token: AT@239..242 "@" [Newline("\n"), Newline("\n")] [], + rule: ScssImportAtRule { + import_token: IMPORT_KW@242..249 "import" [] [Whitespace(" ")], + imports: ScssImportItemList [ + ScssPlainImport { + url: CssString { + value_token: CSS_STRING_LITERAL@249..261 "\"print.css\"" [] [Whitespace(" ")], + }, + layer: missing (optional), + supports: missing (optional), + media: CssMediaQueryList [ + CssMediaTypeQuery { + modifier: missing (optional), + ty: CssMediaType { + value: CssIdentifier { + value_token: IDENT@261..266 "print" [] [], + }, + }, + }, + ], + }, + COMMA@266..268 "," [] [Whitespace(" ")], + CssString { + value_token: CSS_STRING_LITERAL@268..277 "\"spacing\"" [] [], + }, + ], + semicolon_token: SEMICOLON@277..278 ";" [] [], + }, + }, + CssAtRule { + at_token: AT@278..281 "@" [Newline("\n"), Newline("\n")] [], + rule: ScssImportAtRule { + import_token: IMPORT_KW@281..288 "import" [] [Whitespace(" ")], + imports: ScssImportItemList [ + ScssPlainImport { + url: CssString { + value_token: CSS_STRING_LITERAL@288..314 "\"http://example.com/theme\"" [] [], + }, + layer: missing (optional), + supports: missing (optional), + media: CssMediaQueryList [], + }, + ], + semicolon_token: SEMICOLON@314..315 ";" [] [], + }, + }, + CssAtRule { + at_token: AT@315..318 "@" [Newline("\n"), Newline("\n")] [], + rule: ScssImportAtRule { + import_token: IMPORT_KW@318..325 "import" [] [Whitespace(" ")], + imports: ScssImportItemList [ + ScssPlainImport { + url: CssUrlFunction { + name: URL_KW@325..328 "url" [] [], + l_paren_token: L_PAREN@328..329 "(" [] [], + value: CssString { + value_token: CSS_STRING_LITERAL@329..340 "\"theme.css\"" [] [], + }, + modifiers: CssUrlModifierList [], + r_paren_token: R_PAREN@340..341 ")" [] [], + }, + layer: missing (optional), + supports: missing (optional), + media: CssMediaQueryList [], + }, + ], + semicolon_token: SEMICOLON@341..342 ";" [] [], + }, + }, + CssAtRule { + at_token: AT@342..345 "@" [Newline("\n"), Newline("\n")] [], + rule: ScssImportAtRule { + import_token: IMPORT_KW@345..352 "import" [] [Whitespace(" ")], + imports: ScssImportItemList [ + ScssPlainImport { + url: CssString { + value_token: CSS_STRING_LITERAL@352..373 "\"//example.com/theme\"" [] [], + }, + layer: missing (optional), + supports: missing (optional), + media: CssMediaQueryList [], + }, + ], + semicolon_token: SEMICOLON@373..374 ";" [] [], + }, + }, + CssAtRule { + at_token: AT@374..377 "@" [Newline("\n"), Newline("\n")] [], + rule: ScssImportAtRule { + import_token: IMPORT_KW@377..384 "import" [] [Whitespace(" ")], + imports: ScssImportItemList [ + ScssPlainImport { + url: CssString { + value_token: CSS_STRING_LITERAL@384..415 "\"https://example.com/theme.css\"" [] [], + }, + layer: missing (optional), + supports: missing (optional), + media: CssMediaQueryList [], + }, + ], + semicolon_token: SEMICOLON@415..416 ";" [] [], + }, + }, + CssAtRule { + at_token: AT@416..419 "@" [Newline("\n"), Newline("\n")] [], + rule: ScssMixinAtRule { + mixin_token: MIXIN_KW@419..425 "mixin" [] [Whitespace(" ")], + name: CssIdentifier { + value_token: IDENT@425..438 "plain-import" [] [Whitespace(" ")], + }, + parameters: missing (optional), + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@438..439 "{" [] [], + items: CssDeclarationOrRuleList [ + CssAtRule { + at_token: AT@439..443 "@" [Newline("\n"), Whitespace(" ")] [], + rule: ScssImportAtRule { + import_token: IMPORT_KW@443..450 "import" [] [Whitespace(" ")], + imports: ScssImportItemList [ + ScssPlainImport { + url: CssString { + value_token: CSS_STRING_LITERAL@450..461 "\"theme.css\"" [] [], + }, + layer: missing (optional), + supports: missing (optional), + media: CssMediaQueryList [], + }, + ], + semicolon_token: SEMICOLON@461..462 ";" [] [], + }, + }, + ], + r_curly_token: R_CURLY@462..464 "}" [Newline("\n")] [], + }, + }, + }, + CssAtRule { + at_token: AT@464..467 "@" [Newline("\n"), Newline("\n")] [], + rule: ScssMixinAtRule { + mixin_token: MIXIN_KW@467..473 "mixin" [] [Whitespace(" ")], + name: CssIdentifier { + value_token: IDENT@473..488 "dynamic-import" [] [Whitespace(" ")], + }, + parameters: missing (optional), + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@488..489 "{" [] [], + items: CssDeclarationOrRuleList [ + CssAtRule { + at_token: AT@489..493 "@" [Newline("\n"), Whitespace(" ")] [], + rule: ScssImportAtRule { + import_token: IMPORT_KW@493..500 "import" [] [Whitespace(" ")], + imports: ScssImportItemList [ + CssString { + value_token: CSS_STRING_LITERAL@500..507 "\"theme\"" [] [], + }, + ], + semicolon_token: SEMICOLON@507..508 ";" [] [], + }, + }, + ], + r_curly_token: R_CURLY@508..510 "}" [Newline("\n")] [], + }, + }, + }, + CssAtRule { + at_token: AT@510..513 "@" [Newline("\n"), Newline("\n")] [], + rule: ScssIfAtRule { + if_token: IF_KW@513..516 "if" [] [Whitespace(" ")], + condition: ScssExpression { + items: ScssExpressionItemList [ + CssIdentifier { + value_token: IDENT@516..521 "true" [] [Whitespace(" ")], + }, + ], + }, + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@521..522 "{" [] [], + items: CssDeclarationOrRuleList [ + CssAtRule { + at_token: AT@522..526 "@" [Newline("\n"), Whitespace(" ")] [], + rule: ScssImportAtRule { + import_token: IMPORT_KW@526..533 "import" [] [Whitespace(" ")], + imports: ScssImportItemList [ + CssString { + value_token: CSS_STRING_LITERAL@533..540 "\"theme\"" [] [], + }, + ], + semicolon_token: SEMICOLON@540..541 ";" [] [], + }, + }, + ], + r_curly_token: R_CURLY@541..543 "}" [Newline("\n")] [], + }, + else_clause: missing (optional), + }, + }, + ], + eof_token: EOF@543..544 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: CSS_ROOT@0..544 + 0: (empty) + 1: CSS_ROOT_ITEM_LIST@0..543 + 0: CSS_AT_RULE@0..16 + 0: AT@0..1 "@" [] [] + 1: SCSS_IMPORT_AT_RULE@1..16 + 0: IMPORT_KW@1..8 "import" [] [Whitespace(" ")] + 1: SCSS_IMPORT_ITEM_LIST@8..15 + 0: CSS_STRING@8..15 + 0: CSS_STRING_LITERAL@8..15 "\"theme\"" [] [] + 2: SEMICOLON@15..16 ";" [] [] + 1: CSS_AT_RULE@16..59 + 0: AT@16..19 "@" [Newline("\n"), Newline("\n")] [] + 1: SCSS_IMPORT_AT_RULE@19..59 + 0: IMPORT_KW@19..26 "import" [] [Whitespace(" ")] + 1: SCSS_IMPORT_ITEM_LIST@26..58 + 0: CSS_STRING@26..43 + 0: CSS_STRING_LITERAL@26..43 "\"rounded-corners\"" [] [] + 1: COMMA@43..45 "," [] [Whitespace(" ")] + 2: CSS_STRING@45..58 + 0: CSS_STRING_LITERAL@45..58 "\"text-shadow\"" [] [] + 2: SEMICOLON@58..59 ";" [] [] + 2: CSS_AT_RULE@59..82 + 0: AT@59..62 "@" [Newline("\n"), Newline("\n")] [] + 1: SCSS_IMPORT_AT_RULE@62..82 + 0: IMPORT_KW@62..69 "import" [] [Whitespace(" ")] + 1: SCSS_IMPORT_ITEM_LIST@69..81 + 0: CSS_STRING@69..81 + 0: CSS_STRING_LITERAL@69..81 "\"theme.scss\"" [] [] + 2: SEMICOLON@81..82 ";" [] [] + 3: CSS_AT_RULE@82..115 + 0: AT@82..85 "@" [Newline("\n"), Newline("\n")] [] + 1: SCSS_IMPORT_AT_RULE@85..115 + 0: IMPORT_KW@85..92 "import" [] [Whitespace(" ")] + 1: SCSS_IMPORT_ITEM_LIST@92..114 + 0: SCSS_PLAIN_IMPORT@92..103 + 0: CSS_STRING@92..103 + 0: CSS_STRING_LITERAL@92..103 "\"theme.css\"" [] [] + 1: (empty) + 2: (empty) + 3: CSS_MEDIA_QUERY_LIST@103..103 + 1: COMMA@103..105 "," [] [Whitespace(" ")] + 2: CSS_STRING@105..114 + 0: CSS_STRING_LITERAL@105..114 "\"spacing\"" [] [] + 2: SEMICOLON@114..115 ";" [] [] + 4: CSS_AT_RULE@115..137 + 0: AT@115..118 "@" [Newline("\n"), Newline("\n")] [] + 1: SCSS_IMPORT_AT_RULE@118..137 + 0: IMPORT_KW@118..125 "import" [] [Whitespace(" ")] + 1: SCSS_IMPORT_ITEM_LIST@125..136 + 0: SCSS_PLAIN_IMPORT@125..136 + 0: CSS_STRING@125..136 + 0: CSS_STRING_LITERAL@125..136 "\"theme.css\"" [] [] + 1: (empty) + 2: (empty) + 3: CSS_MEDIA_QUERY_LIST@136..136 + 2: SEMICOLON@136..137 ";" [] [] + 5: CSS_AT_RULE@137..175 + 0: AT@137..140 "@" [Newline("\n"), Newline("\n")] [] + 1: SCSS_IMPORT_AT_RULE@140..175 + 0: IMPORT_KW@140..147 "import" [] [Whitespace(" ")] + 1: SCSS_IMPORT_ITEM_LIST@147..174 + 0: CSS_STRING@147..174 + 0: CSS_STRING_LITERAL@147..174 "\"theme.CSS?cache=1#section\"" [] [] + 2: SEMICOLON@174..175 ";" [] [] + 6: CSS_AT_RULE@175..203 + 0: AT@175..178 "@" [Newline("\n"), Newline("\n")] [] + 1: SCSS_IMPORT_AT_RULE@178..203 + 0: IMPORT_KW@178..185 "import" [] [Whitespace(" ")] + 1: SCSS_IMPORT_ITEM_LIST@185..202 + 0: SCSS_PLAIN_IMPORT@185..202 + 0: CSS_STRING@185..197 + 0: CSS_STRING_LITERAL@185..197 "\"print.css\"" [] [Whitespace(" ")] + 1: (empty) + 2: (empty) + 3: CSS_MEDIA_QUERY_LIST@197..202 + 0: CSS_MEDIA_TYPE_QUERY@197..202 + 0: (empty) + 1: CSS_MEDIA_TYPE@197..202 + 0: CSS_IDENTIFIER@197..202 + 0: IDENT@197..202 "print" [] [] + 2: SEMICOLON@202..203 ";" [] [] + 7: CSS_AT_RULE@203..239 + 0: AT@203..206 "@" [Newline("\n"), Newline("\n")] [] + 1: SCSS_IMPORT_AT_RULE@206..239 + 0: IMPORT_KW@206..213 "import" [] [Whitespace(" ")] + 1: SCSS_IMPORT_ITEM_LIST@213..238 + 0: SCSS_PLAIN_IMPORT@213..238 + 0: CSS_STRING@213..225 + 0: CSS_STRING_LITERAL@213..225 "\"print.css\"" [] [Whitespace(" ")] + 1: (empty) + 2: (empty) + 3: CSS_MEDIA_QUERY_LIST@225..238 + 0: CSS_MEDIA_TYPE_QUERY@225..230 + 0: (empty) + 1: CSS_MEDIA_TYPE@225..230 + 0: CSS_IDENTIFIER@225..230 + 0: IDENT@225..230 "print" [] [] + 1: COMMA@230..232 "," [] [Whitespace(" ")] + 2: CSS_MEDIA_TYPE_QUERY@232..238 + 0: (empty) + 1: CSS_MEDIA_TYPE@232..238 + 0: CSS_IDENTIFIER@232..238 + 0: IDENT@232..238 "screen" [] [] + 2: SEMICOLON@238..239 ";" [] [] + 8: CSS_AT_RULE@239..278 + 0: AT@239..242 "@" [Newline("\n"), Newline("\n")] [] + 1: SCSS_IMPORT_AT_RULE@242..278 + 0: IMPORT_KW@242..249 "import" [] [Whitespace(" ")] + 1: SCSS_IMPORT_ITEM_LIST@249..277 + 0: SCSS_PLAIN_IMPORT@249..266 + 0: CSS_STRING@249..261 + 0: CSS_STRING_LITERAL@249..261 "\"print.css\"" [] [Whitespace(" ")] + 1: (empty) + 2: (empty) + 3: CSS_MEDIA_QUERY_LIST@261..266 + 0: CSS_MEDIA_TYPE_QUERY@261..266 + 0: (empty) + 1: CSS_MEDIA_TYPE@261..266 + 0: CSS_IDENTIFIER@261..266 + 0: IDENT@261..266 "print" [] [] + 1: COMMA@266..268 "," [] [Whitespace(" ")] + 2: CSS_STRING@268..277 + 0: CSS_STRING_LITERAL@268..277 "\"spacing\"" [] [] + 2: SEMICOLON@277..278 ";" [] [] + 9: CSS_AT_RULE@278..315 + 0: AT@278..281 "@" [Newline("\n"), Newline("\n")] [] + 1: SCSS_IMPORT_AT_RULE@281..315 + 0: IMPORT_KW@281..288 "import" [] [Whitespace(" ")] + 1: SCSS_IMPORT_ITEM_LIST@288..314 + 0: SCSS_PLAIN_IMPORT@288..314 + 0: CSS_STRING@288..314 + 0: CSS_STRING_LITERAL@288..314 "\"http://example.com/theme\"" [] [] + 1: (empty) + 2: (empty) + 3: CSS_MEDIA_QUERY_LIST@314..314 + 2: SEMICOLON@314..315 ";" [] [] + 10: CSS_AT_RULE@315..342 + 0: AT@315..318 "@" [Newline("\n"), Newline("\n")] [] + 1: SCSS_IMPORT_AT_RULE@318..342 + 0: IMPORT_KW@318..325 "import" [] [Whitespace(" ")] + 1: SCSS_IMPORT_ITEM_LIST@325..341 + 0: SCSS_PLAIN_IMPORT@325..341 + 0: CSS_URL_FUNCTION@325..341 + 0: URL_KW@325..328 "url" [] [] + 1: L_PAREN@328..329 "(" [] [] + 2: CSS_STRING@329..340 + 0: CSS_STRING_LITERAL@329..340 "\"theme.css\"" [] [] + 3: CSS_URL_MODIFIER_LIST@340..340 + 4: R_PAREN@340..341 ")" [] [] + 1: (empty) + 2: (empty) + 3: CSS_MEDIA_QUERY_LIST@341..341 + 2: SEMICOLON@341..342 ";" [] [] + 11: CSS_AT_RULE@342..374 + 0: AT@342..345 "@" [Newline("\n"), Newline("\n")] [] + 1: SCSS_IMPORT_AT_RULE@345..374 + 0: IMPORT_KW@345..352 "import" [] [Whitespace(" ")] + 1: SCSS_IMPORT_ITEM_LIST@352..373 + 0: SCSS_PLAIN_IMPORT@352..373 + 0: CSS_STRING@352..373 + 0: CSS_STRING_LITERAL@352..373 "\"//example.com/theme\"" [] [] + 1: (empty) + 2: (empty) + 3: CSS_MEDIA_QUERY_LIST@373..373 + 2: SEMICOLON@373..374 ";" [] [] + 12: CSS_AT_RULE@374..416 + 0: AT@374..377 "@" [Newline("\n"), Newline("\n")] [] + 1: SCSS_IMPORT_AT_RULE@377..416 + 0: IMPORT_KW@377..384 "import" [] [Whitespace(" ")] + 1: SCSS_IMPORT_ITEM_LIST@384..415 + 0: SCSS_PLAIN_IMPORT@384..415 + 0: CSS_STRING@384..415 + 0: CSS_STRING_LITERAL@384..415 "\"https://example.com/theme.css\"" [] [] + 1: (empty) + 2: (empty) + 3: CSS_MEDIA_QUERY_LIST@415..415 + 2: SEMICOLON@415..416 ";" [] [] + 13: CSS_AT_RULE@416..464 + 0: AT@416..419 "@" [Newline("\n"), Newline("\n")] [] + 1: SCSS_MIXIN_AT_RULE@419..464 + 0: MIXIN_KW@419..425 "mixin" [] [Whitespace(" ")] + 1: CSS_IDENTIFIER@425..438 + 0: IDENT@425..438 "plain-import" [] [Whitespace(" ")] + 2: (empty) + 3: CSS_DECLARATION_OR_RULE_BLOCK@438..464 + 0: L_CURLY@438..439 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@439..462 + 0: CSS_AT_RULE@439..462 + 0: AT@439..443 "@" [Newline("\n"), Whitespace(" ")] [] + 1: SCSS_IMPORT_AT_RULE@443..462 + 0: IMPORT_KW@443..450 "import" [] [Whitespace(" ")] + 1: SCSS_IMPORT_ITEM_LIST@450..461 + 0: SCSS_PLAIN_IMPORT@450..461 + 0: CSS_STRING@450..461 + 0: CSS_STRING_LITERAL@450..461 "\"theme.css\"" [] [] + 1: (empty) + 2: (empty) + 3: CSS_MEDIA_QUERY_LIST@461..461 + 2: SEMICOLON@461..462 ";" [] [] + 2: R_CURLY@462..464 "}" [Newline("\n")] [] + 14: CSS_AT_RULE@464..510 + 0: AT@464..467 "@" [Newline("\n"), Newline("\n")] [] + 1: SCSS_MIXIN_AT_RULE@467..510 + 0: MIXIN_KW@467..473 "mixin" [] [Whitespace(" ")] + 1: CSS_IDENTIFIER@473..488 + 0: IDENT@473..488 "dynamic-import" [] [Whitespace(" ")] + 2: (empty) + 3: CSS_DECLARATION_OR_RULE_BLOCK@488..510 + 0: L_CURLY@488..489 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@489..508 + 0: CSS_AT_RULE@489..508 + 0: AT@489..493 "@" [Newline("\n"), Whitespace(" ")] [] + 1: SCSS_IMPORT_AT_RULE@493..508 + 0: IMPORT_KW@493..500 "import" [] [Whitespace(" ")] + 1: SCSS_IMPORT_ITEM_LIST@500..507 + 0: CSS_STRING@500..507 + 0: CSS_STRING_LITERAL@500..507 "\"theme\"" [] [] + 2: SEMICOLON@507..508 ";" [] [] + 2: R_CURLY@508..510 "}" [Newline("\n")] [] + 15: CSS_AT_RULE@510..543 + 0: AT@510..513 "@" [Newline("\n"), Newline("\n")] [] + 1: SCSS_IF_AT_RULE@513..543 + 0: IF_KW@513..516 "if" [] [Whitespace(" ")] + 1: SCSS_EXPRESSION@516..521 + 0: SCSS_EXPRESSION_ITEM_LIST@516..521 + 0: CSS_IDENTIFIER@516..521 + 0: IDENT@516..521 "true" [] [Whitespace(" ")] + 2: CSS_DECLARATION_OR_RULE_BLOCK@521..543 + 0: L_CURLY@521..522 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@522..541 + 0: CSS_AT_RULE@522..541 + 0: AT@522..526 "@" [Newline("\n"), Whitespace(" ")] [] + 1: SCSS_IMPORT_AT_RULE@526..541 + 0: IMPORT_KW@526..533 "import" [] [Whitespace(" ")] + 1: SCSS_IMPORT_ITEM_LIST@533..540 + 0: CSS_STRING@533..540 + 0: CSS_STRING_LITERAL@533..540 "\"theme\"" [] [] + 2: SEMICOLON@540..541 ";" [] [] + 2: R_CURLY@541..543 "}" [Newline("\n")] [] + 3: (empty) + 2: EOF@543..544 "" [Newline("\n")] [] + +``` diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/supports-expression.scss.snap b/crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/supports-expression.scss.snap index bd28509cd9a7..05b89962037b 100644 --- a/crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/supports-expression.scss.snap +++ b/crates/biome_css_parser/tests/css_test_suite/ok/scss/at-rule/supports-expression.scss.snap @@ -1,5 +1,6 @@ --- source: crates/biome_css_parser/tests/spec_test.rs +assertion_line: 208 expression: snapshot --- @@ -45,43 +46,47 @@ CssRoot { }, CssAtRule { at_token: AT@10..13 "@" [Newline("\n"), Newline("\n")] [], - rule: CssImportAtRule { + rule: ScssImportAtRule { import_token: IMPORT_KW@13..20 "import" [] [Whitespace(" ")], - url: CssString { - value_token: CSS_STRING_LITERAL@20..32 "\"theme.css\"" [] [Whitespace(" ")], - }, - layer: missing (optional), - supports: CssImportSupports { - supports_token: SUPPORTS_KW@32..40 "supports" [] [], - l_paren_token: L_PAREN@40..41 "(" [] [], - condition: CssDeclaration { - property: CssGenericProperty { - name: CssIdentifier { - value_token: IDENT@41..46 "width" [] [], - }, - colon_token: COLON@46..48 ":" [] [Whitespace(" ")], - value: ScssExpression { - items: ScssExpressionItemList [ - ScssBinaryExpression { - left: ScssIdentifier { - dollar_token: DOLLAR@48..49 "$" [] [], - name: CssIdentifier { - value_token: IDENT@49..54 "base" [] [Whitespace(" ")], + imports: ScssImportItemList [ + ScssPlainImport { + url: CssString { + value_token: CSS_STRING_LITERAL@20..32 "\"theme.css\"" [] [Whitespace(" ")], + }, + layer: missing (optional), + supports: CssImportSupports { + supports_token: SUPPORTS_KW@32..40 "supports" [] [], + l_paren_token: L_PAREN@40..41 "(" [] [], + condition: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@41..46 "width" [] [], + }, + colon_token: COLON@46..48 ":" [] [Whitespace(" ")], + value: ScssExpression { + items: ScssExpressionItemList [ + ScssBinaryExpression { + left: ScssIdentifier { + dollar_token: DOLLAR@48..49 "$" [] [], + name: CssIdentifier { + value_token: IDENT@49..54 "base" [] [Whitespace(" ")], + }, + }, + operator: PLUS@54..56 "+" [] [Whitespace(" ")], + right: CssNumber { + value_token: CSS_NUMBER_LITERAL@56..57 "1" [] [], + }, }, - }, - operator: PLUS@54..56 "+" [] [Whitespace(" ")], - right: CssNumber { - value_token: CSS_NUMBER_LITERAL@56..57 "1" [] [], - }, + ], }, - ], + }, + important: missing (optional), }, + r_paren_token: R_PAREN@57..58 ")" [] [], }, - important: missing (optional), + media: CssMediaQueryList [], }, - r_paren_token: R_PAREN@57..58 ")" [] [], - }, - media: CssMediaQueryList [], + ], semicolon_token: SEMICOLON@58..59 ";" [] [], }, }, @@ -203,33 +208,35 @@ CssRoot { 4: SEMICOLON@9..10 ";" [] [] 1: CSS_AT_RULE@10..59 0: AT@10..13 "@" [Newline("\n"), Newline("\n")] [] - 1: CSS_IMPORT_AT_RULE@13..59 + 1: SCSS_IMPORT_AT_RULE@13..59 0: IMPORT_KW@13..20 "import" [] [Whitespace(" ")] - 1: CSS_STRING@20..32 - 0: CSS_STRING_LITERAL@20..32 "\"theme.css\"" [] [Whitespace(" ")] - 2: (empty) - 3: CSS_IMPORT_SUPPORTS@32..58 - 0: SUPPORTS_KW@32..40 "supports" [] [] - 1: L_PAREN@40..41 "(" [] [] - 2: CSS_DECLARATION@41..57 - 0: CSS_GENERIC_PROPERTY@41..57 - 0: CSS_IDENTIFIER@41..46 - 0: IDENT@41..46 "width" [] [] - 1: COLON@46..48 ":" [] [Whitespace(" ")] - 2: SCSS_EXPRESSION@48..57 - 0: SCSS_EXPRESSION_ITEM_LIST@48..57 - 0: SCSS_BINARY_EXPRESSION@48..57 - 0: SCSS_IDENTIFIER@48..54 - 0: DOLLAR@48..49 "$" [] [] - 1: CSS_IDENTIFIER@49..54 - 0: IDENT@49..54 "base" [] [Whitespace(" ")] - 1: PLUS@54..56 "+" [] [Whitespace(" ")] - 2: CSS_NUMBER@56..57 - 0: CSS_NUMBER_LITERAL@56..57 "1" [] [] + 1: SCSS_IMPORT_ITEM_LIST@20..58 + 0: SCSS_PLAIN_IMPORT@20..58 + 0: CSS_STRING@20..32 + 0: CSS_STRING_LITERAL@20..32 "\"theme.css\"" [] [Whitespace(" ")] 1: (empty) - 3: R_PAREN@57..58 ")" [] [] - 4: CSS_MEDIA_QUERY_LIST@58..58 - 5: SEMICOLON@58..59 ";" [] [] + 2: CSS_IMPORT_SUPPORTS@32..58 + 0: SUPPORTS_KW@32..40 "supports" [] [] + 1: L_PAREN@40..41 "(" [] [] + 2: CSS_DECLARATION@41..57 + 0: CSS_GENERIC_PROPERTY@41..57 + 0: CSS_IDENTIFIER@41..46 + 0: IDENT@41..46 "width" [] [] + 1: COLON@46..48 ":" [] [Whitespace(" ")] + 2: SCSS_EXPRESSION@48..57 + 0: SCSS_EXPRESSION_ITEM_LIST@48..57 + 0: SCSS_BINARY_EXPRESSION@48..57 + 0: SCSS_IDENTIFIER@48..54 + 0: DOLLAR@48..49 "$" [] [] + 1: CSS_IDENTIFIER@49..54 + 0: IDENT@49..54 "base" [] [Whitespace(" ")] + 1: PLUS@54..56 "+" [] [Whitespace(" ")] + 2: CSS_NUMBER@56..57 + 0: CSS_NUMBER_LITERAL@56..57 "1" [] [] + 1: (empty) + 3: R_PAREN@57..58 ")" [] [] + 3: CSS_MEDIA_QUERY_LIST@58..58 + 2: SEMICOLON@58..59 ";" [] [] 2: CSS_QUALIFIED_RULE@59..128 0: CSS_SELECTOR_LIST@59..66 0: CSS_COMPOUND_SELECTOR@59..66 diff --git a/crates/biome_css_semantic/src/semantic_model/specificity.rs b/crates/biome_css_semantic/src/semantic_model/specificity.rs index 39f4987e177c..60f5faab02b4 100644 --- a/crates/biome_css_semantic/src/semantic_model/specificity.rs +++ b/crates/biome_css_semantic/src/semantic_model/specificity.rs @@ -17,6 +17,7 @@ fn evaluate_any_simple_selector(selector: &AnyCssSimpleSelector) -> Specificity match selector { AnyCssSimpleSelector::CssTypeSelector(_) => TYPE_SPECIFICITY, AnyCssSimpleSelector::CssUniversalSelector(_) => ZERO_SPECIFICITY, + AnyCssSimpleSelector::ScssPlaceholderSelector(_) => CLASS_SPECIFICITY, } } diff --git a/crates/biome_css_syntax/src/generated/kind.rs b/crates/biome_css_syntax/src/generated/kind.rs index 3d930de2f624..ea43495ac5d6 100644 --- a/crates/biome_css_syntax/src/generated/kind.rs +++ b/crates/biome_css_syntax/src/generated/kind.rs @@ -100,11 +100,13 @@ pub enum CssSyntaxKind { WARN_KW, ERROR_KW, CONTENT_KW, + EXTEND_KW, FOR_KW, FORWARD_KW, HIDE_KW, INCLUDE_KW, MIXIN_KW, + OPTIONAL_KW, RETURN_KW, SHOW_KW, USE_KW, @@ -569,6 +571,13 @@ pub enum CssSyntaxKind { SCSS_HIDE_CLAUSE, SCSS_IF_AT_RULE, SCSS_CONTENT_AT_RULE, + SCSS_EXTEND_AT_RULE, + SCSS_EXTEND_OPTIONAL_MODIFIER, + SCSS_EXTEND_TARGET_TOKEN, + SCSS_EXTEND_TARGET_VALUE, + SCSS_EXTEND_TARGET_COMPONENT_LIST, + SCSS_IMPORT_AT_RULE, + SCSS_IMPORT_ITEM_LIST, SCSS_INCLUDE_AT_RULE, SCSS_INCLUDE_ARGUMENT_LIST, SCSS_MIXIN_AT_RULE, @@ -580,6 +589,9 @@ pub enum CssSyntaxKind { SCSS_PARAMETER_ITEM_LIST, SCSS_PARAMETER, SCSS_PARAMETER_DEFAULT_VALUE, + SCSS_PLACEHOLDER_SELECTOR, + SCSS_INTERPOLATION, + SCSS_PLAIN_IMPORT, SCSS_SHOW_CLAUSE, SCSS_USE_ALL_NAMESPACE, SCSS_USE_AS_CLAUSE, @@ -781,6 +793,8 @@ impl CssSyntaxKind { | CSS_VALUE_AT_RULE_IMPORT_SPECIFIER_LIST | CSS_FUNCTION_PARAMETER_LIST | SCSS_EACH_BINDING_LIST + | SCSS_EXTEND_TARGET_COMPONENT_LIST + | SCSS_IMPORT_ITEM_LIST | SCSS_INCLUDE_ARGUMENT_LIST | SCSS_MODULE_CONFIGURATION_LIST | SCSS_MODULE_CONFIGURATION_ITEM_LIST @@ -840,11 +854,13 @@ impl CssSyntaxKind { "warn" => WARN_KW, "error" => ERROR_KW, "content" => CONTENT_KW, + "extend" => EXTEND_KW, "for" => FOR_KW, "forward" => FORWARD_KW, "hide" => HIDE_KW, "include" => INCLUDE_KW, "mixin" => MIXIN_KW, + "optional" => OPTIONAL_KW, "return" => RETURN_KW, "show" => SHOW_KW, "use" => USE_KW, @@ -1110,11 +1126,13 @@ impl CssSyntaxKind { WARN_KW => "warn", ERROR_KW => "error", CONTENT_KW => "content", + EXTEND_KW => "extend", FOR_KW => "for", FORWARD_KW => "forward", HIDE_KW => "hide", INCLUDE_KW => "include", MIXIN_KW => "mixin", + OPTIONAL_KW => "optional", RETURN_KW => "return", SHOW_KW => "show", USE_KW => "use", @@ -1295,4 +1313,4 @@ impl CssSyntaxKind { } #[doc = r" Utility macro for creating a SyntaxKind through simple macro syntax"] #[macro_export] -macro_rules ! T { [;] => { $ crate :: CssSyntaxKind :: SEMICOLON } ; [,] => { $ crate :: CssSyntaxKind :: COMMA } ; ['('] => { $ crate :: CssSyntaxKind :: L_PAREN } ; [')'] => { $ crate :: CssSyntaxKind :: R_PAREN } ; ['{'] => { $ crate :: CssSyntaxKind :: L_CURLY } ; ['}'] => { $ crate :: CssSyntaxKind :: R_CURLY } ; ['['] => { $ crate :: CssSyntaxKind :: L_BRACK } ; [']'] => { $ crate :: CssSyntaxKind :: R_BRACK } ; [<] => { $ crate :: CssSyntaxKind :: L_ANGLE } ; [>] => { $ crate :: CssSyntaxKind :: R_ANGLE } ; [~] => { $ crate :: CssSyntaxKind :: TILDE } ; [$] => { $ crate :: CssSyntaxKind :: DOLLAR } ; [#] => { $ crate :: CssSyntaxKind :: HASH } ; [&] => { $ crate :: CssSyntaxKind :: AMP } ; [|] => { $ crate :: CssSyntaxKind :: PIPE } ; [||] => { $ crate :: CssSyntaxKind :: PIPE2 } ; [+] => { $ crate :: CssSyntaxKind :: PLUS } ; [*] => { $ crate :: CssSyntaxKind :: STAR } ; [/] => { $ crate :: CssSyntaxKind :: SLASH } ; [^] => { $ crate :: CssSyntaxKind :: CARET } ; [%] => { $ crate :: CssSyntaxKind :: PERCENT } ; [.] => { $ crate :: CssSyntaxKind :: DOT } ; [...] => { $ crate :: CssSyntaxKind :: DOT3 } ; [:] => { $ crate :: CssSyntaxKind :: COLON } ; [::] => { $ crate :: CssSyntaxKind :: COLON2 } ; [=] => { $ crate :: CssSyntaxKind :: EQ } ; [==] => { $ crate :: CssSyntaxKind :: EQ2 } ; [!] => { $ crate :: CssSyntaxKind :: BANG } ; [!=] => { $ crate :: CssSyntaxKind :: NEQ } ; [-] => { $ crate :: CssSyntaxKind :: MINUS } ; [<=] => { $ crate :: CssSyntaxKind :: LTEQ } ; [>=] => { $ crate :: CssSyntaxKind :: GTEQ } ; [+=] => { $ crate :: CssSyntaxKind :: PLUSEQ } ; [|=] => { $ crate :: CssSyntaxKind :: PIPEEQ } ; [&=] => { $ crate :: CssSyntaxKind :: AMPEQ } ; [^=] => { $ crate :: CssSyntaxKind :: CARETEQ } ; [/=] => { $ crate :: CssSyntaxKind :: SLASHEQ } ; [*=] => { $ crate :: CssSyntaxKind :: STAREQ } ; [%=] => { $ crate :: CssSyntaxKind :: PERCENTEQ } ; [@] => { $ crate :: CssSyntaxKind :: AT } ; ["$="] => { $ crate :: CssSyntaxKind :: DOLLAR_EQ } ; [~=] => { $ crate :: CssSyntaxKind :: TILDE_EQ } ; [-->] => { $ crate :: CssSyntaxKind :: CDC } ; [] => { $ crate :: CssSyntaxKind :: CDC } ; [