Skip to content

Commit 57e91a6

Browse files
authored
Ensure } and { are valid boundary characters (#17001)
1 parent 85c6e04 commit 57e91a6

File tree

6 files changed

+112
-49
lines changed

6 files changed

+112
-49
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121

2222
- Ensure utilities are sorted based on their actual property order ([#16995](https://github.com/tailwindlabs/tailwindcss/pull/16995))
2323
- Ensure strings in Pug and Slim templates are handled correctly ([#17000](https://github.com/tailwindlabs/tailwindcss/pull/17000))
24+
- Ensure `}` and `{` are valid boundary characters when extracting candidates ([#17001](https://github.com/tailwindlabs/tailwindcss/pull/17001))
2425

2526
## [4.0.11] - 2025-03-06
2627

crates/oxide/src/extractor/candidate_machine.rs

+10-7
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ fn is_valid_common_boundary(c: &u8) -> bool {
191191
/// A candidate must be preceded by any of these characters.
192192
#[inline(always)]
193193
fn is_valid_before_boundary(c: &u8) -> bool {
194-
is_valid_common_boundary(c) || matches!(c, b'.')
194+
is_valid_common_boundary(c) || matches!(c, b'.' | b'}')
195195
}
196196

197197
/// A candidate must be followed by any of these characters.
@@ -200,8 +200,8 @@ fn is_valid_before_boundary(c: &u8) -> bool {
200200
/// E.g.: `<div class:flex="bool">` Svelte
201201
/// ^
202202
#[inline(always)]
203-
fn is_valid_after_boundary(c: &u8) -> bool {
204-
is_valid_common_boundary(c) || matches!(c, b'}' | b']' | b'=')
203+
pub fn is_valid_after_boundary(c: &u8) -> bool {
204+
is_valid_common_boundary(c) || matches!(c, b'}' | b']' | b'=' | b'{')
205205
}
206206

207207
#[inline(always)]
@@ -316,13 +316,16 @@ mod tests {
316316
//
317317
// HTML
318318
// Inside a class (on its own)
319-
(r#"<div class="{}"></div>"#, vec![]),
319+
(r#"<div class="{}"></div>"#, vec!["class"]),
320320
// Inside a class (first)
321-
(r#"<div class="{} foo"></div>"#, vec!["foo"]),
321+
(r#"<div class="{} foo"></div>"#, vec!["class", "foo"]),
322322
// Inside a class (second)
323-
(r#"<div class="foo {}"></div>"#, vec!["foo"]),
323+
(r#"<div class="foo {}"></div>"#, vec!["class", "foo"]),
324324
// Inside a class (surrounded)
325-
(r#"<div class="foo {} bar"></div>"#, vec!["foo", "bar"]),
325+
(
326+
r#"<div class="foo {} bar"></div>"#,
327+
vec!["class", "foo", "bar"],
328+
),
326329
// --------------------------
327330
//
328331
// JavaScript

crates/oxide/src/extractor/mod.rs

+53-9
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,22 @@ mod tests {
248248
assert_eq!(actual, expected);
249249
}
250250

251+
fn assert_extract_candidates_contains(input: &str, expected: Vec<&str>) {
252+
let actual = extract_sorted_candidates(input);
253+
254+
let mut missing = vec![];
255+
for item in &expected {
256+
if !actual.contains(item) {
257+
missing.push(item);
258+
}
259+
}
260+
261+
if !missing.is_empty() {
262+
dbg!(&actual, &missing);
263+
panic!("Missing some items");
264+
}
265+
}
266+
251267
fn assert_extract_sorted_css_variables(input: &str, expected: Vec<&str>) {
252268
let actual = extract_sorted_css_variables(input);
253269

@@ -311,6 +327,7 @@ mod tests {
311327
(
312328
r#"<div class="flex items-center px-2.5 bg-[#0088cc] text-(--my-color)"></div>"#,
313329
vec![
330+
"class",
314331
"flex",
315332
"items-center",
316333
"px-2.5",
@@ -363,7 +380,7 @@ mod tests {
363380
("{ underline: true }", vec!["underline", "true"]),
364381
(
365382
r#" <CheckIcon className={clsx('h-4 w-4', { invisible: index !== 0 })} />"#,
366-
vec!["h-4", "w-4", "invisible", "index"],
383+
vec!["className", "h-4", "w-4", "invisible", "index"],
367384
),
368385
// You can have variants but in a string. Vue example.
369386
(
@@ -480,13 +497,16 @@ mod tests {
480497
//
481498
// HTML
482499
// Inside a class (on its own)
483-
(r#"<div class="{}"></div>"#, vec![]),
500+
(r#"<div class="{}"></div>"#, vec!["class"]),
484501
// Inside a class (first)
485-
(r#"<div class="{} foo"></div>"#, vec!["foo"]),
502+
(r#"<div class="{} foo"></div>"#, vec!["class", "foo"]),
486503
// Inside a class (second)
487-
(r#"<div class="foo {}"></div>"#, vec!["foo"]),
504+
(r#"<div class="foo {}"></div>"#, vec!["class", "foo"]),
488505
// Inside a class (surrounded)
489-
(r#"<div class="foo {} bar"></div>"#, vec!["foo", "bar"]),
506+
(
507+
r#"<div class="foo {} bar"></div>"#,
508+
vec!["class", "foo", "bar"],
509+
),
490510
// --------------------------
491511
//
492512
// JavaScript
@@ -590,7 +610,7 @@ mod tests {
590610
// Quoted attribute
591611
(
592612
r#"input(type="checkbox" class="px-2.5")"#,
593-
vec!["checkbox", "px-2.5"],
613+
vec!["checkbox", "class", "px-2.5"],
594614
),
595615
] {
596616
assert_extract_sorted_candidates(&pre_process_input(input, "pug"), expected);
@@ -611,7 +631,7 @@ mod tests {
611631
vec!["bg-blue-100", "2xl:bg-red-100"],
612632
),
613633
// Quoted attribute
614-
(r#"div class="px-2.5""#, vec!["div", "px-2.5"]),
634+
(r#"div class="px-2.5""#, vec!["div", "class", "px-2.5"]),
615635
] {
616636
assert_extract_sorted_candidates(&pre_process_input(input, "slim"), expected);
617637
}
@@ -831,6 +851,25 @@ mod tests {
831851
&pre_process_input(r#"<div class:px-4='condition'></div>"#, "svelte"),
832852
vec!["class", "px-4", "condition"],
833853
);
854+
assert_extract_sorted_candidates(
855+
&pre_process_input(r#"<div class:flex='condition'></div>"#, "svelte"),
856+
vec!["class", "flex", "condition"],
857+
);
858+
}
859+
860+
// https://github.com/tailwindlabs/tailwindcss/issues/16999
861+
#[test]
862+
fn test_twig_syntax() {
863+
assert_extract_candidates_contains(
864+
r#"<div class="flex items-center mx-4{% if session.isValid %}{% else %} h-4{% endif %}"></div>"#,
865+
vec!["flex", "items-center", "mx-4", "h-4"],
866+
);
867+
868+
// With touching both `}` and `{`
869+
assert_extract_candidates_contains(
870+
r#"<div class="{% if true %}flex{% else %}block{% endif %}">"#,
871+
vec!["flex", "block"],
872+
);
834873
}
835874

836875
// https://github.com/tailwindlabs/tailwindcss/issues/16982
@@ -839,6 +878,7 @@ mod tests {
839878
assert_extract_sorted_candidates(
840879
r#"<div class="@md:flex @max-md:flex @-[36rem]:flex @[36rem]:flex"></div>"#,
841880
vec![
881+
"class",
842882
"@md:flex",
843883
"@max-md:flex",
844884
"@-[36rem]:flex",
@@ -852,7 +892,7 @@ mod tests {
852892
fn test_classes_containing_number_followed_by_dash_or_underscore() {
853893
assert_extract_sorted_candidates(
854894
r#"<div class="text-Title1_Strong"></div>"#,
855-
vec!["text-Title1_Strong"],
895+
vec!["class", "text-Title1_Strong"],
856896
);
857897
}
858898

@@ -861,7 +901,11 @@ mod tests {
861901
fn test_arbitrary_variable_with_data_type() {
862902
assert_extract_sorted_candidates(
863903
r#"<div class="bg-(length:--my-length) bg-[color:var(--my-color)]"></div>"#,
864-
vec!["bg-(length:--my-length)", "bg-[color:var(--my-color)]"],
904+
vec![
905+
"class",
906+
"bg-(length:--my-length)",
907+
"bg-[color:var(--my-color)]",
908+
],
865909
);
866910
}
867911

crates/oxide/src/extractor/named_utility_machine.rs

+38-25
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::cursor;
22
use crate::extractor::arbitrary_value_machine::ArbitraryValueMachine;
33
use crate::extractor::arbitrary_variable_machine::ArbitraryVariableMachine;
4+
use crate::extractor::candidate_machine::is_valid_after_boundary;
45
use crate::extractor::machine::{Machine, MachineState};
56
use classification_macros::ClassifyBytes;
67

@@ -120,19 +121,22 @@ impl Machine for NamedUtilityMachine {
120121
// E.g.: `:div="{ flex: true }"` (JavaScript object syntax)
121122
// ^
122123
Class::AlphaLower | Class::AlphaUpper => {
123-
match cursor.next.into() {
124-
Class::Quote
125-
| Class::Whitespace
126-
| Class::CloseBracket
127-
| Class::Dot
128-
| Class::Colon
129-
| Class::End
130-
| Class::Slash
131-
| Class::Exclamation => return self.done(self.start_pos, cursor),
132-
133-
// Still valid characters
134-
_ => cursor.advance(),
124+
if is_valid_after_boundary(&cursor.next) || {
125+
// Or any of these characters
126+
//
127+
// - `:`, because of JS object keys
128+
// - `/`, because of modifiers
129+
// - `!`, because of important
130+
matches!(
131+
cursor.next.into(),
132+
Class::Colon | Class::Slash | Class::Exclamation
133+
)
134+
} {
135+
return self.done(self.start_pos, cursor);
135136
}
137+
138+
// Still valid characters
139+
cursor.advance()
136140
}
137141

138142
Class::Dash => match cursor.next.into() {
@@ -213,14 +217,20 @@ impl Machine for NamedUtilityMachine {
213217
// ^
214218
// E.g.: `:div="{ flex: true }"` (JavaScript object syntax)
215219
// ^
216-
Class::Quote
217-
| Class::Whitespace
218-
| Class::CloseBracket
219-
| Class::Dot
220-
| Class::Colon
221-
| Class::End
222-
| Class::Slash
223-
| Class::Exclamation => return self.done(self.start_pos, cursor),
220+
_ if is_valid_after_boundary(&cursor.next) || {
221+
// Or any of these characters
222+
//
223+
// - `:`, because of JS object keys
224+
// - `/`, because of modifiers
225+
// - `!`, because of important
226+
matches!(
227+
cursor.next.into(),
228+
Class::Colon | Class::Slash | Class::Exclamation
229+
)
230+
} =>
231+
{
232+
return self.done(self.start_pos, cursor)
233+
}
224234

225235
// Everything else is invalid
226236
_ => return self.restart(),
@@ -454,15 +464,15 @@ mod tests {
454464
//
455465
// HTML
456466
// Inside a class (on its own)
457-
(r#"<div class="{}"></div>"#, vec!["div"]),
467+
(r#"<div class="{}"></div>"#, vec!["div", "class"]),
458468
// Inside a class (first)
459-
(r#"<div class="{} foo"></div>"#, vec!["div", "foo"]),
469+
(r#"<div class="{} foo"></div>"#, vec!["div", "class", "foo"]),
460470
// Inside a class (second)
461-
(r#"<div class="foo {}"></div>"#, vec!["div", "foo"]),
471+
(r#"<div class="foo {}"></div>"#, vec!["div", "class", "foo"]),
462472
// Inside a class (surrounded)
463473
(
464474
r#"<div class="foo {} bar"></div>"#,
465-
vec!["div", "foo", "bar"],
475+
vec!["div", "class", "foo", "bar"],
466476
),
467477
// --------------------------
468478
//
@@ -475,7 +485,10 @@ mod tests {
475485
vec!["let", "classes", "true"],
476486
),
477487
// Inside an object (no spaces, key)
478-
(r#"let classes = {'{}':true};"#, vec!["let", "classes"]),
488+
(
489+
r#"let classes = {'{}':true};"#,
490+
vec!["let", "classes", "true"],
491+
),
479492
// Inside an object (value)
480493
(
481494
r#"let classes = { primary: '{}' };"#,

crates/oxide/src/extractor/utility_machine.rs

+8-7
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,6 @@ mod tests {
266266
"bg-(--my-color) flex px-(--my-padding)",
267267
vec!["bg-(--my-color)", "flex", "px-(--my-padding)"],
268268
),
269-
// Pug syntax
270-
(".flex.bg-red-500", vec!["flex", "bg-red-500"]),
271269
// --------------------------------------------------------
272270

273271
// Exceptions:
@@ -293,15 +291,15 @@ mod tests {
293291
//
294292
// HTML
295293
// Inside a class (on its own)
296-
(r#"<div class="{}"></div>"#, vec!["div"]),
294+
(r#"<div class="{}"></div>"#, vec!["div", "class"]),
297295
// Inside a class (first)
298-
(r#"<div class="{} foo"></div>"#, vec!["div", "foo"]),
296+
(r#"<div class="{} foo"></div>"#, vec!["div", "class", "foo"]),
299297
// Inside a class (second)
300-
(r#"<div class="foo {}"></div>"#, vec!["div", "foo"]),
298+
(r#"<div class="foo {}"></div>"#, vec!["div", "class", "foo"]),
301299
// Inside a class (surrounded)
302300
(
303301
r#"<div class="foo {} bar"></div>"#,
304-
vec!["div", "foo", "bar"],
302+
vec!["div", "class", "foo", "bar"],
305303
),
306304
// --------------------------
307305
//
@@ -314,7 +312,10 @@ mod tests {
314312
vec!["let", "classes", "true"],
315313
),
316314
// Inside an object (no spaces, key)
317-
(r#"let classes = {'{}':true};"#, vec!["let", "classes"]),
315+
(
316+
r#"let classes = {'{}':true};"#,
317+
vec!["let", "classes", "true"],
318+
),
318319
// Inside an object (value)
319320
(
320321
r#"let classes = { primary: '{}' };"#,

crates/oxide/src/lib.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use crate::scanner::allowed_paths::resolve_paths;
33
use crate::scanner::detect_sources::DetectSources;
44
use bexpand::Expression;
55
use bstr::ByteSlice;
6-
use extractor::string_machine::StringMachine;
76
use extractor::{Extracted, Extractor};
87
use fast_glob::glob_match;
98
use fxhash::{FxHashMap, FxHashSet};
@@ -541,6 +540,7 @@ mod tests {
541540
(
542541
r#"<div class="!tw__flex sm:!tw__block tw__bg-gradient-to-t flex tw:[color:red] group-[]:tw__flex"#,
543542
vec![
543+
("class".to_string(), 5),
544544
("!tw__flex".to_string(), 12),
545545
("sm:!tw__block".to_string(), 22),
546546
("tw__bg-gradient-to-t".to_string(), 36),
@@ -553,6 +553,7 @@ mod tests {
553553
(
554554
r#"<div class="tw:flex! tw:sm:block! tw:bg-linear-to-t flex tw:[color:red] tw:in-[.tw\:group]:flex"></div>"#,
555555
vec![
556+
("class".to_string(), 5),
556557
("tw:flex!".to_string(), 12),
557558
("tw:sm:block!".to_string(), 21),
558559
("tw:bg-linear-to-t".to_string(), 34),

0 commit comments

Comments
 (0)