From c29506b5985ffe9437f3febbed8d477302b23bec Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 31 Oct 2025 12:41:00 -0400 Subject: [PATCH 1/4] 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 fa2be753f7730282d486d750bd1b2a102d37dc53 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 31 Oct 2025 12:41:19 -0400 Subject: [PATCH 2/4] 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 128775df069aed5199fd9f2153020c4a5e163ce9 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 31 Oct 2025 14:30:57 -0400 Subject: [PATCH 3/4] 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 1ed473a0b64a6e36b6e7eba4c75ce4db699185f6 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 31 Oct 2025 14:31:24 -0400 Subject: [PATCH 4/4] 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; - }, + } _ => {} }