From 0f9b97520159c3ca4ef2951a468d0f05efd4b9a7 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 24 Sep 2025 11:12:04 +0200 Subject: [PATCH 1/6] add Rust pre-processor This was based on the Svelte pre processor for Leptos support, see: https://github.com/tailwindlabs/tailwindcss/pull/18093 --- .../oxide/src/extractor/pre_processors/mod.rs | 2 + .../src/extractor/pre_processors/rust.rs | 44 +++++++++++++++++++ crates/oxide/src/scanner/mod.rs | 3 +- 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 crates/oxide/src/extractor/pre_processors/rust.rs diff --git a/crates/oxide/src/extractor/pre_processors/mod.rs b/crates/oxide/src/extractor/pre_processors/mod.rs index 7f55b9d0307b..efcbc53d86d2 100644 --- a/crates/oxide/src/extractor/pre_processors/mod.rs +++ b/crates/oxide/src/extractor/pre_processors/mod.rs @@ -7,6 +7,7 @@ pub mod pre_processor; pub mod pug; pub mod razor; pub mod ruby; +pub mod rust; pub mod slim; pub mod svelte; pub mod vue; @@ -20,6 +21,7 @@ pub use pre_processor::*; pub use pug::*; pub use razor::*; pub use ruby::*; +pub use rust::*; pub use slim::*; pub use svelte::*; pub use vue::*; diff --git a/crates/oxide/src/extractor/pre_processors/rust.rs b/crates/oxide/src/extractor/pre_processors/rust.rs new file mode 100644 index 000000000000..42289fd0bfde --- /dev/null +++ b/crates/oxide/src/extractor/pre_processors/rust.rs @@ -0,0 +1,44 @@ +use crate::extractor::pre_processors::pre_processor::PreProcessor; +use bstr::ByteSlice; + +#[derive(Debug, Default)] +pub struct Rust; + +impl PreProcessor for Rust { + fn process(&self, content: &[u8]) -> Vec { + // Leptos support: https://github.com/tailwindlabs/tailwindcss/pull/18093 + content + .replace(" class:", " class ") + .replace("\tclass:", " class ") + .replace("\nclass:", " class ") + } +} + +#[cfg(test)] +mod tests { + use super::Rust; + use crate::extractor::pre_processors::pre_processor::PreProcessor; + + #[test] + fn test_leptos_extraction() { + for (input, expected) in [ + // Spaces + ( + "
", + "
", + ), + // Tabs + ( + "", + "
", + ), + // Newlines + ( + "", + "
", + ), + ] { + Rust::test(input, expected); + } + } +} diff --git a/crates/oxide/src/scanner/mod.rs b/crates/oxide/src/scanner/mod.rs index a5fe797a0b88..ec6aea642481 100644 --- a/crates/oxide/src/scanner/mod.rs +++ b/crates/oxide/src/scanner/mod.rs @@ -490,7 +490,8 @@ pub fn pre_process_input(content: &[u8], extension: &str) -> Vec { "pug" => Pug.process(content), "rb" | "erb" => Ruby.process(content), "slim" | "slang" => Slim.process(content), - "svelte" | "rs" => Svelte.process(content), + "svelte" => Svelte.process(content), + "rs" => Rust.process(content), "vue" => Vue.process(content), _ => content.to_vec(), } From cc40fbd0ce9aabaebc9e79c97f744eec96e0c26b Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 24 Sep 2025 11:28:38 +0200 Subject: [PATCH 2/6] add support for Maud templates in Rust --- .../src/extractor/pre_processors/rust.rs | 133 +++++++++++++++++- 1 file changed, 131 insertions(+), 2 deletions(-) diff --git a/crates/oxide/src/extractor/pre_processors/rust.rs b/crates/oxide/src/extractor/pre_processors/rust.rs index 42289fd0bfde..6524d88a9ea0 100644 --- a/crates/oxide/src/extractor/pre_processors/rust.rs +++ b/crates/oxide/src/extractor/pre_processors/rust.rs @@ -1,4 +1,9 @@ +use crate::extractor::bracket_stack; +use crate::extractor::cursor; +use crate::extractor::machine::Machine; use crate::extractor::pre_processors::pre_processor::PreProcessor; +use crate::extractor::variant_machine::VariantMachine; +use crate::extractor::MachineState; use bstr::ByteSlice; #[derive(Debug, Default)] @@ -7,10 +12,84 @@ pub struct Rust; impl PreProcessor for Rust { fn process(&self, content: &[u8]) -> Vec { // Leptos support: https://github.com/tailwindlabs/tailwindcss/pull/18093 - content + let replaced_content = content .replace(" class:", " class ") .replace("\tclass:", " class ") - .replace("\nclass:", " class ") + .replace("\nclass:", " class "); + + if replaced_content.contains_str(b"html!") { + self.process_maud_templates(&replaced_content) + } else { + replaced_content + } + } +} + +impl Rust { + fn process_maud_templates(&self, replaced_content: &[u8]) -> Vec { + let len = replaced_content.len(); + let mut result = replaced_content.to_vec(); + let mut cursor = cursor::Cursor::new(replaced_content); + let mut bracket_stack = bracket_stack::BracketStack::default(); + + while cursor.pos < len { + match cursor.curr { + // Only replace `.` with a space if it's not surrounded by numbers. E.g.: + // + // ```diff + // - .flex.items-center + // + flex items-center + // ``` + // + // But with numbers, it's allowed: + // + // ```diff + // - px-2.5 + // + px-2.5 + // ``` + b'.' => { + // Don't replace dots with spaces when inside of any type of brackets, because + // this could be part of arbitrary values. E.g.: `bg-[url(https://example.com)]` + // ^ + if !bracket_stack.is_empty() { + cursor.advance(); + continue; + } + + // If the dot is surrounded by digits, we want to keep it. E.g.: `px-2.5` + // EXCEPT if it's followed by a valid variant that happens to start with a + // digit. + // E.g.: `bg-red-500.2xl:flex` + // ^^^ + if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() { + let mut next_cursor = cursor.clone(); + next_cursor.advance(); + + let mut variant_machine = VariantMachine::default(); + if let MachineState::Done(_) = variant_machine.next(&mut next_cursor) { + result[cursor.pos] = b' '; + } + } else { + result[cursor.pos] = b' '; + } + } + + b'[' => { + bracket_stack.push(cursor.curr); + } + + b']' if !bracket_stack.is_empty() => { + bracket_stack.pop(cursor.curr); + } + + // Consume everything else + _ => {} + }; + + cursor.advance(); + } + + result } } @@ -41,4 +120,54 @@ mod tests { Rust::test(input, expected); } } + + // https://github.com/tailwindlabs/tailwindcss/issues/18984 + #[test] + fn test_maud_template_extraction() { + let input = r#" + use maud::{html, Markup}; + + pub fn main() -> Markup { + html! { + header.px-8.py-4.text-black { + "Hello, world!" + } + } + } + "#; + + Rust::test_extract_contains(input, vec!["px-8", "py-4", "text-black"]); + + // https://maud.lambda.xyz/elements-attributes.html#classes-and-ids-foo-bar + let input = r#" + html! { + input #cannon .big.scary.bright-red type="button" value="Launch Party Cannon"; + } + "#; + Rust::test_extract_contains(input, vec!["big", "scary", "bright-red"]); + + let input = r#" + html! { + div."bg-[#0088cc]" { "Quotes for backticks" } + } + "#; + Rust::test_extract_contains(input, vec!["bg-[#0088cc]"]); + + let input = r#" + html! { + #main { + "Main content!" + .tip { "Storing food in a refrigerator can make it 20% cooler." } + } + } + "#; + Rust::test_extract_contains(input, vec!["tip"]); + + let input = r#" + html! { + div."bg-[url(https://example.com)]" { "Arbitrary values" } + } + "#; + Rust::test_extract_contains(input, vec!["bg-[url(https://example.com)]"]); + } } From b99d2abe25e61530c1691789c81220d26daaf144 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 24 Sep 2025 11:56:54 +0200 Subject: [PATCH 3/6] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15701c0e8b21..e8c3ce0903ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Re-throw errors from PostCSS nodes ([#18373](https://github.com/tailwindlabs/tailwindcss/pull/18373)) - Detect classes in markdown inline directives ([#18967](https://github.com/tailwindlabs/tailwindcss/pull/18967)) - Ensure files with only `@theme` produce no output when built ([#18979](https://github.com/tailwindlabs/tailwindcss/pull/18979)) +- Support Maud templates when extracting classes ([#18988](https://github.com/tailwindlabs/tailwindcss/pull/18988)) ## [4.1.13] - 2025-09-03 From a289dd4dfe7f48490f9b528db05c997ca1d7b610 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 24 Sep 2025 12:04:18 +0200 Subject: [PATCH 4/6] handle strings as-is --- .../src/extractor/pre_processors/rust.rs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/crates/oxide/src/extractor/pre_processors/rust.rs b/crates/oxide/src/extractor/pre_processors/rust.rs index 6524d88a9ea0..344211d26556 100644 --- a/crates/oxide/src/extractor/pre_processors/rust.rs +++ b/crates/oxide/src/extractor/pre_processors/rust.rs @@ -34,6 +34,31 @@ impl Rust { while cursor.pos < len { match cursor.curr { + // Escaped character, skip ahead to the next character + b'\\' => cursor.advance_twice(), + + // Consume strings as-is + b'"' => { + result[cursor.pos] = 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'"' => { + result[cursor.pos] = b' '; + break; + } + + // Everything else is valid + _ => cursor.advance(), + }; + } + } + // Only replace `.` with a space if it's not surrounded by numbers. E.g.: // // ```diff @@ -169,5 +194,17 @@ mod tests { } "#; Rust::test_extract_contains(input, vec!["bg-[url(https://example.com)]"]); + + let input = r#" + html! { + div.px-4.text-black { + "Some text, with unbalanced brackets ][" + } + div.px-8.text-white { + "Some more text, with unbalanced brackets ][" + } + } + "#; + Rust::test_extract_contains(input, vec!["px-4", "text-black", "px-8", "text-white"]); } } From a205f60e9beac39cf02c4a55c819cf178d59b388 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 24 Sep 2025 13:23:11 +0200 Subject: [PATCH 5/6] continue after advancing, because we advance later again --- crates/oxide/src/extractor/pre_processors/rust.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/oxide/src/extractor/pre_processors/rust.rs b/crates/oxide/src/extractor/pre_processors/rust.rs index 344211d26556..fef9ce77fb85 100644 --- a/crates/oxide/src/extractor/pre_processors/rust.rs +++ b/crates/oxide/src/extractor/pre_processors/rust.rs @@ -35,7 +35,10 @@ impl Rust { while cursor.pos < len { match cursor.curr { // Escaped character, skip ahead to the next character - b'\\' => cursor.advance_twice(), + b'\\' => { + cursor.advance_twice(); + continue; + } // Consume strings as-is b'"' => { From 22896925c123a9e8dba7ef2d6a7245981fc182a1 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 24 Sep 2025 13:28:09 +0200 Subject: [PATCH 6/6] add escape test --- crates/oxide/src/extractor/pre_processors/rust.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/oxide/src/extractor/pre_processors/rust.rs b/crates/oxide/src/extractor/pre_processors/rust.rs index fef9ce77fb85..6404fffb5e29 100644 --- a/crates/oxide/src/extractor/pre_processors/rust.rs +++ b/crates/oxide/src/extractor/pre_processors/rust.rs @@ -209,5 +209,8 @@ mod tests { } "#; Rust::test_extract_contains(input, vec!["px-4", "text-black", "px-8", "text-white"]); + + let input = r#"html! { \x.px-4.text-black { } }"#; + Rust::test(input, r#"html! { \x px-4 text-black { } }"#); } }