diff --git a/CHANGELOG.md b/CHANGELOG.md index 77bf95241..2db870f8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure `@tailwindcss/vite` doesn't crash during HMR when scanned files or directories are deleted ([#20259](https://github.com/tailwindlabs/tailwindcss/pull/20259)) - Ensure `text-[--spacing(…)]` generates `font-size` instead of `color` ([#20260](https://github.com/tailwindlabs/tailwindcss/pull/20260)) - Prevent `@source` patterns from scanning unrelated sibling files and folders ([#20263](https://github.com/tailwindlabs/tailwindcss/pull/20263)) +- Extract class candidates adjacent to Template Toolkit delimiters like `%]…[%` in `.tt`, `.tt2`, and `.tx` files ([#20269](https://github.com/tailwindlabs/tailwindcss/pull/20269)) +- Extract class candidates from conditional Maud syntax like `p.text-black[condition]` ([#20269](https://github.com/tailwindlabs/tailwindcss/pull/20269)) ## [4.3.1] - 2026-06-12 diff --git a/crates/oxide/src/extractor/mod.rs b/crates/oxide/src/extractor/mod.rs index a37d6a0a4..a3d806a27 100644 --- a/crates/oxide/src/extractor/mod.rs +++ b/crates/oxide/src/extractor/mod.rs @@ -908,6 +908,18 @@ mod tests { ); } + // https://github.com/tailwindlabs/tailwindcss/issues/20233 + #[test] + fn test_template_toolkit_syntax() { + assert_extract_candidates_contains( + &pre_process_input( + r#"
"#, + "tx", + ), + vec!["bg-white/40", "bg-white/10"], + ); + } + // https://github.com/tailwindlabs/tailwindcss/issues/17050 #[test] fn test_haml_syntax() { diff --git a/crates/oxide/src/extractor/pre_processors/mod.rs b/crates/oxide/src/extractor/pre_processors/mod.rs index 1876ec6aa..1a04af5c9 100644 --- a/crates/oxide/src/extractor/pre_processors/mod.rs +++ b/crates/oxide/src/extractor/pre_processors/mod.rs @@ -10,6 +10,7 @@ pub mod ruby; pub mod rust; pub mod slim; pub mod svelte; +pub mod template_toolkit; pub mod twig; pub mod vue; @@ -25,5 +26,6 @@ pub use ruby::*; pub use rust::*; pub use slim::*; pub use svelte::*; +pub use template_toolkit::*; pub use twig::*; pub use vue::*; diff --git a/crates/oxide/src/extractor/pre_processors/rust.rs b/crates/oxide/src/extractor/pre_processors/rust.rs index 34c9e0c91..4fa53f793 100644 --- a/crates/oxide/src/extractor/pre_processors/rust.rs +++ b/crates/oxide/src/extractor/pre_processors/rust.rs @@ -104,6 +104,12 @@ impl Rust { b'[' => { bracket_stack.push(cursor.curr()); + + // Handle `p.flex[condition]`. If there is a `-` before it, it will likely be an + // arbitrary value e.g. `text-[red]` + if !matches!(cursor.prev(), b'-') { + result[cursor.pos] = b' '; + } } b']' if !bracket_stack.is_empty() => { @@ -213,4 +219,20 @@ mod tests { let input = r#"html! { \x.px-4.text-black { } }"#; Rust::test(input, r#"html! { \x px-4 text-black { } }"#); } + + // https://github.com/tailwindlabs/tailwindcss/issues/20233 + #[test] + fn test_maud_template_extraction_with_conditional_classes() { + let input = r#" + use maud::{html, Markup}; + + pub fn main() -> Markup { + html! { + p.text-black[cuteness > 50] { "Squee!" } + } + } + "#; + + Rust::test_extract_contains(input, vec!["text-black"]); + } } diff --git a/crates/oxide/src/extractor/pre_processors/template_toolkit.rs b/crates/oxide/src/extractor/pre_processors/template_toolkit.rs new file mode 100644 index 000000000..6456a2694 --- /dev/null +++ b/crates/oxide/src/extractor/pre_processors/template_toolkit.rs @@ -0,0 +1,52 @@ +use crate::cursor; +use crate::extractor::pre_processors::pre_processor::PreProcessor; + +#[derive(Debug, Default)] +pub struct TemplateToolkit; + +impl PreProcessor for TemplateToolkit { + fn process(&self, content: &[u8]) -> Vec { + let len = content.len(); + let mut result = content.to_vec(); + let mut cursor = cursor::Cursor::new(content); + + while cursor.pos < len { + match (cursor.curr(), cursor.next()) { + (b'[', b'%') => result[cursor.pos] = b' ', + (b'%', b']') => result[cursor.pos + 1] = b' ', + _ => {} + } + + cursor.advance(); + } + + result + } +} + +#[cfg(test)] +mod tests { + use super::TemplateToolkit; + use crate::extractor::pre_processors::pre_processor::PreProcessor; + + #[test] + fn test_template_toolkit_pre_processor() { + for (input, expected) in [ + ( + "[% IF $is_open %]bg-white/40[% ELSE %]bg-white/10[% END %]", + " % IF $is_open % bg-white/40 % ELSE % bg-white/10 % END % ", + ), + ("[% WRAPPER %]flex[% END %]", " % WRAPPER % flex % END % "), + ] { + TemplateToolkit::test(input, expected); + } + } + + #[test] + fn test_extraction_between_template_tags_works() { + TemplateToolkit::test_extract_contains( + r#"
"#, + vec!["bg-white/40", "bg-white/10"], + ); + } +} diff --git a/crates/oxide/src/scanner/mod.rs b/crates/oxide/src/scanner/mod.rs index d665b2f4d..02830555d 100644 --- a/crates/oxide/src/scanner/mod.rs +++ b/crates/oxide/src/scanner/mod.rs @@ -480,6 +480,7 @@ pub fn pre_process_input(content: Vec, extension: &str) -> Vec { "slim" | "slang" => Slim.process(&content), "svelte" => Svelte.process(&content), "rs" => Rust.process(&content), + "tt" | "tt2" | "tx" => TemplateToolkit.process(&content), "twig" => Twig.process(&content), "vue" => Vue.process(&content), _ => content,