Skip to content

Commit

Permalink
Re-introduce automatic var injection shorthand (#15020)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
RobinMalfait and adamwathan authored Nov 18, 2024
1 parent 9c3bfd6 commit 3dc3bad
Show file tree
Hide file tree
Showing 11 changed files with 352 additions and 76 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
126 changes: 95 additions & 31 deletions crates/oxide/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand All @@ -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,

Expand Down Expand Up @@ -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,

Expand Down Expand Up @@ -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();
}
Expand All @@ -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
_ => {}
Expand All @@ -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;
}
}
}

Expand All @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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)
}
Expand All @@ -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()
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down
20 changes: 10 additions & 10 deletions integrations/upgrade/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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)`,
])
},
)
Expand Down Expand Up @@ -639,7 +639,7 @@ test(
'src/index.html',
// prettier-ignore
js`
<div class="bg-[var(--my-red)]"></div>
<div class="bg-(--my-red)"></div>
`,
)

Expand Down Expand Up @@ -798,7 +798,7 @@ test(
'src/index.html',
// prettier-ignore
js`
<div class="bg-[var(--my-red)]"></div>
<div class="bg-(--my-red)"></div>
`,
)

Expand Down Expand Up @@ -873,7 +873,7 @@ test(
'src/index.html',
// prettier-ignore
js`
<div class="bg-[var(--my-red)]"></div>
<div class="bg-(--my-red)"></div>
`,
)

Expand Down Expand Up @@ -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 ---
Expand Down Expand Up @@ -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 ---
Expand Down Expand Up @@ -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 ---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))',
],
]

Expand Down
Loading

0 comments on commit 3dc3bad

Please sign in to comment.