mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Re-introduce automatic var injection shorthand (#15020)
This PR re-introduces the automatic var injection feature. For some backstory, we used to support classes such as `bg-[--my-color]` that resolved as-if you wrote `bg-[var(--my-color)]`. The is issue is that some newer CSS properties accepts dashed-idents (without the `var(…)`). This means that some properties accept `view-timeline-name: --my-name;` (see: https://developer.mozilla.org/en-US/docs/Web/CSS/view-timeline-name). To make this a tiny bit worse, these properties _also_ accept `var(--my-name-reference)` where the variable `--my-name-reference` could reference a dashed-ident such as `--my-name`. This makes the `bg-[--my-color]` ambiguous because we don't know if you want `var(--my-color)` or `--my-color`. With this PR, we bring back the automatic var injection feature as syntactic sugar, but we use a different syntax to avoid the ambiguity. Instead of `bg-[--my-color]`, you can now write `bg-(--my-color)` to get the same effect as `bg-[var(--my-color)]`. This also applies to modifiers, so `bg-red-500/[var(--my-opacity)]` can be written as `bg-red-500/(--my-opacity)`. To go full circle, you can rewrite `bg-[var(--my-color)]/[var(--my-opacity)]` as `bg-(--my-color)/(--my-opacity)`. --- This is implemented as syntactical sugar at the parsing stage and handled when re-printing. Internally the system (and every plugin) still see the proper `var(--my-color)` value. Since this is also handled during printing of the candidate, codemods don't need to be changed but they will provide the newly updated syntax. E.g.: running this on the Catalyst codebase, you'll now see changes like this: <img width="542" alt="image" src="https://github.com/user-attachments/assets/8f0e26f8-f4c9-4cdc-9f28-52307c38610e"> Whereas before we converted this to the much longer `min-w-[var(--button-width)]`. --- Additionally, this required some changes to the Oxide scanner to make sure that `(` and `)` are valid characters for arbitrary-like values. --------- Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This commit is contained in:
parent
9c3bfd6bb7
commit
3dc3bad781
@ -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
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -100,7 +100,7 @@ test(
|
||||
--- ./src/index.html ---
|
||||
<h1>🤠👋</h1>
|
||||
<div
|
||||
class="flex! sm:block! bg-linear-to-t bg-[var(--my-red)] max-w-[var(--breakpoint-md)] ml-[var(--breakpoint-md)]"
|
||||
class="flex! sm:block! bg-linear-to-t bg-(--my-red) max-w-(--breakpoint-md) ml-(--breakpoint-md)"
|
||||
></div>
|
||||
<!-- Migrate to sm -->
|
||||
<div class="blur-sm shadow-sm rounded-sm inset-shadow-sm drop-shadow-sm"></div>
|
||||
@ -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`
|
||||
<div class="bg-[var(--my-red)]"></div>
|
||||
<div class="bg-(--my-red)"></div>
|
||||
`,
|
||||
)
|
||||
|
||||
@ -798,7 +798,7 @@ test(
|
||||
'src/index.html',
|
||||
// prettier-ignore
|
||||
js`
|
||||
<div class="bg-[var(--my-red)]"></div>
|
||||
<div class="bg-(--my-red)"></div>
|
||||
`,
|
||||
)
|
||||
|
||||
@ -873,7 +873,7 @@ test(
|
||||
'src/index.html',
|
||||
// prettier-ignore
|
||||
js`
|
||||
<div class="bg-[var(--my-red)]"></div>
|
||||
<div class="bg-(--my-red)"></div>
|
||||
`,
|
||||
)
|
||||
|
||||
@ -1447,7 +1447,7 @@ test(
|
||||
"
|
||||
--- ./src/index.html ---
|
||||
<div
|
||||
class="flex! sm:block! bg-linear-to-t bg-[var(--my-red)]"
|
||||
class="flex! sm:block! bg-linear-to-t bg-(--my-red)"
|
||||
></div>
|
||||
|
||||
--- ./src/root.1.css ---
|
||||
@ -1664,7 +1664,7 @@ test(
|
||||
"
|
||||
--- ./src/index.html ---
|
||||
<div
|
||||
class="flex! sm:block! bg-linear-to-t bg-[var(--my-red)]"
|
||||
class="flex! sm:block! bg-linear-to-t bg-(--my-red)"
|
||||
></div>
|
||||
|
||||
--- ./src/index.css ---
|
||||
@ -1799,7 +1799,7 @@ test(
|
||||
"
|
||||
--- ./src/index.html ---
|
||||
<div
|
||||
class="flex! sm:block! bg-linear-to-t bg-[var(--my-red)]"
|
||||
class="flex! sm:block! bg-linear-to-t bg-(--my-red)"
|
||||
></div>
|
||||
|
||||
--- ./src/index.css ---
|
||||
|
||||
@ -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))',
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
@ -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}`)
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)]'],
|
||||
|
||||
@ -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', () => [])
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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");
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
@ -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([])
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user