diff --git a/CHANGELOG.md b/CHANGELOG.md index a275a0134..4f6ba3b56 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 - Vite: Ensure that updates to an imported CSS file are properly propagated after updating templates ([#17347](https://github.com/tailwindlabs/tailwindcss/pull/17347)) - Fix class extraction followed by `(` in Pug ([#17320](https://github.com/tailwindlabs/tailwindcss/pull/17320)) +- Pre process `Slim` templates embedded in Ruby files ([#17336](https://github.com/tailwindlabs/tailwindcss/pull/17336)) ### [4.0.15] - 2025-03-20 diff --git a/Cargo.lock b/Cargo.lock index 21f0b3a13..31b5529a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,6 +27,21 @@ dependencies = [ "nom", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "2.6.0" @@ -156,6 +171,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fancy-regex" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" +dependencies = [ + "bit-set", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", +] + [[package]] name = "fast-glob" version = "0.4.3" @@ -538,6 +564,7 @@ dependencies = [ "classification-macros", "crossbeam", "dunce", + "fancy-regex", "fast-glob", "globwalk", "ignore", diff --git a/crates/oxide/Cargo.toml b/crates/oxide/Cargo.toml index eb8b6156c..f3b67d8d7 100644 --- a/crates/oxide/Cargo.toml +++ b/crates/oxide/Cargo.toml @@ -19,6 +19,7 @@ bexpand = "1.2.0" fast-glob = "0.4.3" classification-macros = { path = "../classification-macros" } regex = "1.11.1" +fancy-regex = "0.14.0" [dev-dependencies] tempfile = "3.13.0" diff --git a/crates/oxide/src/extractor/pre_processors/ruby.rs b/crates/oxide/src/extractor/pre_processors/ruby.rs index 180101479..edc7be49d 100644 --- a/crates/oxide/src/extractor/pre_processors/ruby.rs +++ b/crates/oxide/src/extractor/pre_processors/ruby.rs @@ -3,6 +3,14 @@ use crate::cursor; use crate::extractor::bracket_stack; use crate::extractor::pre_processors::pre_processor::PreProcessor; +use crate::pre_process_input; +use bstr::ByteSlice; +use fancy_regex::Regex; +use std::sync; + +static TEMPLATE_REGEX: sync::LazyLock = sync::LazyLock::new(|| { + Regex::new(r#"\s*(.*?)_template\s*<<[-~]?([A-Z]+?)\n([\s\S]*?)\2"#).unwrap() +}); #[derive(Debug, Default)] pub struct Ruby; @@ -14,6 +22,20 @@ impl PreProcessor for Ruby { let mut cursor = cursor::Cursor::new(content); let mut bracket_stack = bracket_stack::BracketStack::default(); + // Extract embedded template languages + // https://viewcomponent.org/guide/templates.html#interpolations + let content_as_str = std::str::from_utf8(content).unwrap(); + for capture in TEMPLATE_REGEX + .captures_iter(content_as_str) + .filter_map(Result::ok) + { + let lang = capture.get(1).unwrap().as_str(); + let body = capture.get(3).unwrap().as_str(); + let replaced = pre_process_input(body.as_bytes(), lang); + result = result.replace(body, replaced); + } + + // Ruby extraction while cursor.pos < len { // Looking for `%w` or `%W` if cursor.curr != b'%' && !matches!(cursor.next, b'w' | b'W') { @@ -153,4 +175,51 @@ mod tests { Ruby::test_extract_contains(input, expected); } } + + // https://github.com/tailwindlabs/tailwindcss/issues/17334 + #[test] + fn test_embedded_slim_extraction() { + let input = r#" + class QweComponent < ApplicationComponent + slim_template <<~SLIM + button.rounded-full.bg-red-500 + | Some text + button.rounded-full( + class="flex" + ) + | Some text + SLIM + end + "#; + + Ruby::test_extract_contains(input, vec!["rounded-full", "bg-red-500", "flex"]); + + // Embedded Svelte just to verify that we properly pick up the `{x}_template` + let input = r#" + class QweComponent < ApplicationComponent + svelte_template <<~HTML +
+ HTML + end + "#; + + Ruby::test_extract_contains(input, vec!["flex"]); + + // Together in the same file + let input = r#" + class QweComponent < ApplicationComponent + slim_template <<~SLIM + button.z-1.z-2 + | Some text + SLIM + end + + class QweComponent < ApplicationComponent + svelte_template <<~HTML +
+ HTML + end + "#; + Ruby::test_extract_contains(input, vec!["z-1", "z-2", "z-3"]); + } }