From 5c54516463525c6934fce1d81b53fedb33cd4a67 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 31 Oct 2025 12:41:00 -0400 Subject: [PATCH 01/10] Skip over strings in Ruby files --- .../src/extractor/pre_processors/ruby.rs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/crates/oxide/src/extractor/pre_processors/ruby.rs b/crates/oxide/src/extractor/pre_processors/ruby.rs index 6bb1d49238a3..cf916d1fe325 100644 --- a/crates/oxide/src/extractor/pre_processors/ruby.rs +++ b/crates/oxide/src/extractor/pre_processors/ruby.rs @@ -77,6 +77,50 @@ impl PreProcessor for Ruby { // Ruby extraction while cursor.pos < len { + match cursor.curr { + b'"' => { + cursor.advance(); + + while cursor.pos < len { + match cursor.curr { + // Escaped character, skip ahead to the next character + b'\\' => cursor.advance_twice(), + + // End of the string + b'"' => break, + + // Everything else is valid + _ => cursor.advance(), + }; + } + + cursor.advance(); + continue; + }, + + b'\'' => { + cursor.advance(); + + while cursor.pos < len { + match cursor.curr { + // Escaped character, skip ahead to the next character + b'\\' => cursor.advance_twice(), + + // End of the string + b'\'' => break, + + // Everything else is valid + _ => cursor.advance(), + }; + } + + cursor.advance(); + continue; + }, + + _ => {} + } + // Looking for `%w` or `%W` if cursor.curr != b'%' && !matches!(cursor.next, b'w' | b'W') { cursor.advance(); @@ -179,6 +223,13 @@ mod tests { // The nested delimiters evaluated to a flat array of strings // (not nested array). (r#"%w[foo[bar baz]qux]"#, r#"%w foo[bar baz]qux "#), + + (r#""foo # bar""#, r#""foo # bar""#), + (r#"'foo # bar'"#, r#"'foo # bar'"#), + ( + r#"def call = tag.span "Foo", class: %w[rounded-full h-0.75 w-0.75]"#, + r#"def call = tag.span "Foo", class: %w rounded-full h-0.75 w-0.75 "# + ), ] { Ruby::test(input, expected); } @@ -211,6 +262,8 @@ mod tests { "%w(flex data-[state=pending]:bg-(--my-color) flex-col)", vec!["flex", "data-[state=pending]:bg-(--my-color)", "flex-col"], ), + (r#""foo # bar""#, vec!["foo", "bar"]), + (r#"'foo # bar'"#, vec!["foo", "bar"]), ] { Ruby::test_extract_contains(input, expected); } From 54617bc1d9f5cd6cbcd2d0d8364a1f09eea28d1d Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 31 Oct 2025 12:41:19 -0400 Subject: [PATCH 02/10] Replace comments in Ruby code --- .../src/extractor/pre_processors/ruby.rs | 33 +++++++++++++++++++ .../test-fixtures/haml/dst-17051.haml | 2 -- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/crates/oxide/src/extractor/pre_processors/ruby.rs b/crates/oxide/src/extractor/pre_processors/ruby.rs index cf916d1fe325..c1ef70321a02 100644 --- a/crates/oxide/src/extractor/pre_processors/ruby.rs +++ b/crates/oxide/src/extractor/pre_processors/ruby.rs @@ -118,6 +118,28 @@ impl PreProcessor for Ruby { continue; }, + // Replace comments in Ruby files + b'#' => { + result[cursor.pos] = b' '; + cursor.advance(); + + while cursor.pos < len { + match cursor.curr { + // End of the comment + b'\n' => break, + + // Everything else is part of the comment and replaced + _ => { + result[cursor.pos] = b' '; + cursor.advance(); + }, + }; + } + + cursor.advance(); + continue; + }, + _ => {} } @@ -224,6 +246,11 @@ mod tests { // (not nested array). (r#"%w[foo[bar baz]qux]"#, r#"%w foo[bar baz]qux "#), + ( + "# test\n# test\n# {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!]\n%w[flex px-2.5]", + " \n \n \n%w flex px-2.5 " + ), + (r#""foo # bar""#, r#""foo # bar""#), (r#"'foo # bar'"#, r#"'foo # bar'"#), ( @@ -262,6 +289,12 @@ mod tests { "%w(flex data-[state=pending]:bg-(--my-color) flex-col)", vec!["flex", "data-[state=pending]:bg-(--my-color)", "flex-col"], ), + + ( + "# test\n# test\n# {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!]\n%w[flex px-2.5]", + vec!["flex", "px-2.5"], + ), + (r#""foo # bar""#, vec!["foo", "bar"]), (r#"'foo # bar'"#, vec!["foo", "bar"]), ] { diff --git a/crates/oxide/src/extractor/pre_processors/test-fixtures/haml/dst-17051.haml b/crates/oxide/src/extractor/pre_processors/test-fixtures/haml/dst-17051.haml index b233f3b8b2a4..4a3543379420 100644 --- a/crates/oxide/src/extractor/pre_processors/test-fixtures/haml/dst-17051.haml +++ b/crates/oxide/src/extractor/pre_processors/test-fixtures/haml/dst-17051.haml @@ -7,7 +7,6 @@ .relative ^^^^^^^^ - # Blurred background star - ^^^^^^^^^^ ^^^^ .absolute.left-0.z-0{ class: "-top-[400px] -right-[400px]" } ^^^^^^^^ ^^^^^^ ^^^ ^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^^^ .flex.justify-end.blur-3xl @@ -196,7 +195,6 @@ ^^^^^^^^ ^^^^ ^^^ ^^^^ ^^ :escaped - # app/components/character_component.html.haml - ^^^^ = part(:component) do ^^ = part(:head) From a413e53b06962d6c089b5de2505eac36045a778a Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 31 Oct 2025 14:30:57 -0400 Subject: [PATCH 03/10] Discard arbitrary properties with `!` at the top-level MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unless it’s part of `!important` at the end --- .../extractor/arbitrary_property_machine.rs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/crates/oxide/src/extractor/arbitrary_property_machine.rs b/crates/oxide/src/extractor/arbitrary_property_machine.rs index 8ed3f1763091..21b11ce55094 100644 --- a/crates/oxide/src/extractor/arbitrary_property_machine.rs +++ b/crates/oxide/src/extractor/arbitrary_property_machine.rs @@ -231,6 +231,18 @@ impl Machine for ArbitraryPropertyMachine { return self.restart() } + // An `!` at the top-level must be followed by "important" *and* be at the end + // otherwise its invalid + Class::Exclamation if self.bracket_stack.is_empty() => { + if cursor.input[cursor.pos..].starts_with(b"!important]") { + cursor.advance_by(10); + + return self.done(self.start_pos, cursor); + } + + return self.restart() + } + // Everything else is valid _ => cursor.advance(), }; @@ -293,6 +305,9 @@ enum Class { #[bytes(b'/')] Slash, + #[bytes(b'!')] + Exclamation, + #[bytes(b' ', b'\t', b'\n', b'\r', b'\x0C')] Whitespace, @@ -369,6 +384,12 @@ mod tests { "[background:url(https://example.com?q={[{[([{[[2]]}])]}]})]", vec!["[background:url(https://example.com?q={[{[([{[[2]]}])]}]})]"], ), + + // A property containing `!` at the top-level is invalid + ("[color:red!]", vec![]), + + // Unless its part of `!important at the end + ("[color:red!important]", vec!["[color:red!important]"]), ] { for wrapper in [ // No wrapper From 956ce18d1b4666e254320274b12b05adfa16eadb Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 31 Oct 2025 14:31:24 -0400 Subject: [PATCH 04/10] Fix formatting --- .../src/extractor/arbitrary_property_machine.rs | 8 +++----- crates/oxide/src/extractor/pre_processors/ruby.rs | 12 ++++++------ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/crates/oxide/src/extractor/arbitrary_property_machine.rs b/crates/oxide/src/extractor/arbitrary_property_machine.rs index 21b11ce55094..d9baab00275e 100644 --- a/crates/oxide/src/extractor/arbitrary_property_machine.rs +++ b/crates/oxide/src/extractor/arbitrary_property_machine.rs @@ -235,12 +235,12 @@ impl Machine for ArbitraryPropertyMachine { // otherwise its invalid Class::Exclamation if self.bracket_stack.is_empty() => { if cursor.input[cursor.pos..].starts_with(b"!important]") { - cursor.advance_by(10); + cursor.advance_by(10); - return self.done(self.start_pos, cursor); + return self.done(self.start_pos, cursor); } - return self.restart() + return self.restart(); } // Everything else is valid @@ -384,10 +384,8 @@ mod tests { "[background:url(https://example.com?q={[{[([{[[2]]}])]}]})]", vec!["[background:url(https://example.com?q={[{[([{[[2]]}])]}]})]"], ), - // A property containing `!` at the top-level is invalid ("[color:red!]", vec![]), - // Unless its part of `!important at the end ("[color:red!important]", vec!["[color:red!important]"]), ] { diff --git a/crates/oxide/src/extractor/pre_processors/ruby.rs b/crates/oxide/src/extractor/pre_processors/ruby.rs index c1ef70321a02..9649c8c55287 100644 --- a/crates/oxide/src/extractor/pre_processors/ruby.rs +++ b/crates/oxide/src/extractor/pre_processors/ruby.rs @@ -96,7 +96,7 @@ impl PreProcessor for Ruby { cursor.advance(); continue; - }, + } b'\'' => { cursor.advance(); @@ -116,7 +116,7 @@ impl PreProcessor for Ruby { cursor.advance(); continue; - }, + } // Replace comments in Ruby files b'#' => { @@ -130,15 +130,15 @@ impl PreProcessor for Ruby { // Everything else is part of the comment and replaced _ => { - result[cursor.pos] = b' '; - cursor.advance(); - }, + result[cursor.pos] = b' '; + cursor.advance(); + } }; } cursor.advance(); continue; - }, + } _ => {} } From d9163d848f6c251dac575b7d6a35185b24c37266 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 11 Nov 2025 15:54:59 -0500 Subject: [PATCH 05/10] Add tests --- .../src/extractor/pre_processors/ruby.rs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/crates/oxide/src/extractor/pre_processors/ruby.rs b/crates/oxide/src/extractor/pre_processors/ruby.rs index 9649c8c55287..77d0983c89f1 100644 --- a/crates/oxide/src/extractor/pre_processors/ruby.rs +++ b/crates/oxide/src/extractor/pre_processors/ruby.rs @@ -239,9 +239,14 @@ mod tests { "%w(flex data-[state=pending]:bg-(--my-color) flex-col)", "%w flex data-[state=pending]:bg-(--my-color) flex-col ", ), + + // %w …\n + ("%w flex px-2.5\n", "%w flex px-2.5\n"), + // Use backslash to embed spaces in the strings. (r#"%w[foo\ bar baz\ bat]"#, r#"%w foo bar baz bat "#), (r#"%W[foo\ bar baz\ bat]"#, r#"%W foo bar baz bat "#), + // The nested delimiters evaluated to a flat array of strings // (not nested array). (r#"%w[foo[bar baz]qux]"#, r#"%w foo[bar baz]qux "#), @@ -257,6 +262,23 @@ mod tests { r#"def call = tag.span "Foo", class: %w[rounded-full h-0.75 w-0.75]"#, r#"def call = tag.span "Foo", class: %w rounded-full h-0.75 w-0.75 "# ), + + (r#"%w[foo ' bar]"#, r#"%w foo ' bar "#), + (r#"%w[foo " bar]"#, r#"%w foo " bar "#), + (r#"%W[foo ' bar]"#, r#"%W foo ' bar "#), + (r#"%W[foo " bar]"#, r#"%W foo " bar "#), + + (r#"%p foo ' bar "#, r#"%p foo ' bar "#), + (r#"%p foo " bar "#, r#"%p foo " bar "#), + + ( + "%p has a ' quote\n# this should be removed\n%p has a ' quote", + "%p has a ' quote\n \n%p has a ' quote" + ), + ( + "%p has a \" quote\n# this should be removed\n%p has a \" quote", + "%p has a \" quote\n \n%p has a \" quote" + ), ] { Ruby::test(input, expected); } @@ -297,6 +319,8 @@ mod tests { (r#""foo # bar""#, vec!["foo", "bar"]), (r#"'foo # bar'"#, vec!["foo", "bar"]), + + (r#"%w[foo ' bar]"#, vec!["foo", "bar"]), ] { Ruby::test_extract_contains(input, expected); } From 3778c28363322261b3623336d6da7d322b73a7cc Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 11 Nov 2025 15:55:31 -0500 Subject: [PATCH 06/10] Support single line string literal specifiers --- crates/oxide/src/extractor/pre_processors/ruby.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/oxide/src/extractor/pre_processors/ruby.rs b/crates/oxide/src/extractor/pre_processors/ruby.rs index 77d0983c89f1..e815afffa32f 100644 --- a/crates/oxide/src/extractor/pre_processors/ruby.rs +++ b/crates/oxide/src/extractor/pre_processors/ruby.rs @@ -156,6 +156,7 @@ impl PreProcessor for Ruby { b'[' => b']', b'(' => b')', b'{' => b'}', + b' ' => b'\n', _ => { cursor.advance(); continue; @@ -197,7 +198,10 @@ impl PreProcessor for Ruby { // End of the pattern, replace the boundary character with a space _ if cursor.curr == boundary => { - result[cursor.pos] = b' '; + if boundary != b'\n' { + result[cursor.pos] = b' '; + } + break; } From 14f888f1edd8d2b83a143e734d85bcc5c49c127d Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 11 Nov 2025 15:55:57 -0500 Subject: [PATCH 07/10] Only match expected string literal specifiers --- crates/oxide/src/extractor/pre_processors/ruby.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/oxide/src/extractor/pre_processors/ruby.rs b/crates/oxide/src/extractor/pre_processors/ruby.rs index e815afffa32f..046cb4548a32 100644 --- a/crates/oxide/src/extractor/pre_processors/ruby.rs +++ b/crates/oxide/src/extractor/pre_processors/ruby.rs @@ -143,8 +143,8 @@ impl PreProcessor for Ruby { _ => {} } - // Looking for `%w` or `%W` - if cursor.curr != b'%' && !matches!(cursor.next, b'w' | b'W') { + // Looking for `%w`, `%W`, or `%p` + if cursor.curr != b'%' || !matches!(cursor.next, b'w' | b'W' | b'p') { cursor.advance(); continue; } From 5662aaf0f1e327676aae6e8cddd3a6946a489362 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 11 Nov 2025 15:56:30 -0500 Subject: [PATCH 08/10] Fix formatting --- crates/oxide/src/extractor/pre_processors/ruby.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/oxide/src/extractor/pre_processors/ruby.rs b/crates/oxide/src/extractor/pre_processors/ruby.rs index 046cb4548a32..c4ff7ef4b471 100644 --- a/crates/oxide/src/extractor/pre_processors/ruby.rs +++ b/crates/oxide/src/extractor/pre_processors/ruby.rs @@ -199,7 +199,7 @@ impl PreProcessor for Ruby { // End of the pattern, replace the boundary character with a space _ if cursor.curr == boundary => { if boundary != b'\n' { - result[cursor.pos] = b' '; + result[cursor.pos] = b' '; } break; From f3811182327d16e3b34f2f6d80c28ab185cd8238 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 12 Nov 2025 12:13:57 -0500 Subject: [PATCH 09/10] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e44a8358a064..8d2b417a4d80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure validation of `source(…)` happens relative to the file it is in ([#19274](https://github.com/tailwindlabs/tailwindcss/pull/19274)) - Include filename and line numbers in CSS parse errors ([#19282](https://github.com/tailwindlabs/tailwindcss/pull/19282)) +- Skip comments in Ruby files when checking for class names ([#19243](https://github.com/tailwindlabs/tailwindcss/pull/19243)) ### Added From 7f34f3f730fc656cbe53d0f82f7758055c9d618d Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 12 Nov 2025 15:23:29 -0500 Subject: [PATCH 10/10] Handle `#` as a string format delimiter --- crates/oxide/src/extractor/pre_processors/ruby.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/oxide/src/extractor/pre_processors/ruby.rs b/crates/oxide/src/extractor/pre_processors/ruby.rs index c4ff7ef4b471..c94bd945f56e 100644 --- a/crates/oxide/src/extractor/pre_processors/ruby.rs +++ b/crates/oxide/src/extractor/pre_processors/ruby.rs @@ -156,6 +156,7 @@ impl PreProcessor for Ruby { b'[' => b']', b'(' => b')', b'{' => b'}', + b'#' => b'#', b' ' => b'\n', _ => { cursor.advance(); @@ -283,6 +284,11 @@ mod tests { "%p has a \" quote\n# this should be removed\n%p has a \" quote", "%p has a \" quote\n \n%p has a \" quote" ), + + ( + "%w#this text is kept# # this text is not", + "%w this text is kept ", + ), ] { Ruby::test(input, expected); }