Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions crates/oxide/src/extractor/pre_processors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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::*;
213 changes: 213 additions & 0 deletions crates/oxide/src/extractor/pre_processors/rust.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
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)]
pub struct Rust;

impl PreProcessor for Rust {
fn process(&self, content: &[u8]) -> Vec<u8> {
// Leptos support: https://github.com/tailwindlabs/tailwindcss/pull/18093
let replaced_content = content
.replace(" class:", " class ")
.replace("\tclass:", " 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<u8> {
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 {
// Escaped character, skip ahead to the next character
b'\\' => {
cursor.advance_twice();
continue;
}

// 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
// - .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
}
}

#[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
(
"<div class:flex class:px-2.5={condition()}>",
"<div class flex class px-2.5={condition()}>",
),
// Tabs
(
"<div\tclass:flex class:px-2.5={condition()}>",
"<div class flex class px-2.5={condition()}>",
),
// Newlines
(
"<div\nclass:flex class:px-2.5={condition()}>",
"<div class flex class px-2.5={condition()}>",
),
] {
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)]"]);

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"]);
}
}
3 changes: 2 additions & 1 deletion crates/oxide/src/scanner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,8 @@ pub fn pre_process_input(content: &[u8], extension: &str) -> Vec<u8> {
"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(),
}
Expand Down