diff --git a/CHANGELOG.md b/CHANGELOG.md index d5e258e47118..85babe29c019 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Reintroduce `max-w-screen-*` utilities that read from the `--breakpoint` namespace as deprecated utilities ([#15013](https://github.com/tailwindlabs/tailwindcss/pull/15013)) +- Support using CSS variables as arbitrary values without `var(…)` by using parentheses instead of square brackets (e.g. `bg-(--my-color)`) ([#15020](https://github.com/tailwindlabs/tailwindcss/pull/15020)) ### Fixed diff --git a/crates/oxide/src/parser.rs b/crates/oxide/src/parser.rs index b58204d8712f..b32f36beaaa9 100644 --- a/crates/oxide/src/parser.rs +++ b/crates/oxide/src/parser.rs @@ -39,6 +39,24 @@ pub struct ExtractorOptions { pub preserve_spaces_in_arbitrary: bool, } +#[derive(Debug, PartialEq, Eq, Clone)] +enum Arbitrary { + /// Not inside any arbitrary value + None, + + /// In arbitrary value mode with square brackets + /// + /// E.g.: `bg-[…]` + /// ^ + Brackets { start_idx: usize }, + + /// In arbitrary value mode with parens + /// + /// E.g.: `bg-(…)` + /// ^ + Parens { start_idx: usize }, +} + pub struct Extractor<'a> { opts: ExtractorOptions, @@ -48,9 +66,9 @@ pub struct Extractor<'a> { idx_start: usize, idx_end: usize, idx_last: usize, - idx_arbitrary_start: usize, - in_arbitrary: bool, + arbitrary: Arbitrary, + in_candidate: bool, in_escape: bool, @@ -105,9 +123,8 @@ impl<'a> Extractor<'a> { idx_start: 0, idx_end: 0, - idx_arbitrary_start: 0, - in_arbitrary: false, + arbitrary: Arbitrary::None, in_candidate: false, in_escape: false, @@ -461,7 +478,7 @@ impl<'a> Extractor<'a> { #[inline(always)] fn parse_arbitrary(&mut self) -> ParseAction<'a> { - // In this we could technically use memchr 6 times (then looped) to find the indexes / bounds of arbitrary valuesq + // In this we could technically use memchr 6 times (then looped) to find the indexes / bounds of arbitrary values if self.in_escape { return self.parse_escaped(); } @@ -479,9 +496,29 @@ impl<'a> Extractor<'a> { self.bracket_stack.pop(); } - // Last bracket is different compared to what we expect, therefore we are not in a - // valid arbitrary value. - _ if !self.in_quotes() => return ParseAction::Skip, + // This is the last bracket meaning the end of arbitrary content + _ if !self.in_quotes() => { + if matches!(self.cursor.next, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9') { + return ParseAction::Consume; + } + + if let Arbitrary::Parens { start_idx } = self.arbitrary { + trace!("Arbitrary::End\t"); + self.arbitrary = Arbitrary::None; + + if self.cursor.pos - start_idx == 1 { + // We have an empty arbitrary value, which is not allowed + return ParseAction::Skip; + } + + // We have a valid arbitrary value + return ParseAction::Consume; + } + + // Last parenthesis is different compared to what we expect, therefore we are + // not in a valid arbitrary value. + return ParseAction::Skip; + } // We're probably in quotes or nested brackets, so we keep going _ => {} @@ -501,12 +538,14 @@ impl<'a> Extractor<'a> { return ParseAction::Consume; } - trace!("Arbitrary::End\t"); - self.in_arbitrary = false; + if let Arbitrary::Brackets { start_idx } = self.arbitrary { + trace!("Arbitrary::End\t"); + self.arbitrary = Arbitrary::None; - if self.cursor.pos - self.idx_arbitrary_start == 1 { - // We have an empty arbitrary value, which is not allowed - return ParseAction::Skip; + if self.cursor.pos - start_idx == 1 { + // We have an empty arbitrary value, which is not allowed + return ParseAction::Skip; + } } } @@ -531,9 +570,13 @@ impl<'a> Extractor<'a> { b' ' if !self.opts.preserve_spaces_in_arbitrary => { trace!("Arbitrary::SkipAndEndEarly\t"); - // Restart the parser ahead of the arbitrary value - // It may pick up more candidates - return ParseAction::RestartAt(self.idx_arbitrary_start + 1); + if let Arbitrary::Brackets { start_idx } | Arbitrary::Parens { start_idx } = + self.arbitrary + { + // Restart the parser ahead of the arbitrary value It may pick up more + // candidates + return ParseAction::RestartAt(start_idx + 1); + } } // Arbitrary values allow any character inside them @@ -550,11 +593,12 @@ impl<'a> Extractor<'a> { #[inline(always)] fn parse_start(&mut self) -> ParseAction<'a> { match self.cursor.curr { - // Enter arbitrary value mode + // Enter arbitrary property mode b'[' => { trace!("Arbitrary::Start\t"); - self.in_arbitrary = true; - self.idx_arbitrary_start = self.cursor.pos; + self.arbitrary = Arbitrary::Brackets { + start_idx: self.cursor.pos, + }; ParseAction::Consume } @@ -584,22 +628,31 @@ impl<'a> Extractor<'a> { #[inline(always)] fn parse_continue(&mut self) -> ParseAction<'a> { match self.cursor.curr { - // Enter arbitrary value mode + // Enter arbitrary value mode. E.g.: `bg-[rgba(0, 0, 0)]` + // ^ b'[' if matches!( self.cursor.prev, b'@' | b'-' | b' ' | b':' | b'/' | b'!' | b'\0' ) => { trace!("Arbitrary::Start\t"); - self.in_arbitrary = true; - self.idx_arbitrary_start = self.cursor.pos; + self.arbitrary = Arbitrary::Brackets { + start_idx: self.cursor.pos, + }; } - // Can't enter arbitrary value mode - // This can't be a candidate - b'[' => { - trace!("Arbitrary::Skip_Start\t"); + // Enter arbitrary value mode. E.g.: `bg-(--my-color)` + // ^ + b'(' if matches!(self.cursor.prev, b'-' | b'/') => { + trace!("Arbitrary::Start\t"); + self.arbitrary = Arbitrary::Parens { + start_idx: self.cursor.pos, + }; + } + // Can't enter arbitrary value mode. This can't be a candidate. + b'[' | b'(' => { + trace!("Arbitrary::Skip_Start\t"); return ParseAction::Skip; } @@ -684,7 +737,7 @@ impl<'a> Extractor<'a> { #[inline(always)] fn can_be_candidate(&mut self) -> bool { self.in_candidate - && !self.in_arbitrary + && matches!(self.arbitrary, Arbitrary::None) && (0..=127).contains(&self.cursor.curr) && (self.idx_start == 0 || self.input[self.idx_start - 1] <= 127) } @@ -696,13 +749,13 @@ impl<'a> Extractor<'a> { self.idx_start = self.cursor.pos; self.idx_end = self.cursor.pos; self.in_candidate = false; - self.in_arbitrary = false; + self.arbitrary = Arbitrary::None; self.in_escape = false; } #[inline(always)] fn parse_char(&mut self) -> ParseAction<'a> { - if self.in_arbitrary { + if !matches!(self.arbitrary, Arbitrary::None) { self.parse_arbitrary() } else if self.in_candidate { self.parse_continue() @@ -732,9 +785,8 @@ impl<'a> Extractor<'a> { self.idx_start = pos; self.idx_end = pos; - self.idx_arbitrary_start = 0; - self.in_arbitrary = false; + self.arbitrary = Arbitrary::None; self.in_candidate = false; self.in_escape = false; @@ -977,6 +1029,18 @@ mod test { assert_eq!(candidates, vec!["m-[2px]"]); } + #[test] + fn it_can_parse_utilities_with_arbitrary_var_shorthand() { + let candidates = run("m-(--my-var)", false); + assert_eq!(candidates, vec!["m-(--my-var)"]); + } + + #[test] + fn it_can_parse_utilities_with_arbitrary_var_shorthand_as_modifier() { + let candidates = run("bg-(--my-color)/(--my-opacity)", false); + assert_eq!(candidates, vec!["bg-(--my-color)/(--my-opacity)"]); + } + #[test] fn it_throws_away_arbitrary_values_that_are_unbalanced() { let candidates = run("m-[calc(100px*2]", false); diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index 5db4487b4803..ebc251a66c92 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -100,7 +100,7 @@ test( --- ./src/index.html ---

🤠👋

@@ -151,9 +151,9 @@ test( candidate`flex!`, candidate`sm:block!`, candidate`bg-linear-to-t`, - candidate`bg-[var(--my-red)]`, - candidate`max-w-[var(--breakpoint-md)]`, - candidate`ml-[var(--breakpoint-md)`, + candidate`bg-(--my-red)`, + candidate`max-w-(--breakpoint-md)`, + candidate`ml-(--breakpoint-md)`, ]) }, ) @@ -639,7 +639,7 @@ test( 'src/index.html', // prettier-ignore js` -
+
`, ) @@ -798,7 +798,7 @@ test( 'src/index.html', // prettier-ignore js` -
+
`, ) @@ -873,7 +873,7 @@ test( 'src/index.html', // prettier-ignore js` -
+
`, ) @@ -1447,7 +1447,7 @@ test( " --- ./src/index.html ---
--- ./src/root.1.css --- @@ -1664,7 +1664,7 @@ test( " --- ./src/index.html ---
--- ./src/index.css --- @@ -1799,7 +1799,7 @@ test( " --- ./src/index.html ---
--- ./src/index.css --- diff --git a/packages/@tailwindcss-upgrade/src/template/candidates.test.ts b/packages/@tailwindcss-upgrade/src/template/candidates.test.ts index 68c3783b7f5d..74f5974fcb8d 100644 --- a/packages/@tailwindcss-upgrade/src/template/candidates.test.ts +++ b/packages/@tailwindcss-upgrade/src/template/candidates.test.ts @@ -123,7 +123,7 @@ const candidates = [ ['bg-[no-repeat_url(/image_13.png)]', 'bg-[no-repeat_url(/image_13.png)]'], [ 'bg-[var(--spacing-0_5,_var(--spacing-1_5,_3rem))]', - 'bg-[var(--spacing-0_5,var(--spacing-1_5,3rem))]', + 'bg-(--spacing-0_5,var(--spacing-1_5,3rem))', ], ] diff --git a/packages/@tailwindcss-upgrade/src/template/candidates.ts b/packages/@tailwindcss-upgrade/src/template/candidates.ts index e4849a6be2a5..6a0ab65bd78f 100644 --- a/packages/@tailwindcss-upgrade/src/template/candidates.ts +++ b/packages/@tailwindcss-upgrade/src/template/candidates.ts @@ -42,12 +42,16 @@ export function printCandidate(designSystem: DesignSystem, candidate: Candidate) if (candidate.value) { if (candidate.value.kind === 'arbitrary') { - if (candidate.value === null) { - base += '' - } else if (candidate.value.dataType) { - base += `-[${candidate.value.dataType}:${printArbitraryValue(candidate.value.value)}]` - } else { - base += `-[${printArbitraryValue(candidate.value.value)}]` + if (candidate.value !== null) { + let isVarValue = isVar(candidate.value.value) + let value = isVarValue ? candidate.value.value.slice(4, -1) : candidate.value.value + let [open, close] = isVarValue ? ['(', ')'] : ['[', ']'] + + if (candidate.value.dataType) { + base += `-${open}${candidate.value.dataType}:${printArbitraryValue(value)}${close}` + } else { + base += `-${open}${printArbitraryValue(value)}${close}` + } } } else if (candidate.value.kind === 'named') { base += `-${candidate.value.value}` @@ -63,8 +67,12 @@ export function printCandidate(designSystem: DesignSystem, candidate: Candidate) // Handle modifier if (candidate.kind === 'arbitrary' || candidate.kind === 'functional') { if (candidate.modifier) { + let isVarValue = isVar(candidate.modifier.value) + let value = isVarValue ? candidate.modifier.value.slice(4, -1) : candidate.modifier.value + let [open, close] = isVarValue ? ['(', ')'] : ['[', ']'] + if (candidate.modifier.kind === 'arbitrary') { - base += `/[${printArbitraryValue(candidate.modifier.value)}]` + base += `/${open}${printArbitraryValue(value)}${close}` } else if (candidate.modifier.kind === 'named') { base += `/${candidate.modifier.value}` } @@ -99,7 +107,11 @@ function printVariant(variant: Variant) { base += variant.root if (variant.value) { if (variant.value.kind === 'arbitrary') { - base += `-[${printArbitraryValue(variant.value.value)}]` + let isVarValue = isVar(variant.value.value) + let value = isVarValue ? variant.value.value.slice(4, -1) : variant.value.value + let [open, close] = isVarValue ? ['(', ')'] : ['[', ']'] + + base += `-${open}${printArbitraryValue(value)}${close}` } else if (variant.value.kind === 'named') { base += `-${variant.value.value}` } @@ -246,9 +258,15 @@ function recursivelyEscapeUnderscores(ast: ValueParser.ValueAstNode[]) { break } case 'separator': - case 'word': { node.value = escapeUnderscore(node.value) break + case 'word': { + // Dashed idents and variables `var(--my-var)` and `--my-var` should not + // have underscores escaped + if (node.value[0] !== '-' && node.value[1] !== '-') { + node.value = escapeUnderscore(node.value) + } + break } default: never(node) @@ -256,6 +274,11 @@ function recursivelyEscapeUnderscores(ast: ValueParser.ValueAstNode[]) { } } +function isVar(value: string) { + let ast = ValueParser.parse(value) + return ast.length === 1 && ast[0].kind === 'function' && ast[0].value === 'var' +} + function never(value: never): never { throw new Error(`Unexpected value: ${value}`) } diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.test.ts b/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.test.ts index b3ddb06d338a..d9e714e9e1e5 100644 --- a/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.test.ts +++ b/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.test.ts @@ -9,10 +9,10 @@ test.each([ ['[--my-color:--my-other-color]', '[--my-color:var(--my-other-color)]'], // Arbitrary values for functional candidates - ['bg-[--my-color]', 'bg-[var(--my-color)]'], - ['bg-[color:--my-color]', 'bg-[color:var(--my-color)]'], - ['border-[length:--my-length]', 'border-[length:var(--my-length)]'], - ['border-[line-width:--my-width]', 'border-[line-width:var(--my-width)]'], + ['bg-[--my-color]', 'bg-(--my-color)'], + ['bg-[color:--my-color]', 'bg-(color:--my-color)'], + ['border-[length:--my-length]', 'border-(length:--my-length)'], + ['border-[line-width:--my-width]', 'border-(line-width:--my-width)'], // Can clean up the workaround for opting out of automatic var injection ['bg-[_--my-color]', 'bg-[--my-color]'], @@ -21,19 +21,19 @@ test.each([ ['border-[line-width:_--my-width]', 'border-[line-width:--my-width]'], // Modifiers - ['[color:--my-color]/[--my-opacity]', '[color:var(--my-color)]/[var(--my-opacity)]'], - ['bg-red-500/[--my-opacity]', 'bg-red-500/[var(--my-opacity)]'], - ['bg-[--my-color]/[--my-opacity]', 'bg-[var(--my-color)]/[var(--my-opacity)]'], - ['bg-[color:--my-color]/[--my-opacity]', 'bg-[color:var(--my-color)]/[var(--my-opacity)]'], + ['[color:--my-color]/[--my-opacity]', '[color:var(--my-color)]/(--my-opacity)'], + ['bg-red-500/[--my-opacity]', 'bg-red-500/(--my-opacity)'], + ['bg-[--my-color]/[--my-opacity]', 'bg-(--my-color)/(--my-opacity)'], + ['bg-[color:--my-color]/[--my-opacity]', 'bg-(color:--my-color)/(--my-opacity)'], // Can clean up the workaround for opting out of automatic var injection ['[color:--my-color]/[_--my-opacity]', '[color:var(--my-color)]/[--my-opacity]'], ['bg-red-500/[_--my-opacity]', 'bg-red-500/[--my-opacity]'], - ['bg-[--my-color]/[_--my-opacity]', 'bg-[var(--my-color)]/[--my-opacity]'], - ['bg-[color:--my-color]/[_--my-opacity]', 'bg-[color:var(--my-color)]/[--my-opacity]'], + ['bg-[--my-color]/[_--my-opacity]', 'bg-(--my-color)/[--my-opacity]'], + ['bg-[color:--my-color]/[_--my-opacity]', 'bg-(color:--my-color)/[--my-opacity]'], // Variants - ['supports-[--test]:flex', 'supports-[var(--test)]:flex'], + ['supports-[--test]:flex', 'supports-(--test):flex'], ['supports-[_--test]:flex', 'supports-[--test]:flex'], // Some properties never had var() injection in v3. diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.test.ts b/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.test.ts index 2bad8ba80912..f5f2a41db31a 100644 --- a/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.test.ts +++ b/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.test.ts @@ -19,11 +19,11 @@ test.each([ // Convert to `var(…)` if we can resolve the path ['[color:theme(colors.red.500)]', '[color:var(--color-red-500)]'], // Arbitrary property ['[color:theme(colors.red.500)]/50', '[color:var(--color-red-500)]/50'], // Arbitrary property + modifier - ['bg-[theme(colors.red.500)]', 'bg-[var(--color-red-500)]'], // Arbitrary value + ['bg-[theme(colors.red.500)]', 'bg-(--color-red-500)'], // Arbitrary value ['bg-[size:theme(spacing.4)]', 'bg-[size:calc(var(--spacing)*4)]'], // Arbitrary value + data type hint // Convert to `var(…)` if we can resolve the path, but keep fallback values - ['bg-[theme(colors.red.500,red)]', 'bg-[var(--color-red-500,red)]'], + ['bg-[theme(colors.red.500,red)]', 'bg-(--color-red-500,red)'], // Keep `theme(…)` if we can't resolve the path ['bg-[theme(colors.foo.1000)]', 'bg-[theme(colors.foo.1000)]'], @@ -66,13 +66,13 @@ test.each([ // Arbitrary property, with more complex modifier (we only allow whole numbers // as bare modifiers). Convert the complex numbers to arbitrary values instead. ['[color:theme(colors.red.500/12.34%)]', '[color:var(--color-red-500)]/[12.34%]'], - ['[color:theme(colors.red.500/var(--opacity))]', '[color:var(--color-red-500)]/[var(--opacity)]'], + ['[color:theme(colors.red.500/var(--opacity))]', '[color:var(--color-red-500)]/(--opacity)'], ['[color:theme(colors.red.500/.12345)]', '[color:var(--color-red-500)]/[12.345]'], ['[color:theme(colors.red.500/50.25%)]', '[color:var(--color-red-500)]/[50.25%]'], // Arbitrary value - ['bg-[theme(colors.red.500/75%)]', 'bg-[var(--color-red-500)]/75'], - ['bg-[theme(colors.red.500/12.34%)]', 'bg-[var(--color-red-500)]/[12.34%]'], + ['bg-[theme(colors.red.500/75%)]', 'bg-(--color-red-500)/75'], + ['bg-[theme(colors.red.500/12.34%)]', 'bg-(--color-red-500)/[12.34%]'], // Arbitrary property that already contains a modifier ['[color:theme(colors.red.500/50%)]/50', '[color:theme(--color-red-500/50%)]/50'], @@ -109,8 +109,8 @@ test.each([ ['[--foo:theme(transitionDuration.500)]', '[--foo:theme(transitionDuration.500)]'], // Renamed theme keys - ['max-w-[theme(screens.md)]', 'max-w-[var(--breakpoint-md)]'], - ['w-[theme(maxWidth.md)]', 'w-[var(--container-md)]'], + ['max-w-[theme(screens.md)]', 'max-w-(--breakpoint-md)'], + ['w-[theme(maxWidth.md)]', 'w-(--container-md)'], // Invalid cases ['[--foo:theme(colors.red.500/50/50)]', '[--foo:theme(colors.red.500/50/50)]'], diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index 570a3424977a..d7f458061715 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -167,6 +167,35 @@ it('should parse a simple utility with an arbitrary variant', () => { `) }) +it('should parse an arbitrary variant using the automatic var shorthand', () => { + let utilities = new Utilities() + utilities.static('flex', () => []) + let variants = new Variants() + variants.functional('supports', () => {}) + + expect(run('supports-(--test):flex', { utilities, variants })).toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "static", + "raw": "supports-(--test):flex", + "root": "flex", + "variants": [ + { + "kind": "functional", + "modifier": null, + "root": "supports", + "value": { + "kind": "arbitrary", + "value": "var(--test)", + }, + }, + ], + }, + ] + `) +}) + it('should parse a simple utility with a parameterized variant', () => { let utilities = new Utilities() utilities.static('flex', () => []) @@ -511,6 +540,29 @@ it('should parse a utility with an arbitrary value', () => { `) }) +it('should parse a utility with an arbitrary value with parens', () => { + let utilities = new Utilities() + utilities.functional('bg', () => []) + + expect(run('bg-(--my-color)', { utilities })).toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "functional", + "modifier": null, + "raw": "bg-(--my-color)", + "root": "bg", + "value": { + "dataType": null, + "kind": "arbitrary", + "value": "var(--my-color)", + }, + "variants": [], + }, + ] + `) +}) + it('should parse a utility with an arbitrary value including a typehint', () => { let utilities = new Utilities() utilities.functional('bg', () => []) @@ -534,6 +586,52 @@ it('should parse a utility with an arbitrary value including a typehint', () => `) }) +it('should parse a utility with an arbitrary value with parens including a typehint', () => { + let utilities = new Utilities() + utilities.functional('bg', () => []) + + expect(run('bg-(color:--my-color)', { utilities })).toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "functional", + "modifier": null, + "raw": "bg-(color:--my-color)", + "root": "bg", + "value": { + "dataType": "color", + "kind": "arbitrary", + "value": "var(--my-color)", + }, + "variants": [], + }, + ] + `) +}) + +it('should parse a utility with an arbitrary value with parens and a fallback', () => { + let utilities = new Utilities() + utilities.functional('bg', () => []) + + expect(run('bg-(color:--my-color,#0088cc)', { utilities })).toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "functional", + "modifier": null, + "raw": "bg-(color:--my-color,#0088cc)", + "root": "bg", + "value": { + "dataType": "color", + "kind": "arbitrary", + "value": "var(--my-color,#0088cc)", + }, + "variants": [], + }, + ] + `) +}) + it('should parse a utility with an arbitrary value with a modifier', () => { let utilities = new Utilities() utilities.functional('bg', () => []) @@ -757,6 +855,32 @@ it('should parse a utility with an implicit variable as the modifier', () => { `) }) +it('should parse a utility with an implicit variable as the modifier using the shorthand', () => { + let utilities = new Utilities() + utilities.functional('bg', () => []) + + expect(run('bg-red-500/(--value)', { utilities })).toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "functional", + "modifier": { + "kind": "arbitrary", + "value": "var(--value)", + }, + "raw": "bg-red-500/(--value)", + "root": "bg", + "value": { + "fraction": null, + "kind": "named", + "value": "red-500", + }, + "variants": [], + }, + ] + `) +}) + it('should parse a utility with an implicit variable as the modifier that is important', () => { let utilities = new Utilities() utilities.functional('bg', () => []) diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts index 669ef4f7c103..c5c8bd85c679 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -14,6 +14,9 @@ type ArbitraryUtilityValue = { * ``` * bg-[color:var(--my-color)] * ^^^^^ + * + * bg-(color:--my-color) + * ^^^^^ * ``` */ dataType: string | null @@ -25,6 +28,9 @@ type ArbitraryUtilityValue = { * * bg-[var(--my_variable)] * ^^^^^^^^^^^^^^^^^^ + * + * bg-(--my_variable) + * ^^^^^^^^^^^^^^ * ``` */ value: string @@ -340,9 +346,9 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter // ^^ -> Root // ^^^^^^^^^ -> Arbitrary value // - // bg-red-[#0088cc] - // ^^^^^^ -> Root - // ^^^^^^^^^ -> Arbitrary value + // border-l-[#0088cc] + // ^^^^^^^^ -> Root + // ^^^^^^^^^ -> Arbitrary value // ``` if (baseWithoutModifier[baseWithoutModifier.length - 1] === ']') { let idx = baseWithoutModifier.indexOf('-[') @@ -359,6 +365,43 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter roots = [[root, value]] } + // If the base of the utility ends with a `)`, then we know it's an arbitrary + // value that encapsulates a CSS variable. This also means that everything + // before the `(…)` part should be the root of the utility. + // + // E.g.: + // + // bg-(--my-var) + // ^^ -> Root + // ^^^^^^^^^^ -> Arbitrary value + // ``` + else if (baseWithoutModifier[baseWithoutModifier.length - 1] === ')') { + let idx = baseWithoutModifier.indexOf('-(') + if (idx === -1) return + + let root = baseWithoutModifier.slice(0, idx) + + // The root of the utility should exist as-is in the utilities map. If not, + // it's an invalid utility and we can skip continue parsing. + if (!designSystem.utilities.has(root, 'functional')) return + + let value = baseWithoutModifier.slice(idx + 2, -1) + + let parts = segment(value, ':') + + let dataType = null + if (parts.length === 2) { + dataType = parts[0] + value = parts[1] + } + + // An arbitrary value with `(…)` should always start with `--` since it + // represents a CSS variable. + if (value[0] !== '-' && value[1] !== '-') return + + roots = [[root, dataType === null ? `[var(${value})]` : `[${dataType}:var(${value})]`]] + } + // Not an arbitrary value else { roots = findRoots(baseWithoutModifier, (root: string) => { @@ -446,6 +489,15 @@ function parseModifier(modifier: string): CandidateModifier { } } + if (modifier[0] === '(' && modifier[modifier.length - 1] === ')') { + let arbitraryValue = modifier.slice(1, -1) + + return { + kind: 'arbitrary', + value: decodeArbitraryValue(`var(${arbitraryValue})`), + } + } + return { kind: 'named', value: modifier, @@ -546,6 +598,18 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia } } + if (value[0] === '(' && value[value.length - 1] === ')') { + return { + kind: 'functional', + root, + modifier: modifier === null ? null : parseModifier(modifier), + value: { + kind: 'arbitrary', + value: decodeArbitraryValue(`var(${value.slice(1, -1)})`), + }, + } + } + return { kind: 'functional', root, diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 4e44129cb96c..ece3d78f8324 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -127,7 +127,7 @@ describe('compiling CSS', () => { @tailwind utilities; `, [ - 'bg-[no-repeat_url(./my_file.jpg)', + 'bg-[no-repeat_url(./my_file.jpg)]', 'ml-[var(--spacing-1_5,_var(--spacing-2_5,_1rem))]', 'ml-[theme(--spacing-1_5,theme(--spacing-2_5,_1rem)))]', ], @@ -146,8 +146,8 @@ describe('compiling CSS', () => { margin-left: var(--spacing-1_5, var(--spacing-2_5, 1rem)); } - .bg-\\[no-repeat_url\\(\\.\\/my_file\\.jpg\\) { - background-color: no-repeat url("./")my file. jpg; + .bg-\\[no-repeat_url\\(\\.\\/my_file\\.jpg\\)\\] { + background-color: no-repeat url("./my_file.jpg"); }" `) }) diff --git a/packages/tailwindcss/src/intellisense.test.ts b/packages/tailwindcss/src/intellisense.test.ts index cccf3d4eb14a..e13ce4ab1b3a 100644 --- a/packages/tailwindcss/src/intellisense.test.ts +++ b/packages/tailwindcss/src/intellisense.test.ts @@ -365,7 +365,7 @@ test('Functional utilities from plugins are listed in hovers and completions', a expect(classNames).not.toContain('custom-2-unknown') - // matchUtilities with a any modifiers + // matchUtilities with any modifiers expect(classNames).toContain('custom-3-red') expect(classMap.get('custom-3-red')?.modifiers).toEqual([])