diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c3ea1823..23cd3eac1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure utilities are sorted based on their actual property order ([#16995](https://github.com/tailwindlabs/tailwindcss/pull/16995)) - Ensure strings in Pug and Slim templates are handled correctly ([#17000](https://github.com/tailwindlabs/tailwindcss/pull/17000)) +- Ensure `}` and `{` are valid boundary characters when extracting candidates ([#17001](https://github.com/tailwindlabs/tailwindcss/pull/17001)) ## [4.0.11] - 2025-03-06 diff --git a/crates/oxide/src/extractor/candidate_machine.rs b/crates/oxide/src/extractor/candidate_machine.rs index 7ba8441a0..e998c17d5 100644 --- a/crates/oxide/src/extractor/candidate_machine.rs +++ b/crates/oxide/src/extractor/candidate_machine.rs @@ -191,7 +191,7 @@ fn is_valid_common_boundary(c: &u8) -> bool { /// A candidate must be preceded by any of these characters. #[inline(always)] fn is_valid_before_boundary(c: &u8) -> bool { - is_valid_common_boundary(c) || matches!(c, b'.') + is_valid_common_boundary(c) || matches!(c, b'.' | b'}') } /// A candidate must be followed by any of these characters. @@ -200,8 +200,8 @@ fn is_valid_before_boundary(c: &u8) -> bool { /// E.g.: `
` Svelte /// ^ #[inline(always)] -fn is_valid_after_boundary(c: &u8) -> bool { - is_valid_common_boundary(c) || matches!(c, b'}' | b']' | b'=') +pub fn is_valid_after_boundary(c: &u8) -> bool { + is_valid_common_boundary(c) || matches!(c, b'}' | b']' | b'=' | b'{') } #[inline(always)] @@ -316,13 +316,16 @@ mod tests { // // HTML // Inside a class (on its own) - (r#"
"#, vec![]), + (r#"
"#, vec!["class"]), // Inside a class (first) - (r#"
"#, vec!["foo"]), + (r#"
"#, vec!["class", "foo"]), // Inside a class (second) - (r#"
"#, vec!["foo"]), + (r#"
"#, vec!["class", "foo"]), // Inside a class (surrounded) - (r#"
"#, vec!["foo", "bar"]), + ( + r#"
"#, + vec!["class", "foo", "bar"], + ), // -------------------------- // // JavaScript diff --git a/crates/oxide/src/extractor/mod.rs b/crates/oxide/src/extractor/mod.rs index 8f71f27eb..a3e16c6ca 100644 --- a/crates/oxide/src/extractor/mod.rs +++ b/crates/oxide/src/extractor/mod.rs @@ -248,6 +248,22 @@ mod tests { assert_eq!(actual, expected); } + fn assert_extract_candidates_contains(input: &str, expected: Vec<&str>) { + let actual = extract_sorted_candidates(input); + + let mut missing = vec![]; + for item in &expected { + if !actual.contains(item) { + missing.push(item); + } + } + + if !missing.is_empty() { + dbg!(&actual, &missing); + panic!("Missing some items"); + } + } + fn assert_extract_sorted_css_variables(input: &str, expected: Vec<&str>) { let actual = extract_sorted_css_variables(input); @@ -311,6 +327,7 @@ mod tests { ( r#"
"#, vec![ + "class", "flex", "items-center", "px-2.5", @@ -363,7 +380,7 @@ mod tests { ("{ underline: true }", vec!["underline", "true"]), ( r#" "#, - vec!["h-4", "w-4", "invisible", "index"], + vec!["className", "h-4", "w-4", "invisible", "index"], ), // You can have variants but in a string. Vue example. ( @@ -480,13 +497,16 @@ mod tests { // // HTML // Inside a class (on its own) - (r#"
"#, vec![]), + (r#"
"#, vec!["class"]), // Inside a class (first) - (r#"
"#, vec!["foo"]), + (r#"
"#, vec!["class", "foo"]), // Inside a class (second) - (r#"
"#, vec!["foo"]), + (r#"
"#, vec!["class", "foo"]), // Inside a class (surrounded) - (r#"
"#, vec!["foo", "bar"]), + ( + r#"
"#, + vec!["class", "foo", "bar"], + ), // -------------------------- // // JavaScript @@ -590,7 +610,7 @@ mod tests { // Quoted attribute ( r#"input(type="checkbox" class="px-2.5")"#, - vec!["checkbox", "px-2.5"], + vec!["checkbox", "class", "px-2.5"], ), ] { assert_extract_sorted_candidates(&pre_process_input(input, "pug"), expected); @@ -611,7 +631,7 @@ mod tests { vec!["bg-blue-100", "2xl:bg-red-100"], ), // Quoted attribute - (r#"div class="px-2.5""#, vec!["div", "px-2.5"]), + (r#"div class="px-2.5""#, vec!["div", "class", "px-2.5"]), ] { assert_extract_sorted_candidates(&pre_process_input(input, "slim"), expected); } @@ -831,6 +851,25 @@ mod tests { &pre_process_input(r#"
"#, "svelte"), vec!["class", "px-4", "condition"], ); + assert_extract_sorted_candidates( + &pre_process_input(r#"
"#, "svelte"), + vec!["class", "flex", "condition"], + ); + } + + // https://github.com/tailwindlabs/tailwindcss/issues/16999 + #[test] + fn test_twig_syntax() { + assert_extract_candidates_contains( + r#"
"#, + vec!["flex", "items-center", "mx-4", "h-4"], + ); + + // With touching both `}` and `{` + assert_extract_candidates_contains( + r#"
"#, + vec!["flex", "block"], + ); } // https://github.com/tailwindlabs/tailwindcss/issues/16982 @@ -839,6 +878,7 @@ mod tests { assert_extract_sorted_candidates( r#"
"#, vec![ + "class", "@md:flex", "@max-md:flex", "@-[36rem]:flex", @@ -852,7 +892,7 @@ mod tests { fn test_classes_containing_number_followed_by_dash_or_underscore() { assert_extract_sorted_candidates( r#"
"#, - vec!["text-Title1_Strong"], + vec!["class", "text-Title1_Strong"], ); } @@ -861,7 +901,11 @@ mod tests { fn test_arbitrary_variable_with_data_type() { assert_extract_sorted_candidates( r#"
"#, - vec!["bg-(length:--my-length)", "bg-[color:var(--my-color)]"], + vec![ + "class", + "bg-(length:--my-length)", + "bg-[color:var(--my-color)]", + ], ); } diff --git a/crates/oxide/src/extractor/named_utility_machine.rs b/crates/oxide/src/extractor/named_utility_machine.rs index 3483abe29..a4d3b9de0 100644 --- a/crates/oxide/src/extractor/named_utility_machine.rs +++ b/crates/oxide/src/extractor/named_utility_machine.rs @@ -1,6 +1,7 @@ use crate::cursor; use crate::extractor::arbitrary_value_machine::ArbitraryValueMachine; use crate::extractor::arbitrary_variable_machine::ArbitraryVariableMachine; +use crate::extractor::candidate_machine::is_valid_after_boundary; use crate::extractor::machine::{Machine, MachineState}; use classification_macros::ClassifyBytes; @@ -120,19 +121,22 @@ impl Machine for NamedUtilityMachine { // E.g.: `:div="{ flex: true }"` (JavaScript object syntax) // ^ Class::AlphaLower | Class::AlphaUpper => { - match cursor.next.into() { - Class::Quote - | Class::Whitespace - | Class::CloseBracket - | Class::Dot - | Class::Colon - | Class::End - | Class::Slash - | Class::Exclamation => return self.done(self.start_pos, cursor), - - // Still valid characters - _ => cursor.advance(), + if is_valid_after_boundary(&cursor.next) || { + // Or any of these characters + // + // - `:`, because of JS object keys + // - `/`, because of modifiers + // - `!`, because of important + matches!( + cursor.next.into(), + Class::Colon | Class::Slash | Class::Exclamation + ) + } { + return self.done(self.start_pos, cursor); } + + // Still valid characters + cursor.advance() } Class::Dash => match cursor.next.into() { @@ -213,14 +217,20 @@ impl Machine for NamedUtilityMachine { // ^ // E.g.: `:div="{ flex: true }"` (JavaScript object syntax) // ^ - Class::Quote - | Class::Whitespace - | Class::CloseBracket - | Class::Dot - | Class::Colon - | Class::End - | Class::Slash - | Class::Exclamation => return self.done(self.start_pos, cursor), + _ if is_valid_after_boundary(&cursor.next) || { + // Or any of these characters + // + // - `:`, because of JS object keys + // - `/`, because of modifiers + // - `!`, because of important + matches!( + cursor.next.into(), + Class::Colon | Class::Slash | Class::Exclamation + ) + } => + { + return self.done(self.start_pos, cursor) + } // Everything else is invalid _ => return self.restart(), @@ -454,15 +464,15 @@ mod tests { // // HTML // Inside a class (on its own) - (r#"
"#, vec!["div"]), + (r#"
"#, vec!["div", "class"]), // Inside a class (first) - (r#"
"#, vec!["div", "foo"]), + (r#"
"#, vec!["div", "class", "foo"]), // Inside a class (second) - (r#"
"#, vec!["div", "foo"]), + (r#"
"#, vec!["div", "class", "foo"]), // Inside a class (surrounded) ( r#"
"#, - vec!["div", "foo", "bar"], + vec!["div", "class", "foo", "bar"], ), // -------------------------- // @@ -475,7 +485,10 @@ mod tests { vec!["let", "classes", "true"], ), // Inside an object (no spaces, key) - (r#"let classes = {'{}':true};"#, vec!["let", "classes"]), + ( + r#"let classes = {'{}':true};"#, + vec!["let", "classes", "true"], + ), // Inside an object (value) ( r#"let classes = { primary: '{}' };"#, diff --git a/crates/oxide/src/extractor/utility_machine.rs b/crates/oxide/src/extractor/utility_machine.rs index ea5241a2d..5f8c74aa7 100644 --- a/crates/oxide/src/extractor/utility_machine.rs +++ b/crates/oxide/src/extractor/utility_machine.rs @@ -266,8 +266,6 @@ mod tests { "bg-(--my-color) flex px-(--my-padding)", vec!["bg-(--my-color)", "flex", "px-(--my-padding)"], ), - // Pug syntax - (".flex.bg-red-500", vec!["flex", "bg-red-500"]), // -------------------------------------------------------- // Exceptions: @@ -293,15 +291,15 @@ mod tests { // // HTML // Inside a class (on its own) - (r#"
"#, vec!["div"]), + (r#"
"#, vec!["div", "class"]), // Inside a class (first) - (r#"
"#, vec!["div", "foo"]), + (r#"
"#, vec!["div", "class", "foo"]), // Inside a class (second) - (r#"
"#, vec!["div", "foo"]), + (r#"
"#, vec!["div", "class", "foo"]), // Inside a class (surrounded) ( r#"
"#, - vec!["div", "foo", "bar"], + vec!["div", "class", "foo", "bar"], ), // -------------------------- // @@ -314,7 +312,10 @@ mod tests { vec!["let", "classes", "true"], ), // Inside an object (no spaces, key) - (r#"let classes = {'{}':true};"#, vec!["let", "classes"]), + ( + r#"let classes = {'{}':true};"#, + vec!["let", "classes", "true"], + ), // Inside an object (value) ( r#"let classes = { primary: '{}' };"#, diff --git a/crates/oxide/src/lib.rs b/crates/oxide/src/lib.rs index d8398478f..de1d06d31 100644 --- a/crates/oxide/src/lib.rs +++ b/crates/oxide/src/lib.rs @@ -3,7 +3,6 @@ use crate::scanner::allowed_paths::resolve_paths; use crate::scanner::detect_sources::DetectSources; use bexpand::Expression; use bstr::ByteSlice; -use extractor::string_machine::StringMachine; use extractor::{Extracted, Extractor}; use fast_glob::glob_match; use fxhash::{FxHashMap, FxHashSet}; @@ -541,6 +540,7 @@ mod tests { ( r#"
"#, vec![ + ("class".to_string(), 5), ("tw:flex!".to_string(), 12), ("tw:sm:block!".to_string(), 21), ("tw:bg-linear-to-t".to_string(), 34),