mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Add Maud templating support (#18988)
This PR adds support for Maud templates in Rust. We already had some pre-processing for Rust but for Leptos `class:` syntax. This PR now added a dedicated Rust pre-processor that handles Leptos and Maud syntax. We only start pre-processing Maud templates if the Rust file includes the `html!` macro. ## Test plan Looking at the extractor, you can see that we now do extract the proper classes in Maud templates: <img width="1076" height="1856" alt="image" src="https://github.com/user-attachments/assets/e649e1de-289e-466f-8fab-44a938a47dd5" /> Fixes: #18984
This commit is contained in:
parent
c6e0a55d36
commit
85575a41f4
@ -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
|
||||
|
||||
|
||||
@ -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::*;
|
||||
|
||||
216
crates/oxide/src/extractor/pre_processors/rust.rs
Normal file
216
crates/oxide/src/extractor/pre_processors/rust.rs
Normal file
@ -0,0 +1,216 @@
|
||||
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"]);
|
||||
|
||||
let input = r#"html! { \x.px-4.text-black { } }"#;
|
||||
Rust::test(input, r#"html! { \x px-4 text-black { } }"#);
|
||||
}
|
||||
}
|
||||
@ -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(),
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user