diff --git a/crates/biome_tailwind_factory/src/generated/node_factory.rs b/crates/biome_tailwind_factory/src/generated/node_factory.rs index ddad208a832b..a2ed1f04e435 100644 --- a/crates/biome_tailwind_factory/src/generated/node_factory.rs +++ b/crates/biome_tailwind_factory/src/generated/node_factory.rs @@ -92,6 +92,20 @@ pub fn tw_css_variable_value( ], )) } +pub fn tw_data_attribute( + data_token: SyntaxToken, + minus_token: SyntaxToken, + value: AnyTwDataAttributeValue, +) -> TwDataAttribute { + TwDataAttribute::unwrap_cast(SyntaxNode::new_detached( + TailwindSyntaxKind::TW_DATA_ATTRIBUTE, + [ + Some(SyntaxElement::Token(data_token)), + Some(SyntaxElement::Token(minus_token)), + Some(SyntaxElement::Node(value.into_syntax())), + ], + )) +} pub fn tw_full_candidate( variants: TwVariantList, candidate: AnyTwCandidate, diff --git a/crates/biome_tailwind_factory/src/generated/syntax_factory.rs b/crates/biome_tailwind_factory/src/generated/syntax_factory.rs index b9cbe2494ebc..a3a040757f6f 100644 --- a/crates/biome_tailwind_factory/src/generated/syntax_factory.rs +++ b/crates/biome_tailwind_factory/src/generated/syntax_factory.rs @@ -169,6 +169,39 @@ impl SyntaxFactory for TailwindSyntaxFactory { } slots.into_node(TW_CSS_VARIABLE_VALUE, children) } + TW_DATA_ATTRIBUTE => { + 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![data] + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && element.kind() == T ! [-] + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && AnyTwDataAttributeValue::can_cast(element.kind()) + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if current_element.is_some() { + return RawSyntaxNode::new( + TW_DATA_ATTRIBUTE.to_bogus(), + children.into_iter().map(Some), + ); + } + slots.into_node(TW_DATA_ATTRIBUTE, children) + } TW_FULL_CANDIDATE => { let mut elements = (&children).into_iter(); let mut slots: RawNodeSlots<4usize> = RawNodeSlots::default(); diff --git a/crates/biome_tailwind_parser/src/lexer/mod.rs b/crates/biome_tailwind_parser/src/lexer/mod.rs index f9e3e192aa21..20d11b33d85a 100644 --- a/crates/biome_tailwind_parser/src/lexer/mod.rs +++ b/crates/biome_tailwind_parser/src/lexer/mod.rs @@ -45,7 +45,7 @@ impl<'src> TailwindLexer<'src> { bracket @ (b'[' | b']' | b'(' | b')') => self.consume_bracket(bracket), _ if self.current_kind == T!['['] => self.consume_bracketed_thing(TW_SELECTOR, b']'), _ if self.current_kind == T!['('] => self.consume_bracketed_thing(TW_VALUE, b')'), - _ if self.current_kind == T![-] => self.consume_named_value(), + _ if self.current_kind == T![-] => self.consume_after_dash(), _ if self.current_kind == T![/] => self.consume_modifier(), b':' => self.consume_byte(T![:]), b'-' => self.consume_byte(T![-]), @@ -130,7 +130,11 @@ impl<'src> TailwindLexer<'src> { let end = BASENAME_STORE.matcher(slice).base_end(); self.advance(end); - TW_BASE + if end == 4 && &slice[..end] == b"data" { + DATA_KW + } else { + TW_BASE + } } fn consume_named_value(&mut self) -> TailwindSyntaxKind { @@ -153,6 +157,24 @@ impl<'src> TailwindLexer<'src> { TW_VALUE } + /// After seeing a '-', we usually lex a value. However, if the next bytes are "data" + /// and they are followed by another '-', this is a Tailwind data-attribute variant. + /// In that case, emit DATA_KW so the parser can recognize `TwDataAttribute`. + fn consume_after_dash(&mut self) -> TailwindSyntaxKind { + self.assert_current_char_boundary(); + + let bytes = self.source.as_bytes(); + let slice = &bytes[self.position..]; + + if slice.len() >= 5 && &slice[..4] == b"data" && slice[4] == b'-' { + // Advance past "data" only. The following '-' will be emitted as its own token. + self.advance(4); + return DATA_KW; + } + + self.consume_named_value() + } + fn consume_modifier(&mut self) -> TailwindSyntaxKind { self.assert_current_char_boundary(); diff --git a/crates/biome_tailwind_parser/src/syntax/value.rs b/crates/biome_tailwind_parser/src/syntax/value.rs index 118bd4c482ff..a6e464575396 100644 --- a/crates/biome_tailwind_parser/src/syntax/value.rs +++ b/crates/biome_tailwind_parser/src/syntax/value.rs @@ -1,4 +1,5 @@ use crate::parser::TailwindParser; +use crate::syntax::variant::parse_data_attribute; use crate::token_source::TailwindLexContext; use biome_parser::Parser; use biome_parser::parsed_syntax::ParsedSyntax::{Absent, Present}; @@ -13,6 +14,9 @@ pub(crate) fn parse_value(p: &mut TailwindParser) -> ParsedSyntax { if p.at(T!['(']) { return parse_css_variable_value(p); } + if p.at(T![data]) { + return parse_data_attribute(p); + } parse_named_value(p) } diff --git a/crates/biome_tailwind_parser/src/syntax/variant.rs b/crates/biome_tailwind_parser/src/syntax/variant.rs index dc0ef0389357..8e22a0a7e03f 100644 --- a/crates/biome_tailwind_parser/src/syntax/variant.rs +++ b/crates/biome_tailwind_parser/src/syntax/variant.rs @@ -83,6 +83,9 @@ pub(crate) fn parse_variant(p: &mut TailwindParser) -> ParsedSyntax { // variants can't start with a negative sign return Absent; } + if p.at(T![data]) { + return parse_data_attribute(p); + } if p.at(T!['[']) { return parse_arbitrary_variant(p); } @@ -135,3 +138,16 @@ fn parse_static_or_functional_variant(p: &mut TailwindParser) -> ParsedSyntax { Present(m.complete(p, TW_FUNCTIONAL_VARIANT)) } + +pub(crate) fn parse_data_attribute(p: &mut TailwindParser) -> ParsedSyntax { + if !p.at(T![data]) { + return Absent; + } + + let m = p.start(); + p.bump(T![data]); + p.expect(T![-]); + parse_value(p).or_add_diagnostic(p, expected_value); + + Present(m.complete(p, TW_DATA_ATTRIBUTE)) +} diff --git a/crates/biome_tailwind_parser/tests/quick_test.rs b/crates/biome_tailwind_parser/tests/quick_test.rs index 332e363c4599..7ade966e48f7 100644 --- a/crates/biome_tailwind_parser/tests/quick_test.rs +++ b/crates/biome_tailwind_parser/tests/quick_test.rs @@ -4,7 +4,7 @@ use biome_test_utils::has_bogus_nodes_or_empty_slots; #[ignore] #[test] pub fn quick_test() { - let code = r#"-top-4 -mb-2"#; + let code = r#"group-data-[collapsible=icon]:hidden"#; let root = parse_tailwind(code); let syntax = root.syntax(); diff --git a/crates/biome_tailwind_parser/tests/tailwind_specs/ok/data-attribute.txt b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/data-attribute.txt new file mode 100644 index 000000000000..09f1992ad4eb --- /dev/null +++ b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/data-attribute.txt @@ -0,0 +1 @@ +data-active:text-red-500 diff --git a/crates/biome_tailwind_parser/tests/tailwind_specs/ok/data-attribute.txt.snap b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/data-attribute.txt.snap new file mode 100644 index 000000000000..d8c4b919a0dc --- /dev/null +++ b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/data-attribute.txt.snap @@ -0,0 +1,70 @@ +--- +source: crates/biome_tailwind_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```text +data-active:text-red-500 + +``` + + +## AST + +``` +TwRoot { + bom_token: missing (optional), + candidates: TwCandidateList [ + TwFullCandidate { + variants: TwVariantList [ + TwDataAttribute { + data_token: DATA_KW@0..4 "data" [] [], + minus_token: DASH@4..5 "-" [] [], + value: TwNamedValue { + value_token: TW_VALUE@5..11 "active" [] [], + }, + }, + COLON@11..12 ":" [] [], + ], + negative_token: missing (optional), + candidate: TwFunctionalCandidate { + base_token: TW_BASE@12..16 "text" [] [], + minus_token: DASH@16..17 "-" [] [], + value: TwNamedValue { + value_token: TW_VALUE@17..24 "red-500" [] [], + }, + modifier: missing (optional), + }, + excl_token: missing (optional), + }, + ], + eof_token: EOF@24..25 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: TW_ROOT@0..25 + 0: (empty) + 1: TW_CANDIDATE_LIST@0..24 + 0: TW_FULL_CANDIDATE@0..24 + 0: TW_VARIANT_LIST@0..12 + 0: TW_DATA_ATTRIBUTE@0..11 + 0: DATA_KW@0..4 "data" [] [] + 1: DASH@4..5 "-" [] [] + 2: TW_NAMED_VALUE@5..11 + 0: TW_VALUE@5..11 "active" [] [] + 1: COLON@11..12 ":" [] [] + 1: (empty) + 2: TW_FUNCTIONAL_CANDIDATE@12..24 + 0: TW_BASE@12..16 "text" [] [] + 1: DASH@16..17 "-" [] [] + 2: TW_NAMED_VALUE@17..24 + 0: TW_VALUE@17..24 "red-500" [] [] + 3: (empty) + 3: (empty) + 2: EOF@24..25 "" [Newline("\n")] [] + +``` diff --git a/crates/biome_tailwind_parser/tests/tailwind_specs/ok/group-data.txt b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/group-data.txt new file mode 100644 index 000000000000..86802ce37353 --- /dev/null +++ b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/group-data.txt @@ -0,0 +1 @@ +group-data-[collapsible=icon]:hidden diff --git a/crates/biome_tailwind_parser/tests/tailwind_specs/ok/group-data.txt.snap b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/group-data.txt.snap new file mode 100644 index 000000000000..e9b3bd13695c --- /dev/null +++ b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/group-data.txt.snap @@ -0,0 +1,72 @@ +--- +source: crates/biome_tailwind_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```text +group-data-[collapsible=icon]:hidden + +``` + + +## AST + +``` +TwRoot { + bom_token: missing (optional), + candidates: TwCandidateList [ + TwFullCandidate { + variants: TwVariantList [ + TwFunctionalVariant { + base_token: TW_BASE@0..5 "group" [] [], + minus_token: DASH@5..6 "-" [] [], + value: TwDataAttribute { + data_token: DATA_KW@6..10 "data" [] [], + minus_token: DASH@10..11 "-" [] [], + value: TwArbitraryValue { + l_brack_token: L_BRACKET@11..12 "[" [] [], + value_token: TW_VALUE@12..28 "collapsible=icon" [] [], + r_brack_token: R_BRACKET@28..29 "]" [] [], + }, + }, + }, + COLON@29..30 ":" [] [], + ], + negative_token: missing (optional), + candidate: TwStaticCandidate { + base_token: TW_BASE@30..36 "hidden" [] [], + }, + excl_token: missing (optional), + }, + ], + eof_token: EOF@36..37 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: TW_ROOT@0..37 + 0: (empty) + 1: TW_CANDIDATE_LIST@0..36 + 0: TW_FULL_CANDIDATE@0..36 + 0: TW_VARIANT_LIST@0..30 + 0: TW_FUNCTIONAL_VARIANT@0..29 + 0: TW_BASE@0..5 "group" [] [] + 1: DASH@5..6 "-" [] [] + 2: TW_DATA_ATTRIBUTE@6..29 + 0: DATA_KW@6..10 "data" [] [] + 1: DASH@10..11 "-" [] [] + 2: TW_ARBITRARY_VALUE@11..29 + 0: L_BRACKET@11..12 "[" [] [] + 1: TW_VALUE@12..28 "collapsible=icon" [] [] + 2: R_BRACKET@28..29 "]" [] [] + 1: COLON@29..30 ":" [] [] + 1: (empty) + 2: TW_STATIC_CANDIDATE@30..36 + 0: TW_BASE@30..36 "hidden" [] [] + 3: (empty) + 2: EOF@36..37 "" [Newline("\n")] [] + +``` diff --git a/crates/biome_tailwind_syntax/src/generated/kind.rs b/crates/biome_tailwind_syntax/src/generated/kind.rs index 8c4d2bed7dd0..bab078166fd7 100644 --- a/crates/biome_tailwind_syntax/src/generated/kind.rs +++ b/crates/biome_tailwind_syntax/src/generated/kind.rs @@ -21,6 +21,7 @@ pub enum TailwindSyntaxKind { L_PAREN, R_PAREN, WHITESPACE, + DATA_KW, TW_BASE, TW_VALUE, TW_SELECTOR, @@ -41,6 +42,7 @@ pub enum TailwindSyntaxKind { TW_ARBITRARY_VALUE, TW_CSS_VARIABLE_VALUE, TW_MODIFIER, + TW_DATA_ATTRIBUTE, TW_BOGUS, TW_BOGUS_CANDIDATE, TW_BOGUS_VARIANT, @@ -63,8 +65,12 @@ impl TailwindSyntaxKind { pub const fn is_list(self) -> bool { matches!(self, TW_CANDIDATE_LIST | TW_VARIANT_LIST) } - pub fn from_keyword(_ident: &str) -> Option { - None + pub fn from_keyword(ident: &str) -> Option { + let kw = match ident { + "data" => DATA_KW, + _ => return None, + }; + Some(kw) } pub const fn to_string(&self) -> Option<&'static str> { let tok = match self { @@ -88,4 +94,4 @@ impl TailwindSyntaxKind { } #[doc = r" Utility macro for creating a SyntaxKind through simple macro syntax"] #[macro_export] -macro_rules ! T { [/] => { $ crate :: TailwindSyntaxKind :: SLASH } ; [!] => { $ crate :: TailwindSyntaxKind :: BANG } ; [-] => { $ crate :: TailwindSyntaxKind :: DASH } ; [:] => { $ crate :: TailwindSyntaxKind :: COLON } ; ['['] => { $ crate :: TailwindSyntaxKind :: L_BRACKET } ; [']'] => { $ crate :: TailwindSyntaxKind :: R_BRACKET } ; ['('] => { $ crate :: TailwindSyntaxKind :: L_PAREN } ; [')'] => { $ crate :: TailwindSyntaxKind :: R_PAREN } ; [' '] => { $ crate :: TailwindSyntaxKind :: WHITESPACE } ; [ident] => { $ crate :: TailwindSyntaxKind :: IDENT } ; [EOF] => { $ crate :: TailwindSyntaxKind :: EOF } ; [UNICODE_BOM] => { $ crate :: TailwindSyntaxKind :: UNICODE_BOM } ; [#] => { $ crate :: TailwindSyntaxKind :: HASH } ; } +macro_rules ! T { [/] => { $ crate :: TailwindSyntaxKind :: SLASH } ; [!] => { $ crate :: TailwindSyntaxKind :: BANG } ; [-] => { $ crate :: TailwindSyntaxKind :: DASH } ; [:] => { $ crate :: TailwindSyntaxKind :: COLON } ; ['['] => { $ crate :: TailwindSyntaxKind :: L_BRACKET } ; [']'] => { $ crate :: TailwindSyntaxKind :: R_BRACKET } ; ['('] => { $ crate :: TailwindSyntaxKind :: L_PAREN } ; [')'] => { $ crate :: TailwindSyntaxKind :: R_PAREN } ; [' '] => { $ crate :: TailwindSyntaxKind :: WHITESPACE } ; [data] => { $ crate :: TailwindSyntaxKind :: DATA_KW } ; [ident] => { $ crate :: TailwindSyntaxKind :: IDENT } ; [EOF] => { $ crate :: TailwindSyntaxKind :: EOF } ; [UNICODE_BOM] => { $ crate :: TailwindSyntaxKind :: UNICODE_BOM } ; [#] => { $ crate :: TailwindSyntaxKind :: HASH } ; } diff --git a/crates/biome_tailwind_syntax/src/generated/macros.rs b/crates/biome_tailwind_syntax/src/generated/macros.rs index b49bb775a80e..5bb51152d1c9 100644 --- a/crates/biome_tailwind_syntax/src/generated/macros.rs +++ b/crates/biome_tailwind_syntax/src/generated/macros.rs @@ -32,6 +32,10 @@ macro_rules! map_syntax_node { let $pattern = unsafe { $crate::TwCssVariableValue::new_unchecked(node) }; $body } + $crate::TailwindSyntaxKind::TW_DATA_ATTRIBUTE => { + let $pattern = unsafe { $crate::TwDataAttribute::new_unchecked(node) }; + $body + } $crate::TailwindSyntaxKind::TW_FULL_CANDIDATE => { let $pattern = unsafe { $crate::TwFullCandidate::new_unchecked(node) }; $body diff --git a/crates/biome_tailwind_syntax/src/generated/nodes.rs b/crates/biome_tailwind_syntax/src/generated/nodes.rs index 125ea6a398b7..5624bf4749f9 100644 --- a/crates/biome_tailwind_syntax/src/generated/nodes.rs +++ b/crates/biome_tailwind_syntax/src/generated/nodes.rs @@ -216,6 +216,51 @@ pub struct TwCssVariableValueFields { pub r_paren_token: SyntaxResult, } #[derive(Clone, PartialEq, Eq, Hash)] +pub struct TwDataAttribute { + pub(crate) syntax: SyntaxNode, +} +impl TwDataAttribute { + #[doc = r" Create an AstNode from a SyntaxNode without checking its kind"] + #[doc = r""] + #[doc = r" # Safety"] + #[doc = r" This function must be guarded with a call to [AstNode::can_cast]"] + #[doc = r" or a match on [SyntaxNode::kind]"] + #[inline] + pub const unsafe fn new_unchecked(syntax: SyntaxNode) -> Self { + Self { syntax } + } + pub fn as_fields(&self) -> TwDataAttributeFields { + TwDataAttributeFields { + data_token: self.data_token(), + minus_token: self.minus_token(), + value: self.value(), + } + } + pub fn data_token(&self) -> SyntaxResult { + support::required_token(&self.syntax, 0usize) + } + pub fn minus_token(&self) -> SyntaxResult { + support::required_token(&self.syntax, 1usize) + } + pub fn value(&self) -> SyntaxResult { + support::required_node(&self.syntax, 2usize) + } +} +impl Serialize for TwDataAttribute { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.as_fields().serialize(serializer) + } +} +#[derive(Serialize)] +pub struct TwDataAttributeFields { + pub data_token: SyntaxResult, + pub minus_token: SyntaxResult, + pub value: SyntaxResult, +} +#[derive(Clone, PartialEq, Eq, Hash)] pub struct TwFullCandidate { pub(crate) syntax: SyntaxNode, } @@ -584,6 +629,32 @@ impl AnyTwCandidate { } } #[derive(Clone, PartialEq, Eq, Hash, Serialize)] +pub enum AnyTwDataAttributeValue { + TwArbitraryValue(TwArbitraryValue), + TwBogusValue(TwBogusValue), + TwNamedValue(TwNamedValue), +} +impl AnyTwDataAttributeValue { + pub fn as_tw_arbitrary_value(&self) -> Option<&TwArbitraryValue> { + match &self { + Self::TwArbitraryValue(item) => Some(item), + _ => None, + } + } + pub fn as_tw_bogus_value(&self) -> Option<&TwBogusValue> { + match &self { + Self::TwBogusValue(item) => Some(item), + _ => None, + } + } + pub fn as_tw_named_value(&self) -> Option<&TwNamedValue> { + match &self { + Self::TwNamedValue(item) => Some(item), + _ => None, + } + } +} +#[derive(Clone, PartialEq, Eq, Hash, Serialize)] pub enum AnyTwFullCandidate { TwBogusCandidate(TwBogusCandidate), TwFullCandidate(TwFullCandidate), @@ -626,6 +697,7 @@ pub enum AnyTwValue { TwArbitraryValue(TwArbitraryValue), TwBogusValue(TwBogusValue), TwCssVariableValue(TwCssVariableValue), + TwDataAttribute(TwDataAttribute), TwNamedValue(TwNamedValue), } impl AnyTwValue { @@ -647,6 +719,12 @@ impl AnyTwValue { _ => None, } } + pub fn as_tw_data_attribute(&self) -> Option<&TwDataAttribute> { + match &self { + Self::TwDataAttribute(item) => Some(item), + _ => None, + } + } pub fn as_tw_named_value(&self) -> Option<&TwNamedValue> { match &self { Self::TwNamedValue(item) => Some(item), @@ -658,6 +736,7 @@ impl AnyTwValue { pub enum AnyTwVariant { TwArbitraryVariant(TwArbitraryVariant), TwBogusVariant(TwBogusVariant), + TwDataAttribute(TwDataAttribute), TwFunctionalVariant(TwFunctionalVariant), TwStaticVariant(TwStaticVariant), } @@ -674,6 +753,12 @@ impl AnyTwVariant { _ => None, } } + pub fn as_tw_data_attribute(&self) -> Option<&TwDataAttribute> { + match &self { + Self::TwDataAttribute(item) => Some(item), + _ => None, + } + } pub fn as_tw_functional_variant(&self) -> Option<&TwFunctionalVariant> { match &self { Self::TwFunctionalVariant(item) => Some(item), @@ -928,6 +1013,58 @@ impl From for SyntaxElement { n.syntax.into() } } +impl AstNode for TwDataAttribute { + type Language = Language; + const KIND_SET: SyntaxKindSet = + SyntaxKindSet::from_raw(RawSyntaxKind(TW_DATA_ATTRIBUTE as u16)); + fn can_cast(kind: SyntaxKind) -> bool { + kind == TW_DATA_ATTRIBUTE + } + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } + fn into_syntax(self) -> SyntaxNode { + self.syntax + } +} +impl std::fmt::Debug for TwDataAttribute { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + thread_local! { static DEPTH : std :: cell :: Cell < u8 > = const { std :: cell :: Cell :: new (0) } }; + let current_depth = DEPTH.get(); + let result = if current_depth < 16 { + DEPTH.set(current_depth + 1); + f.debug_struct("TwDataAttribute") + .field("data_token", &support::DebugSyntaxResult(self.data_token())) + .field( + "minus_token", + &support::DebugSyntaxResult(self.minus_token()), + ) + .field("value", &support::DebugSyntaxResult(self.value())) + .finish() + } else { + f.debug_struct("TwDataAttribute").finish() + }; + DEPTH.set(current_depth); + result + } +} +impl From for SyntaxNode { + fn from(n: TwDataAttribute) -> Self { + n.syntax + } +} +impl From for SyntaxElement { + fn from(n: TwDataAttribute) -> Self { + n.syntax.into() + } +} impl AstNode for TwFullCandidate { type Language = Language; const KIND_SET: SyntaxKindSet = @@ -1426,6 +1563,77 @@ impl From for SyntaxElement { node.into() } } +impl From for AnyTwDataAttributeValue { + fn from(node: TwArbitraryValue) -> Self { + Self::TwArbitraryValue(node) + } +} +impl From for AnyTwDataAttributeValue { + fn from(node: TwBogusValue) -> Self { + Self::TwBogusValue(node) + } +} +impl From for AnyTwDataAttributeValue { + fn from(node: TwNamedValue) -> Self { + Self::TwNamedValue(node) + } +} +impl AstNode for AnyTwDataAttributeValue { + type Language = Language; + const KIND_SET: SyntaxKindSet = TwArbitraryValue::KIND_SET + .union(TwBogusValue::KIND_SET) + .union(TwNamedValue::KIND_SET); + fn can_cast(kind: SyntaxKind) -> bool { + matches!(kind, TW_ARBITRARY_VALUE | TW_BOGUS_VALUE | TW_NAMED_VALUE) + } + fn cast(syntax: SyntaxNode) -> Option { + let res = match syntax.kind() { + TW_ARBITRARY_VALUE => Self::TwArbitraryValue(TwArbitraryValue { syntax }), + TW_BOGUS_VALUE => Self::TwBogusValue(TwBogusValue { syntax }), + TW_NAMED_VALUE => Self::TwNamedValue(TwNamedValue { syntax }), + _ => return None, + }; + Some(res) + } + fn syntax(&self) -> &SyntaxNode { + match self { + Self::TwArbitraryValue(it) => it.syntax(), + Self::TwBogusValue(it) => it.syntax(), + Self::TwNamedValue(it) => it.syntax(), + } + } + fn into_syntax(self) -> SyntaxNode { + match self { + Self::TwArbitraryValue(it) => it.into_syntax(), + Self::TwBogusValue(it) => it.into_syntax(), + Self::TwNamedValue(it) => it.into_syntax(), + } + } +} +impl std::fmt::Debug for AnyTwDataAttributeValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::TwArbitraryValue(it) => std::fmt::Debug::fmt(it, f), + Self::TwBogusValue(it) => std::fmt::Debug::fmt(it, f), + Self::TwNamedValue(it) => std::fmt::Debug::fmt(it, f), + } + } +} +impl From for SyntaxNode { + fn from(n: AnyTwDataAttributeValue) -> Self { + match n { + AnyTwDataAttributeValue::TwArbitraryValue(it) => it.into_syntax(), + AnyTwDataAttributeValue::TwBogusValue(it) => it.into_syntax(), + AnyTwDataAttributeValue::TwNamedValue(it) => it.into_syntax(), + } + } +} +impl From for SyntaxElement { + fn from(n: AnyTwDataAttributeValue) -> Self { + let node: SyntaxNode = n.into(); + node.into() + } +} impl From for AnyTwFullCandidate { fn from(node: TwBogusCandidate) -> Self { Self::TwBogusCandidate(node) @@ -1560,6 +1768,11 @@ impl From for AnyTwValue { Self::TwCssVariableValue(node) } } +impl From for AnyTwValue { + fn from(node: TwDataAttribute) -> Self { + Self::TwDataAttribute(node) + } +} impl From for AnyTwValue { fn from(node: TwNamedValue) -> Self { Self::TwNamedValue(node) @@ -1570,11 +1783,16 @@ impl AstNode for AnyTwValue { const KIND_SET: SyntaxKindSet = TwArbitraryValue::KIND_SET .union(TwBogusValue::KIND_SET) .union(TwCssVariableValue::KIND_SET) + .union(TwDataAttribute::KIND_SET) .union(TwNamedValue::KIND_SET); fn can_cast(kind: SyntaxKind) -> bool { matches!( kind, - TW_ARBITRARY_VALUE | TW_BOGUS_VALUE | TW_CSS_VARIABLE_VALUE | TW_NAMED_VALUE + TW_ARBITRARY_VALUE + | TW_BOGUS_VALUE + | TW_CSS_VARIABLE_VALUE + | TW_DATA_ATTRIBUTE + | TW_NAMED_VALUE ) } fn cast(syntax: SyntaxNode) -> Option { @@ -1582,6 +1800,7 @@ impl AstNode for AnyTwValue { TW_ARBITRARY_VALUE => Self::TwArbitraryValue(TwArbitraryValue { syntax }), TW_BOGUS_VALUE => Self::TwBogusValue(TwBogusValue { syntax }), TW_CSS_VARIABLE_VALUE => Self::TwCssVariableValue(TwCssVariableValue { syntax }), + TW_DATA_ATTRIBUTE => Self::TwDataAttribute(TwDataAttribute { syntax }), TW_NAMED_VALUE => Self::TwNamedValue(TwNamedValue { syntax }), _ => return None, }; @@ -1592,6 +1811,7 @@ impl AstNode for AnyTwValue { Self::TwArbitraryValue(it) => it.syntax(), Self::TwBogusValue(it) => it.syntax(), Self::TwCssVariableValue(it) => it.syntax(), + Self::TwDataAttribute(it) => it.syntax(), Self::TwNamedValue(it) => it.syntax(), } } @@ -1600,6 +1820,7 @@ impl AstNode for AnyTwValue { Self::TwArbitraryValue(it) => it.into_syntax(), Self::TwBogusValue(it) => it.into_syntax(), Self::TwCssVariableValue(it) => it.into_syntax(), + Self::TwDataAttribute(it) => it.into_syntax(), Self::TwNamedValue(it) => it.into_syntax(), } } @@ -1610,6 +1831,7 @@ impl std::fmt::Debug for AnyTwValue { Self::TwArbitraryValue(it) => std::fmt::Debug::fmt(it, f), Self::TwBogusValue(it) => std::fmt::Debug::fmt(it, f), Self::TwCssVariableValue(it) => std::fmt::Debug::fmt(it, f), + Self::TwDataAttribute(it) => std::fmt::Debug::fmt(it, f), Self::TwNamedValue(it) => std::fmt::Debug::fmt(it, f), } } @@ -1620,6 +1842,7 @@ impl From for SyntaxNode { AnyTwValue::TwArbitraryValue(it) => it.into_syntax(), AnyTwValue::TwBogusValue(it) => it.into_syntax(), AnyTwValue::TwCssVariableValue(it) => it.into_syntax(), + AnyTwValue::TwDataAttribute(it) => it.into_syntax(), AnyTwValue::TwNamedValue(it) => it.into_syntax(), } } @@ -1640,6 +1863,11 @@ impl From for AnyTwVariant { Self::TwBogusVariant(node) } } +impl From for AnyTwVariant { + fn from(node: TwDataAttribute) -> Self { + Self::TwDataAttribute(node) + } +} impl From for AnyTwVariant { fn from(node: TwFunctionalVariant) -> Self { Self::TwFunctionalVariant(node) @@ -1654,18 +1882,24 @@ impl AstNode for AnyTwVariant { type Language = Language; const KIND_SET: SyntaxKindSet = TwArbitraryVariant::KIND_SET .union(TwBogusVariant::KIND_SET) + .union(TwDataAttribute::KIND_SET) .union(TwFunctionalVariant::KIND_SET) .union(TwStaticVariant::KIND_SET); fn can_cast(kind: SyntaxKind) -> bool { matches!( kind, - TW_ARBITRARY_VARIANT | TW_BOGUS_VARIANT | TW_FUNCTIONAL_VARIANT | TW_STATIC_VARIANT + TW_ARBITRARY_VARIANT + | TW_BOGUS_VARIANT + | TW_DATA_ATTRIBUTE + | TW_FUNCTIONAL_VARIANT + | TW_STATIC_VARIANT ) } fn cast(syntax: SyntaxNode) -> Option { let res = match syntax.kind() { TW_ARBITRARY_VARIANT => Self::TwArbitraryVariant(TwArbitraryVariant { syntax }), TW_BOGUS_VARIANT => Self::TwBogusVariant(TwBogusVariant { syntax }), + TW_DATA_ATTRIBUTE => Self::TwDataAttribute(TwDataAttribute { syntax }), TW_FUNCTIONAL_VARIANT => Self::TwFunctionalVariant(TwFunctionalVariant { syntax }), TW_STATIC_VARIANT => Self::TwStaticVariant(TwStaticVariant { syntax }), _ => return None, @@ -1676,6 +1910,7 @@ impl AstNode for AnyTwVariant { match self { Self::TwArbitraryVariant(it) => it.syntax(), Self::TwBogusVariant(it) => it.syntax(), + Self::TwDataAttribute(it) => it.syntax(), Self::TwFunctionalVariant(it) => it.syntax(), Self::TwStaticVariant(it) => it.syntax(), } @@ -1684,6 +1919,7 @@ impl AstNode for AnyTwVariant { match self { Self::TwArbitraryVariant(it) => it.into_syntax(), Self::TwBogusVariant(it) => it.into_syntax(), + Self::TwDataAttribute(it) => it.into_syntax(), Self::TwFunctionalVariant(it) => it.into_syntax(), Self::TwStaticVariant(it) => it.into_syntax(), } @@ -1694,6 +1930,7 @@ impl std::fmt::Debug for AnyTwVariant { match self { Self::TwArbitraryVariant(it) => std::fmt::Debug::fmt(it, f), Self::TwBogusVariant(it) => std::fmt::Debug::fmt(it, f), + Self::TwDataAttribute(it) => std::fmt::Debug::fmt(it, f), Self::TwFunctionalVariant(it) => std::fmt::Debug::fmt(it, f), Self::TwStaticVariant(it) => std::fmt::Debug::fmt(it, f), } @@ -1704,6 +1941,7 @@ impl From for SyntaxNode { match n { AnyTwVariant::TwArbitraryVariant(it) => it.into_syntax(), AnyTwVariant::TwBogusVariant(it) => it.into_syntax(), + AnyTwVariant::TwDataAttribute(it) => it.into_syntax(), AnyTwVariant::TwFunctionalVariant(it) => it.into_syntax(), AnyTwVariant::TwStaticVariant(it) => it.into_syntax(), } @@ -1720,6 +1958,11 @@ impl std::fmt::Display for AnyTwCandidate { std::fmt::Display::fmt(self.syntax(), f) } } +impl std::fmt::Display for AnyTwDataAttributeValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} impl std::fmt::Display for AnyTwFullCandidate { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(self.syntax(), f) @@ -1760,6 +2003,11 @@ impl std::fmt::Display for TwCssVariableValue { std::fmt::Display::fmt(self.syntax(), f) } } +impl std::fmt::Display for TwDataAttribute { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} impl std::fmt::Display for TwFullCandidate { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(self.syntax(), f) diff --git a/crates/biome_tailwind_syntax/src/generated/nodes_mut.rs b/crates/biome_tailwind_syntax/src/generated/nodes_mut.rs index 94e3652c400c..30752863bb1d 100644 --- a/crates/biome_tailwind_syntax/src/generated/nodes_mut.rs +++ b/crates/biome_tailwind_syntax/src/generated/nodes_mut.rs @@ -101,6 +101,26 @@ impl TwCssVariableValue { ) } } +impl TwDataAttribute { + pub fn with_data_token(self, element: SyntaxToken) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(0usize..=0usize, once(Some(element.into()))), + ) + } + pub fn with_minus_token(self, element: SyntaxToken) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(1usize..=1usize, once(Some(element.into()))), + ) + } + pub fn with_value(self, element: AnyTwDataAttributeValue) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(2usize..=2usize, once(Some(element.into_syntax().into()))), + ) + } +} impl TwFullCandidate { pub fn with_variants(self, element: TwVariantList) -> Self { Self::unwrap_cast( diff --git a/xtask/codegen/src/tailwind_kinds_src.rs b/xtask/codegen/src/tailwind_kinds_src.rs index 1a8ae8b0d6ce..d10cb31d752a 100644 --- a/xtask/codegen/src/tailwind_kinds_src.rs +++ b/xtask/codegen/src/tailwind_kinds_src.rs @@ -14,7 +14,7 @@ pub const TAILWIND_KINDS_SRC: KindsSrc = KindsSrc { ], literals: &["TW_BASE", "TW_VALUE", "TW_SELECTOR", "TW_PROPERTY"], tokens: &["ERROR_TOKEN", "NEWLINE"], - keywords: &[], + keywords: &["data"], nodes: &[ "TW_ROOT", "TW_CANDIDATE_LIST", @@ -30,6 +30,8 @@ pub const TAILWIND_KINDS_SRC: KindsSrc = KindsSrc { "TW_ARBITRARY_VALUE", "TW_CSS_VARIABLE_VALUE", "TW_MODIFIER", + "TW_DATA_ATTRIBUTE", + // "TW_DATA_ATTRIBUTE_ARBITRARY_VALUE", // Bogus nodes "TW_BOGUS", "TW_BOGUS_CANDIDATE", diff --git a/xtask/codegen/tailwind.ungram b/xtask/codegen/tailwind.ungram index 0367d1e6fa2e..dd33659ac203 100644 --- a/xtask/codegen/tailwind.ungram +++ b/xtask/codegen/tailwind.ungram @@ -100,7 +100,11 @@ TwFunctionalCandidate = TwVariantList = (AnyTwVariant (':' AnyTwVariant)* ':'?) // A Variant is a selector that modifies what the Candidate applies to. -AnyTwVariant = TwArbitraryVariant | TwStaticVariant | TwFunctionalVariant | TwBogusVariant +AnyTwVariant = TwArbitraryVariant + | TwStaticVariant + | TwFunctionalVariant + | TwDataAttribute + | TwBogusVariant // Arbitrary variants are variants that take a selector and generate a variant on the fly. // [&_p]:text-red-500 @@ -126,7 +130,11 @@ TwFunctionalVariant = '-' value: AnyTwValue -AnyTwValue = TwNamedValue | TwArbitraryValue | TwCssVariableValue | TwBogusValue +AnyTwValue = TwNamedValue + | TwArbitraryValue + | TwCssVariableValue + | TwDataAttribute + | TwBogusValue // bg-red-500 // ^^^^^^^ @@ -156,3 +164,25 @@ AnyTwModifier = TwModifier | TwBogusModifier TwModifier = '/' value: AnyTwValue + +// data-[state=open]:hidden +// ^^^^^^^^^^^^^^^^^ +// group-data-active:hidden +// ^^^^^^^^^^^ +TwDataAttribute = + 'data' + '-' + value: AnyTwDataAttributeValue + +AnyTwDataAttributeValue = + TwNamedValue | + TwArbitraryValue | + TwBogusValue + +// TODO: replace TwArbitraryValue in AnyTwDataAttributeValue with this +// TwDataAttributeArbitraryValue = +// '[' +// key: 'tw_value' +// '=' +// value: 'tw_value' +// ']'