Skip to content

Commit 4bdc724

Browse files
authored
Fix scanning classes delimited by tab characters (#15169)
This PR fixes an issue where multi-line candidates in Svelte files couldn't be found as reported in #15148 After digging in, the real culprit seems to be that the reproduction used tab `\t` characters instead of spaces and we only delimited explicitly on spaces. Initially I couldn't reproduce this in an integration test until we (@thecrypticace and I) realised that `\t` was being used. ## Test plan: This PR adds an integration test that fails before the fix happens. The fix itself is easy in the sense that we just use all ascii whitespace characters instead of just spaces. Fixes: #15148
1 parent 8ae8f65 commit 4bdc724

File tree

2 files changed

+93
-14
lines changed

2 files changed

+93
-14
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Ensure `.group` and `.peer` are prefixed when using the `prefix(…)` option ([#15174](https://github.com/tailwindlabs/tailwindcss/pull/15174))
1414
- Ensure 3D transforms render correctly in Safari ([#15179](https://github.com/tailwindlabs/tailwindcss/pull/15179))
1515
- Ensure `--spacing-*` variables take precedence over `--container-*` variables ([#15180](https://github.com/tailwindlabs/tailwindcss/pull/15180))
16+
- Fix scanning classes delimited by tab characters ([#15169](https://github.com/tailwindlabs/tailwindcss/pull/15169))
1617

1718
## [4.0.0-beta.2] - 2024-11-22
1819

crates/oxide/src/parser.rs

Lines changed: 92 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -253,9 +253,7 @@ impl<'a> Extractor<'a> {
253253

254254
// Reject candidates that are single camelCase words, e.g.: `useEffect`
255255
if candidate.iter().all(|c| c.is_ascii_alphanumeric())
256-
&& candidate
257-
.iter()
258-
.any(|c| c.is_ascii_uppercase())
256+
&& candidate.iter().any(|c| c.is_ascii_uppercase())
259257
{
260258
return ValidationResult::Invalid;
261259
}
@@ -570,7 +568,7 @@ impl<'a> Extractor<'a> {
570568
}
571569
},
572570

573-
b' ' if !self.opts.preserve_spaces_in_arbitrary => {
571+
c if c.is_ascii_whitespace() && !self.opts.preserve_spaces_in_arbitrary => {
574572
trace!("Arbitrary::SkipAndEndEarly\t");
575573

576574
if let Arbitrary::Brackets { start_idx } | Arbitrary::Parens { start_idx } =
@@ -633,10 +631,8 @@ impl<'a> Extractor<'a> {
633631
match self.cursor.curr {
634632
// Enter arbitrary value mode. E.g.: `bg-[rgba(0, 0, 0)]`
635633
// ^
636-
b'[' if matches!(
637-
self.cursor.prev,
638-
b'@' | b'-' | b' ' | b':' | b'/' | b'!' | b'\0'
639-
) =>
634+
b'[' if matches!(self.cursor.prev, b'@' | b'-' | b':' | b'/' | b'!' | b'\0')
635+
|| self.cursor.prev.is_ascii_whitespace() =>
640636
{
641637
trace!("Arbitrary::Start\t");
642638
self.arbitrary = Arbitrary::Brackets {
@@ -668,7 +664,8 @@ impl<'a> Extractor<'a> {
668664
(true, _) => ParseAction::Consume,
669665

670666
// Looks like the end of a candidate == okay
671-
(_, b' ' | b'\'' | b'"' | b'`') => ParseAction::Consume,
667+
(_, b'\'' | b'"' | b'`') => ParseAction::Consume,
668+
(_, c) if c.is_ascii_whitespace() => ParseAction::Consume,
672669

673670
// Otherwise, not a valid character in a candidate
674671
_ => ParseAction::Skip,
@@ -1542,17 +1539,98 @@ mod test {
15421539

15431540
#[test]
15441541
fn simple_utility_names_with_numbers_work() {
1542+
let candidates = run(r#"<div class="h2 hz"></div>"#, false);
1543+
assert_eq!(candidates, vec!["div", "class", "h2", "hz",]);
1544+
}
1545+
1546+
#[test]
1547+
fn classes_in_an_array_without_whitespace() {
15451548
let candidates = run(
1546-
r#"<div class="h2 hz"></div>"#,
1549+
"let classes = ['bg-black','hover:px-0.5','text-[13px]','[--my-var:1_/_2]','[.foo_&]:px-[0]','[.foo_&]:[color:red]']",
15471550
false,
15481551
);
1552+
15491553
assert_eq!(
15501554
candidates,
15511555
vec![
1552-
"div",
1553-
"class",
1554-
"h2",
1555-
"hz",
1556+
"let",
1557+
"classes",
1558+
"bg-black",
1559+
"hover:px-0.5",
1560+
"text-[13px]",
1561+
"[--my-var:1_/_2]",
1562+
"--my-var:1_/_2",
1563+
"[.foo_&]:px-[0]",
1564+
"[.foo_&]:[color:red]",
1565+
]
1566+
);
1567+
}
1568+
1569+
#[test]
1570+
fn classes_in_an_array_with_spaces() {
1571+
let candidates = run(
1572+
"let classes = ['bg-black', 'hover:px-0.5', 'text-[13px]', '[--my-var:1_/_2]', '[.foo_&]:px-[0]', '[.foo_&]:[color:red]']",
1573+
false,
1574+
);
1575+
1576+
assert_eq!(
1577+
candidates,
1578+
vec![
1579+
"let",
1580+
"classes",
1581+
"bg-black",
1582+
"hover:px-0.5",
1583+
"text-[13px]",
1584+
"[--my-var:1_/_2]",
1585+
"--my-var:1_/_2",
1586+
"[.foo_&]:px-[0]",
1587+
"[.foo_&]:[color:red]",
1588+
]
1589+
);
1590+
}
1591+
1592+
#[test]
1593+
fn classes_in_an_array_with_tabs() {
1594+
let candidates = run(
1595+
"let classes = ['bg-black',\t'hover:px-0.5',\t'text-[13px]',\t'[--my-var:1_/_2]',\t'[.foo_&]:px-[0]',\t'[.foo_&]:[color:red]']",
1596+
false,
1597+
);
1598+
1599+
assert_eq!(
1600+
candidates,
1601+
vec![
1602+
"let",
1603+
"classes",
1604+
"bg-black",
1605+
"hover:px-0.5",
1606+
"text-[13px]",
1607+
"[--my-var:1_/_2]",
1608+
"--my-var:1_/_2",
1609+
"[.foo_&]:px-[0]",
1610+
"[.foo_&]:[color:red]",
1611+
]
1612+
);
1613+
}
1614+
1615+
#[test]
1616+
fn classes_in_an_array_with_newlines() {
1617+
let candidates = run(
1618+
"let classes = [\n'bg-black',\n'hover:px-0.5',\n'text-[13px]',\n'[--my-var:1_/_2]',\n'[.foo_&]:px-[0]',\n'[.foo_&]:[color:red]'\n]",
1619+
false,
1620+
);
1621+
1622+
assert_eq!(
1623+
candidates,
1624+
vec![
1625+
"let",
1626+
"classes",
1627+
"bg-black",
1628+
"hover:px-0.5",
1629+
"text-[13px]",
1630+
"[--my-var:1_/_2]",
1631+
"--my-var:1_/_2",
1632+
"[.foo_&]:px-[0]",
1633+
"[.foo_&]:[color:red]",
15561634
]
15571635
);
15581636
}

0 commit comments

Comments
 (0)