diff --git a/crates/biome_tailwind_parser/src/lexer/mod.rs b/crates/biome_tailwind_parser/src/lexer/mod.rs index f771c7921bc2..229005ff852c 100644 --- a/crates/biome_tailwind_parser/src/lexer/mod.rs +++ b/crates/biome_tailwind_parser/src/lexer/mod.rs @@ -90,6 +90,18 @@ impl<'src> TailwindLexer<'src> { } } + /// Consume a token in the arbitrary candidate context + fn consume_token_arbitrary_candidate(&mut self, current: u8) -> TailwindSyntaxKind { + match current { + bracket @ (b'[' | b']' | b'(' | b')') => self.consume_bracket(bracket), + b'\n' | b'\r' | b'\t' | b' ' => self.consume_newline_or_whitespaces(), + b':' => self.consume_byte(T![:]), + _ if self.current_kind == T!['['] => self.consume_bracketed_thing(TW_PROPERTY, b':'), + _ if self.current_kind == T![:] => self.consume_bracketed_thing(TW_VALUE, b']'), + _ => self.consume_named_value(), + } + } + fn consume_bracket(&mut self, byte: u8) -> TailwindSyntaxKind { let kind = match byte { b'[' => T!['['], @@ -232,6 +244,9 @@ impl<'src> Lexer<'src> for TailwindLexer<'src> { TailwindLexContext::ArbitraryVariant => { self.consume_token_arbitrary_variant(current) } + TailwindLexContext::ArbitraryCandidate => { + self.consume_token_arbitrary_candidate(current) + } }, None => EOF, } diff --git a/crates/biome_tailwind_parser/src/syntax/mod.rs b/crates/biome_tailwind_parser/src/syntax/mod.rs index e430b82d2a36..8184f24b911e 100644 --- a/crates/biome_tailwind_parser/src/syntax/mod.rs +++ b/crates/biome_tailwind_parser/src/syntax/mod.rs @@ -2,6 +2,7 @@ use crate::parser::TailwindParser; use crate::syntax::parse_error::*; use crate::syntax::value::parse_value; use crate::syntax::variant::VariantList; +use crate::token_source::TailwindLexContext; use biome_parser::parse_lists::ParseSeparatedList; use biome_parser::parsed_syntax::ParsedSyntax::{Absent, Present}; use biome_parser::prelude::*; @@ -67,11 +68,13 @@ fn parse_full_candidate(p: &mut TailwindParser) -> ParsedSyntax { VariantList.parse_list(p); - let candidate = parse_functional_or_static_candidate(p).or_recover_with_token_set( - p, - &ParseRecoveryTokenSet::new(TW_BOGUS_CANDIDATE, token_set![WHITESPACE, NEWLINE, EOF]), - expected_candidate, - ); + let candidate = parse_arbitrary_candidate(p) + .or_else(|| parse_functional_or_static_candidate(p)) + .or_recover_with_token_set( + p, + &ParseRecoveryTokenSet::new(TW_BOGUS_CANDIDATE, token_set![WHITESPACE, NEWLINE, EOF]), + expected_candidate, + ); match candidate { Ok(_) => {} @@ -137,6 +140,50 @@ fn parse_functional_or_static_candidate(p: &mut TailwindParser) -> ParsedSyntax Present(m.complete(p, TW_FUNCTIONAL_CANDIDATE)) } +fn parse_arbitrary_candidate(p: &mut TailwindParser) -> ParsedSyntax { + if !p.at(T!['[']) { + return Absent; + } + + let checkpoint = p.checkpoint(); + let m = p.start(); + if !p.expect_with_context(T!['['], TailwindLexContext::ArbitraryCandidate) { + m.abandon(p); + p.rewind(checkpoint); + return Absent; + } + if !p.expect_with_context(TW_PROPERTY, TailwindLexContext::ArbitraryCandidate) { + m.abandon(p); + p.rewind(checkpoint); + return Absent; + } + if !p.expect_with_context(T![:], TailwindLexContext::ArbitraryCandidate) { + m.abandon(p); + p.rewind(checkpoint); + return Absent; + } + if !p.expect_with_context(TW_VALUE, TailwindLexContext::ArbitraryCandidate) { + m.abandon(p); + p.rewind(checkpoint); + return Absent; + } + if !p.expect(T![']']) { + m.abandon(p); + p.rewind(checkpoint); + return Absent; + } + + if !p.at(T![/]) { + return Present(m.complete(p, TW_ARBITRARY_CANDIDATE)); + } + + if p.at(T![/]) { + parse_modifier(p).or_add_diagnostic(p, expected_modifier); + } + + Present(m.complete(p, TW_ARBITRARY_CANDIDATE)) +} + fn parse_modifier(p: &mut TailwindParser) -> ParsedSyntax { let m = p.start(); if !p.expect(T![/]) { diff --git a/crates/biome_tailwind_parser/src/token_source.rs b/crates/biome_tailwind_parser/src/token_source.rs index 0df718f493b7..da7ac0bc8422 100644 --- a/crates/biome_tailwind_parser/src/token_source.rs +++ b/crates/biome_tailwind_parser/src/token_source.rs @@ -26,6 +26,8 @@ pub(crate) enum TailwindLexContext { Arbitrary, /// Like Arbitrary, but specifically for arbitrary variants. ArbitraryVariant, + /// Like Arbitrary, but specifically for arbitrary candidates. + ArbitraryCandidate, } impl LexContext for TailwindLexContext { diff --git a/crates/biome_tailwind_parser/tests/tailwind_specs/error/arbitrary-candidate/missing-property.txt b/crates/biome_tailwind_parser/tests/tailwind_specs/error/arbitrary-candidate/missing-property.txt new file mode 100644 index 000000000000..5bc6bb7717cb --- /dev/null +++ b/crates/biome_tailwind_parser/tests/tailwind_specs/error/arbitrary-candidate/missing-property.txt @@ -0,0 +1 @@ +[:40px] w-5 diff --git a/crates/biome_tailwind_parser/tests/tailwind_specs/error/arbitrary-candidate/missing-property.txt.snap b/crates/biome_tailwind_parser/tests/tailwind_specs/error/arbitrary-candidate/missing-property.txt.snap new file mode 100644 index 000000000000..d09c3a057d1e --- /dev/null +++ b/crates/biome_tailwind_parser/tests/tailwind_specs/error/arbitrary-candidate/missing-property.txt.snap @@ -0,0 +1,92 @@ +--- +source: crates/biome_tailwind_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```text +[:40px] w-5 + +``` + + +## AST + +``` +TwRoot { + bom_token: missing (optional), + candidates: TwCandidateList [ + TwFullCandidate { + variants: TwVariantList [], + candidate: TwBogusCandidate { + items: [ + L_BRACKET@0..1 "[" [] [], + TW_SELECTOR@1..6 ":40px" [] [], + R_BRACKET@6..7 "]" [] [], + ], + }, + excl_token: missing (optional), + }, + WHITESPACE@7..8 " " [] [], + TwFullCandidate { + variants: TwVariantList [], + candidate: TwFunctionalCandidate { + base_token: TW_BASE@8..9 "w" [] [], + minus_token: DASH@9..10 "-" [] [], + value: TwNamedValue { + value_token: TW_VALUE@10..12 "5" [] [Newline("\n")], + }, + modifier: missing (optional), + }, + excl_token: missing (optional), + }, + ], + eof_token: EOF@12..12 "" [] [], +} +``` + +## CST + +``` +0: TW_ROOT@0..12 + 0: (empty) + 1: TW_CANDIDATE_LIST@0..12 + 0: TW_FULL_CANDIDATE@0..7 + 0: TW_VARIANT_LIST@0..0 + 1: TW_BOGUS_CANDIDATE@0..7 + 0: L_BRACKET@0..1 "[" [] [] + 1: TW_SELECTOR@1..6 ":40px" [] [] + 2: R_BRACKET@6..7 "]" [] [] + 2: (empty) + 1: WHITESPACE@7..8 " " [] [] + 2: TW_FULL_CANDIDATE@8..12 + 0: TW_VARIANT_LIST@8..8 + 1: TW_FUNCTIONAL_CANDIDATE@8..12 + 0: TW_BASE@8..9 "w" [] [] + 1: DASH@9..10 "-" [] [] + 2: TW_NAMED_VALUE@10..12 + 0: TW_VALUE@10..12 "5" [] [Newline("\n")] + 3: (empty) + 2: (empty) + 2: EOF@12..12 "" [] [] + +``` + +## Diagnostics + +``` +missing-property.txt:1:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a candidate but instead found '[:40px]'. + + > 1 │ [:40px] w-5 + │ ^^^^^^^ + 2 │ + + i Expected a candidate here. + + > 1 │ [:40px] w-5 + │ ^^^^^^^ + 2 │ + +``` diff --git a/crates/biome_tailwind_parser/tests/tailwind_specs/error/arbitrary-candidate/missing-value-in-arbitrary.txt b/crates/biome_tailwind_parser/tests/tailwind_specs/error/arbitrary-candidate/missing-value-in-arbitrary.txt new file mode 100644 index 000000000000..e49e5fdc236f --- /dev/null +++ b/crates/biome_tailwind_parser/tests/tailwind_specs/error/arbitrary-candidate/missing-value-in-arbitrary.txt @@ -0,0 +1 @@ +[width:] w-5 diff --git a/crates/biome_tailwind_parser/tests/tailwind_specs/error/arbitrary-candidate/missing-value-in-arbitrary.txt.snap b/crates/biome_tailwind_parser/tests/tailwind_specs/error/arbitrary-candidate/missing-value-in-arbitrary.txt.snap new file mode 100644 index 000000000000..295894003103 --- /dev/null +++ b/crates/biome_tailwind_parser/tests/tailwind_specs/error/arbitrary-candidate/missing-value-in-arbitrary.txt.snap @@ -0,0 +1,92 @@ +--- +source: crates/biome_tailwind_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```text +[width:] w-5 + +``` + + +## AST + +``` +TwRoot { + bom_token: missing (optional), + candidates: TwCandidateList [ + TwFullCandidate { + variants: TwVariantList [], + candidate: TwBogusCandidate { + items: [ + L_BRACKET@0..1 "[" [] [], + TW_SELECTOR@1..7 "width:" [] [], + R_BRACKET@7..8 "]" [] [], + ], + }, + excl_token: missing (optional), + }, + WHITESPACE@8..9 " " [] [], + TwFullCandidate { + variants: TwVariantList [], + candidate: TwFunctionalCandidate { + base_token: TW_BASE@9..10 "w" [] [], + minus_token: DASH@10..11 "-" [] [], + value: TwNamedValue { + value_token: TW_VALUE@11..13 "5" [] [Newline("\n")], + }, + modifier: missing (optional), + }, + excl_token: missing (optional), + }, + ], + eof_token: EOF@13..13 "" [] [], +} +``` + +## CST + +``` +0: TW_ROOT@0..13 + 0: (empty) + 1: TW_CANDIDATE_LIST@0..13 + 0: TW_FULL_CANDIDATE@0..8 + 0: TW_VARIANT_LIST@0..0 + 1: TW_BOGUS_CANDIDATE@0..8 + 0: L_BRACKET@0..1 "[" [] [] + 1: TW_SELECTOR@1..7 "width:" [] [] + 2: R_BRACKET@7..8 "]" [] [] + 2: (empty) + 1: WHITESPACE@8..9 " " [] [] + 2: TW_FULL_CANDIDATE@9..13 + 0: TW_VARIANT_LIST@9..9 + 1: TW_FUNCTIONAL_CANDIDATE@9..13 + 0: TW_BASE@9..10 "w" [] [] + 1: DASH@10..11 "-" [] [] + 2: TW_NAMED_VALUE@11..13 + 0: TW_VALUE@11..13 "5" [] [Newline("\n")] + 3: (empty) + 2: (empty) + 2: EOF@13..13 "" [] [] + +``` + +## Diagnostics + +``` +missing-value-in-arbitrary.txt:1:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a candidate but instead found '[width:]'. + + > 1 │ [width:] w-5 + │ ^^^^^^^^ + 2 │ + + i Expected a candidate here. + + > 1 │ [width:] w-5 + │ ^^^^^^^^ + 2 │ + +``` diff --git a/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-0.txt b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-0.txt new file mode 100644 index 000000000000..c98ec6344969 --- /dev/null +++ b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-0.txt @@ -0,0 +1 @@ +[color:red]/50 diff --git a/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-0.txt.snap b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-0.txt.snap new file mode 100644 index 000000000000..493c2c67864d --- /dev/null +++ b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-0.txt.snap @@ -0,0 +1,62 @@ +--- +source: crates/biome_tailwind_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```text +[color:red]/50 + +``` + + +## AST + +``` +TwRoot { + bom_token: missing (optional), + candidates: TwCandidateList [ + TwFullCandidate { + variants: TwVariantList [], + candidate: TwArbitraryCandidate { + l_brack_token: L_BRACKET@0..1 "[" [] [], + property_token: TW_PROPERTY@1..6 "color" [] [], + colon_token: COLON@6..7 ":" [] [], + value_token: TW_VALUE@7..10 "red" [] [], + r_brack_token: R_BRACKET@10..11 "]" [] [], + modifier: TwModifier { + slash_token: SLASH@11..12 "/" [] [], + value: TwNamedValue { + value_token: TW_VALUE@12..15 "50" [] [Newline("\n")], + }, + }, + }, + excl_token: missing (optional), + }, + ], + eof_token: EOF@15..15 "" [] [], +} +``` + +## CST + +``` +0: TW_ROOT@0..15 + 0: (empty) + 1: TW_CANDIDATE_LIST@0..15 + 0: TW_FULL_CANDIDATE@0..15 + 0: TW_VARIANT_LIST@0..0 + 1: TW_ARBITRARY_CANDIDATE@0..15 + 0: L_BRACKET@0..1 "[" [] [] + 1: TW_PROPERTY@1..6 "color" [] [] + 2: COLON@6..7 ":" [] [] + 3: TW_VALUE@7..10 "red" [] [] + 4: R_BRACKET@10..11 "]" [] [] + 5: TW_MODIFIER@11..15 + 0: SLASH@11..12 "/" [] [] + 1: TW_NAMED_VALUE@12..15 + 0: TW_VALUE@12..15 "50" [] [Newline("\n")] + 2: (empty) + 2: EOF@15..15 "" [] [] + +``` diff --git a/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-1.txt b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-1.txt new file mode 100644 index 000000000000..dfef9acc9e0e --- /dev/null +++ b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-1.txt @@ -0,0 +1 @@ +[--pattern-fg:var(--color-gray-950)]/5 diff --git a/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-1.txt.snap b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-1.txt.snap new file mode 100644 index 000000000000..4bc19687b076 --- /dev/null +++ b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-1.txt.snap @@ -0,0 +1,62 @@ +--- +source: crates/biome_tailwind_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```text +[--pattern-fg:var(--color-gray-950)]/5 + +``` + + +## AST + +``` +TwRoot { + bom_token: missing (optional), + candidates: TwCandidateList [ + TwFullCandidate { + variants: TwVariantList [], + candidate: TwArbitraryCandidate { + l_brack_token: L_BRACKET@0..1 "[" [] [], + property_token: TW_PROPERTY@1..13 "--pattern-fg" [] [], + colon_token: COLON@13..14 ":" [] [], + value_token: TW_VALUE@14..35 "var(--color-gray-950)" [] [], + r_brack_token: R_BRACKET@35..36 "]" [] [], + modifier: TwModifier { + slash_token: SLASH@36..37 "/" [] [], + value: TwNamedValue { + value_token: TW_VALUE@37..39 "5" [] [Newline("\n")], + }, + }, + }, + excl_token: missing (optional), + }, + ], + eof_token: EOF@39..39 "" [] [], +} +``` + +## CST + +``` +0: TW_ROOT@0..39 + 0: (empty) + 1: TW_CANDIDATE_LIST@0..39 + 0: TW_FULL_CANDIDATE@0..39 + 0: TW_VARIANT_LIST@0..0 + 1: TW_ARBITRARY_CANDIDATE@0..39 + 0: L_BRACKET@0..1 "[" [] [] + 1: TW_PROPERTY@1..13 "--pattern-fg" [] [] + 2: COLON@13..14 ":" [] [] + 3: TW_VALUE@14..35 "var(--color-gray-950)" [] [] + 4: R_BRACKET@35..36 "]" [] [] + 5: TW_MODIFIER@36..39 + 0: SLASH@36..37 "/" [] [] + 1: TW_NAMED_VALUE@37..39 + 0: TW_VALUE@37..39 "5" [] [Newline("\n")] + 2: (empty) + 2: EOF@39..39 "" [] [] + +``` diff --git a/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-2.txt b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-2.txt new file mode 100644 index 000000000000..00d9d91489b1 --- /dev/null +++ b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-2.txt @@ -0,0 +1 @@ +dark:[--pattern-fg:var(--color-white)]/10 diff --git a/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-2.txt.snap b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-2.txt.snap new file mode 100644 index 000000000000..89c1ac51779e --- /dev/null +++ b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-2.txt.snap @@ -0,0 +1,70 @@ +--- +source: crates/biome_tailwind_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```text +dark:[--pattern-fg:var(--color-white)]/10 + +``` + + +## AST + +``` +TwRoot { + bom_token: missing (optional), + candidates: TwCandidateList [ + TwFullCandidate { + variants: TwVariantList [ + TwStaticVariant { + base_token: TW_BASE@0..4 "dark" [] [], + }, + COLON@4..5 ":" [] [], + ], + candidate: TwArbitraryCandidate { + l_brack_token: L_BRACKET@5..6 "[" [] [], + property_token: TW_PROPERTY@6..18 "--pattern-fg" [] [], + colon_token: COLON@18..19 ":" [] [], + value_token: TW_VALUE@19..37 "var(--color-white)" [] [], + r_brack_token: R_BRACKET@37..38 "]" [] [], + modifier: TwModifier { + slash_token: SLASH@38..39 "/" [] [], + value: TwNamedValue { + value_token: TW_VALUE@39..42 "10" [] [Newline("\n")], + }, + }, + }, + excl_token: missing (optional), + }, + ], + eof_token: EOF@42..42 "" [] [], +} +``` + +## CST + +``` +0: TW_ROOT@0..42 + 0: (empty) + 1: TW_CANDIDATE_LIST@0..42 + 0: TW_FULL_CANDIDATE@0..42 + 0: TW_VARIANT_LIST@0..5 + 0: TW_STATIC_VARIANT@0..4 + 0: TW_BASE@0..4 "dark" [] [] + 1: COLON@4..5 ":" [] [] + 1: TW_ARBITRARY_CANDIDATE@5..42 + 0: L_BRACKET@5..6 "[" [] [] + 1: TW_PROPERTY@6..18 "--pattern-fg" [] [] + 2: COLON@18..19 ":" [] [] + 3: TW_VALUE@19..37 "var(--color-white)" [] [] + 4: R_BRACKET@37..38 "]" [] [] + 5: TW_MODIFIER@38..42 + 0: SLASH@38..39 "/" [] [] + 1: TW_NAMED_VALUE@39..42 + 0: TW_VALUE@39..42 "10" [] [Newline("\n")] + 2: (empty) + 2: EOF@42..42 "" [] [] + +``` diff --git a/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-3.txt b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-3.txt new file mode 100644 index 000000000000..25855b549512 --- /dev/null +++ b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-3.txt @@ -0,0 +1 @@ +[background:blue] diff --git a/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-3.txt.snap b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-3.txt.snap new file mode 100644 index 000000000000..a0f4f5bb50d6 --- /dev/null +++ b/crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-3.txt.snap @@ -0,0 +1,54 @@ +--- +source: crates/biome_tailwind_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```text +[background:blue] + +``` + + +## AST + +``` +TwRoot { + bom_token: missing (optional), + candidates: TwCandidateList [ + TwFullCandidate { + variants: TwVariantList [], + candidate: TwArbitraryCandidate { + l_brack_token: L_BRACKET@0..1 "[" [] [], + property_token: TW_PROPERTY@1..11 "background" [] [], + colon_token: COLON@11..12 ":" [] [], + value_token: TW_VALUE@12..16 "blue" [] [], + r_brack_token: R_BRACKET@16..18 "]" [] [Newline("\n")], + modifier: missing (optional), + }, + excl_token: missing (optional), + }, + ], + eof_token: EOF@18..18 "" [] [], +} +``` + +## CST + +``` +0: TW_ROOT@0..18 + 0: (empty) + 1: TW_CANDIDATE_LIST@0..18 + 0: TW_FULL_CANDIDATE@0..18 + 0: TW_VARIANT_LIST@0..0 + 1: TW_ARBITRARY_CANDIDATE@0..18 + 0: L_BRACKET@0..1 "[" [] [] + 1: TW_PROPERTY@1..11 "background" [] [] + 2: COLON@11..12 ":" [] [] + 3: TW_VALUE@12..16 "blue" [] [] + 4: R_BRACKET@16..18 "]" [] [Newline("\n")] + 5: (empty) + 2: (empty) + 2: EOF@18..18 "" [] [] + +```