Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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