Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/warm-foxes-shine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

Fixed [#7923](https://github.com/biomejs/biome/issues/7923). Added a new `css.parser.vueScopedCss` option that enables parsing of Vue SFC scoped CSS selectors (`:deep()`, `:slotted()`).
6 changes: 6 additions & 0 deletions crates/biome_configuration/src/css.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pub struct CssConfiguration {
pub type CssAllowWrongLineCommentsEnabled = Bool<false>;
pub type CssModulesEnabled = Bool<false>;
pub type CssTailwindDirectivesEnabled = Bool<false>;
pub type CssVueScopedCssEnabled = Bool<false>;

/// Options that changes how the CSS parser behaves
#[derive(
Expand All @@ -57,6 +58,11 @@ pub struct CssParserConfiguration {
/// Enables parsing of Tailwind CSS 4.0 directives and functions.
#[bpaf(long("css-parse-tailwind-directives"), argument("true|false"))]
pub tailwind_directives: Option<CssTailwindDirectivesEnabled>,

/// Enables parsing of Vue SFC scoped CSS selectors (`:deep()`, `:slotted()`).
#[serde(skip_serializing_if = "Option::is_none")]
#[bpaf(long("css-parse-vue-scoped-css"), argument("true|false"))]
pub vue_scoped_css: Option<CssVueScopedCssEnabled>,
}

pub type CssFormatterEnabled = Bool<true>;
Expand Down
2 changes: 2 additions & 0 deletions crates/biome_css_parser/src/lexer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,8 @@ impl<'src> CssLexer<'src> {
b"dir" => DIR_KW,
b"global" => GLOBAL_KW,
b"local" => LOCAL_KW,
b"deep" => DEEP_KW,
b"slotted" => SLOTTED_KW,
b"-moz-any" => ANY_KW,
b"-webkit-any" => ANY_KW,
b"past" => PAST_KW,
Expand Down
15 changes: 15 additions & 0 deletions crates/biome_css_parser/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ pub struct CssParserOptions {
/// Enables parsing of Tailwind CSS 4.0 directives and functions.
/// Defaults to `false`.
pub tailwind_directives: bool,

/// Enables parsing of Vue SFC scoped CSS selectors (`:deep()`, `:slotted()`).
/// Defaults to `false`.
pub vue_scoped_css: bool,
}

impl CssParserOptions {
Expand Down Expand Up @@ -64,6 +68,12 @@ impl CssParserOptions {
self
}

/// Enables parsing of Vue SFC scoped CSS selectors.
pub fn allow_vue_scoped_css(mut self) -> Self {
self.vue_scoped_css = true;
self
}

/// Checks if parsing of CSS Modules features is disabled.
pub fn is_css_modules_disabled(&self) -> bool {
!self.css_modules
Expand All @@ -78,6 +88,11 @@ impl CssParserOptions {
pub fn is_tailwind_directives_enabled(&self) -> bool {
self.tailwind_directives
}

/// Checks if parsing of Vue SFC scoped CSS selectors is enabled.
pub fn is_vue_scoped_css_enabled(&self) -> bool {
self.vue_scoped_css
}
}

impl<'source> CssParser<'source> {
Expand Down
1 change: 1 addition & 0 deletions crates/biome_css_parser/src/syntax/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod property;
mod selector;
mod util;
mod value;
mod vue_scoped_css;

use crate::lexer::CssLexContext;
use crate::parser::CssParser;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,50 +6,44 @@ use crate::syntax::selector::{
eat_or_recover_selector_function_close_token, parse_selector,
recover_selector_function_parameter,
};
use crate::syntax::vue_scoped_css::{VUE_SCOPED_CSS_SET, vue_scoped_css_not_allowed};
use biome_css_syntax::CssSyntaxKind::*;
use biome_css_syntax::CssSyntaxKind::{self, CSS_PSEUDO_CLASS_FUNCTION_SELECTOR};
use biome_css_syntax::T;
use biome_parser::Parser;
use biome_parser::parsed_syntax::ParsedSyntax;
use biome_parser::parsed_syntax::ParsedSyntax::{Absent, Present};

/// Checks if the current parser position is at a pseudo-class function selector for CSS Modules.
///
/// This function determines if the parser is currently positioned at the start of a `:local` or `:global`
/// pseudo-class function selector, which is part of the CSS Modules syntax.
/// Checks if the current parser position is at a pseudo-class function selector
/// (`:local()`, `:global()` for CSS Modules; `:deep()`, `:slotted()` for Vue SFC).
#[inline]
pub(crate) fn is_at_pseudo_class_function_selector(p: &mut CssParser) -> bool {
p.at_ts(CSS_MODULES_SCOPE_SET) && p.nth_at(1, T!['('])
(p.at_ts(CSS_MODULES_SCOPE_SET) || p.at_ts(VUE_SCOPED_CSS_SET)) && p.nth_at(1, T!['('])
}

/// Parses a pseudo-class function selector for CSS Modules.
/// Parses a pseudo-class function selector for CSS Modules (`:local()`, `:global()`)
/// or Vue SFC scoped CSS (`:deep()`, `:slotted()`).
///
/// This function parses a pseudo-class function selector, specifically `:local` or `:global`, in CSS Modules.
/// If the `css.parser.cssModules` option is not enabled, it generates a diagnostic error and skips the selector.
/// ```css
/// :local(.className) {
/// color: red;
/// }
/// :global(.globalClass) .nestedClass {
/// padding: 10px;
/// }
/// :local(.className) > :global(.globalClass) {
/// margin: 0;
/// }
/// ```
/// If the corresponding parser option is not enabled, emits a diagnostic and skips the selector.
#[inline]
pub(crate) fn parse_pseudo_class_function_selector(p: &mut CssParser) -> ParsedSyntax {
if !is_at_pseudo_class_function_selector(p) {
return Absent;
}

if p.options().is_css_modules_disabled() {
// :local and :global are not standard CSS features
// provide a hint on how to enable parsing of these pseudo-classes
p.error(local_or_global_not_allowed(p, p.cur_range()));
// Determine which feature this selector belongs to and whether it's enabled.
let disabled_error = if p.at_ts(VUE_SCOPED_CSS_SET) {
(!p.options().is_vue_scoped_css_enabled())
.then(|| vue_scoped_css_not_allowed(p, p.cur_range()))
} else {
p.options()
.is_css_modules_disabled()
.then(|| local_or_global_not_allowed(p, p.cur_range()))
};

if let Some(error) = disabled_error {
p.error(error);

// Skip the entire pseudo-class function selector
// Skip until the next closing parenthesis
while !p.eat(T![')']) && !p.at(CssSyntaxKind::EOF) {
p.bump_any();
}
Expand Down
20 changes: 20 additions & 0 deletions crates/biome_css_parser/src/syntax/vue_scoped_css.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use crate::parser::CssParser;
use biome_css_syntax::{CssSyntaxKind, T, TextRange};
use biome_parser::diagnostic::ParseDiagnostic;
use biome_parser::{Parser, TokenSet, token_set};

/// A set of tokens representing the Vue SFC scoped CSS selectors `:deep` and `:slotted`.
///
/// Note: `:global` is shared with CSS Modules and handled separately.
pub(crate) const VUE_SCOPED_CSS_SET: TokenSet<CssSyntaxKind> = token_set![T![deep], T![slotted]];

/// Generates a parse diagnostic for when Vue SFC scoped CSS selectors are not allowed.
pub(crate) fn vue_scoped_css_not_allowed(p: &CssParser, range: TextRange) -> ParseDiagnostic {
p.err_builder(
"`:deep` and `:slotted` are Vue SFC scoped CSS selectors, not standard CSS features.",
range,
)
.with_hint(
"You can enable Vue SFC scoped CSS parsing by setting the `css.parser.vueScopedCss` option to `true` in your configuration file.",
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:deep(.b) { color: red; }
:slotted(.b) { color: red; }
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
---
source: crates/biome_css_parser/tests/spec_test.rs
expression: snapshot
---

## Input

```css
:deep(.b) { color: red; }
:slotted(.b) { color: red; }

```


## AST

```
CssRoot {
bom_token: missing (optional),
rules: CssRuleList [
CssQualifiedRule {
prelude: CssSelectorList [
CssCompoundSelector {
nesting_selectors: CssNestedSelectorList [],
simple_selector: missing (optional),
sub_selectors: CssSubSelectorList [
CssBogusSubSelector {
items: [
COLON@0..1 ":" [] [],
DEEP_KW@1..5 "deep" [] [],
L_PAREN@5..6 "(" [] [],
DOT@6..7 "." [] [],
IDENT@7..8 "b" [] [],
R_PAREN@8..10 ")" [] [Whitespace(" ")],
],
},
],
},
],
block: CssDeclarationOrRuleBlock {
l_curly_token: L_CURLY@10..12 "{" [] [Whitespace(" ")],
items: CssDeclarationOrRuleList [
CssDeclarationWithSemicolon {
declaration: CssDeclaration {
property: CssGenericProperty {
name: CssIdentifier {
value_token: IDENT@12..17 "color" [] [],
},
colon_token: COLON@17..19 ":" [] [Whitespace(" ")],
value: CssGenericComponentValueList [
CssIdentifier {
value_token: IDENT@19..22 "red" [] [],
},
],
},
important: missing (optional),
},
semicolon_token: SEMICOLON@22..24 ";" [] [Whitespace(" ")],
},
],
r_curly_token: R_CURLY@24..25 "}" [] [],
},
},
CssQualifiedRule {
prelude: CssSelectorList [
CssCompoundSelector {
nesting_selectors: CssNestedSelectorList [],
simple_selector: missing (optional),
sub_selectors: CssSubSelectorList [
CssBogusSubSelector {
items: [
COLON@25..27 ":" [Newline("\n")] [],
SLOTTED_KW@27..34 "slotted" [] [],
L_PAREN@34..35 "(" [] [],
DOT@35..36 "." [] [],
IDENT@36..37 "b" [] [],
R_PAREN@37..39 ")" [] [Whitespace(" ")],
],
},
],
},
],
block: CssDeclarationOrRuleBlock {
l_curly_token: L_CURLY@39..41 "{" [] [Whitespace(" ")],
items: CssDeclarationOrRuleList [
CssDeclarationWithSemicolon {
declaration: CssDeclaration {
property: CssGenericProperty {
name: CssIdentifier {
value_token: IDENT@41..46 "color" [] [],
},
colon_token: COLON@46..48 ":" [] [Whitespace(" ")],
value: CssGenericComponentValueList [
CssIdentifier {
value_token: IDENT@48..51 "red" [] [],
},
],
},
important: missing (optional),
},
semicolon_token: SEMICOLON@51..53 ";" [] [Whitespace(" ")],
},
],
r_curly_token: R_CURLY@53..54 "}" [] [],
},
},
],
eof_token: EOF@54..55 "" [Newline("\n")] [],
}
```

## CST

```
0: CSS_ROOT@0..55
0: (empty)
1: CSS_RULE_LIST@0..54
0: CSS_QUALIFIED_RULE@0..25
0: CSS_SELECTOR_LIST@0..10
0: CSS_COMPOUND_SELECTOR@0..10
0: CSS_NESTED_SELECTOR_LIST@0..0
1: (empty)
2: CSS_SUB_SELECTOR_LIST@0..10
0: CSS_BOGUS_SUB_SELECTOR@0..10
0: COLON@0..1 ":" [] []
1: DEEP_KW@1..5 "deep" [] []
2: L_PAREN@5..6 "(" [] []
3: DOT@6..7 "." [] []
4: IDENT@7..8 "b" [] []
5: R_PAREN@8..10 ")" [] [Whitespace(" ")]
1: CSS_DECLARATION_OR_RULE_BLOCK@10..25
0: L_CURLY@10..12 "{" [] [Whitespace(" ")]
1: CSS_DECLARATION_OR_RULE_LIST@12..24
0: CSS_DECLARATION_WITH_SEMICOLON@12..24
0: CSS_DECLARATION@12..22
0: CSS_GENERIC_PROPERTY@12..22
0: CSS_IDENTIFIER@12..17
0: IDENT@12..17 "color" [] []
1: COLON@17..19 ":" [] [Whitespace(" ")]
2: CSS_GENERIC_COMPONENT_VALUE_LIST@19..22
0: CSS_IDENTIFIER@19..22
0: IDENT@19..22 "red" [] []
1: (empty)
1: SEMICOLON@22..24 ";" [] [Whitespace(" ")]
2: R_CURLY@24..25 "}" [] []
1: CSS_QUALIFIED_RULE@25..54
0: CSS_SELECTOR_LIST@25..39
0: CSS_COMPOUND_SELECTOR@25..39
0: CSS_NESTED_SELECTOR_LIST@25..25
1: (empty)
2: CSS_SUB_SELECTOR_LIST@25..39
0: CSS_BOGUS_SUB_SELECTOR@25..39
0: COLON@25..27 ":" [Newline("\n")] []
1: SLOTTED_KW@27..34 "slotted" [] []
2: L_PAREN@34..35 "(" [] []
3: DOT@35..36 "." [] []
4: IDENT@36..37 "b" [] []
5: R_PAREN@37..39 ")" [] [Whitespace(" ")]
1: CSS_DECLARATION_OR_RULE_BLOCK@39..54
0: L_CURLY@39..41 "{" [] [Whitespace(" ")]
1: CSS_DECLARATION_OR_RULE_LIST@41..53
0: CSS_DECLARATION_WITH_SEMICOLON@41..53
0: CSS_DECLARATION@41..51
0: CSS_GENERIC_PROPERTY@41..51
0: CSS_IDENTIFIER@41..46
0: IDENT@41..46 "color" [] []
1: COLON@46..48 ":" [] [Whitespace(" ")]
2: CSS_GENERIC_COMPONENT_VALUE_LIST@48..51
0: CSS_IDENTIFIER@48..51
0: IDENT@48..51 "red" [] []
1: (empty)
1: SEMICOLON@51..53 ";" [] [Whitespace(" ")]
2: R_CURLY@53..54 "}" [] []
2: EOF@54..55 "" [Newline("\n")] []

```

## Diagnostics

```
pseudo_class_vue_disabled.css:1:2 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× `:deep` and `:slotted` are Vue SFC scoped CSS selectors, not standard CSS features.

> 1 │ :deep(.b) { color: red; }
│ ^^^^
2 │ :slotted(.b) { color: red; }
3 │

i You can enable Vue SFC scoped CSS parsing by setting the `css.parser.vueScopedCss` option to `true` in your configuration file.

pseudo_class_vue_disabled.css:2:2 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× `:deep` and `:slotted` are Vue SFC scoped CSS selectors, not standard CSS features.

1 │ :deep(.b) { color: red; }
> 2 │ :slotted(.b) { color: red; }
│ ^^^^^^^
3 │

i You can enable Vue SFC scoped CSS parsing by setting the `css.parser.vueScopedCss` option to `true` in your configuration file.

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "../../../../../../../../../packages/@biomejs/biome/configuration_schema.json",
"css": {
"parser": {
"vueScopedCss": true
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
:deep( {}
:deep() {}
:deep(.div, .class) {}
:deep(.div, .class {}
:deep(.div .class {}
:deep(.div .class
:slotted( {}
:slotted() {}
:slotted(.div, .class) {}
:slotted(.div, .class {}
:slotted(.div .class {}
:slotted(.div .class
Loading