Pre process Slim templates embedded in Ruby files (#17336)

This PR fixes an issue where embedded Slim templates inside of Ruby
files are not pre processed because we pre process based on a file
extension.

This PR also handles embedded SLIM templates using the following syntax:

```rb
slim_template <<~SLIM
  .flex
    .flex-1
      h1 Hello World
    .flex-1
      p This is a test
SLIM
```

~~As far as I can tell, this is only a Slim template thing and not a
Haml template thing but I could be wrong here. See:
https://viewcomponent.org/guide/templates.html#interpolations~~

The ViewComponent package handles anything that looks like
`{lang}_template`, so the lang here will be used as the pre processing
language for now.

Fixes: #17334

# Test plan

1. Added test for this
2. Existing tests pass
3. Made sure that the snippet from the issue works as expected:

Added an example where we have a `slim_template` and a `svelte_template`
to prove that it embeds based on the language. I also added a
`html_template` with Svelte syntax to really make sure that that
_doesn't_ work.

<img width="1816" alt="image"
src="https://github.com/user-attachments/assets/35564a32-9c46-4b51-bb1f-e02f4ffe8b01"
/>
This commit is contained in:
Robin Malfait 2025-03-24 11:58:37 +01:00 • committed by GitHub
parent fac8f7df62
commit 711b9cd0db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 98 additions and 0 deletions

View file

@ -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

27
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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<Regex> = 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
<div class:flex="true"></div>
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
<div class:z-3="true"></div>
HTML
end
"#;
Ruby::test_extract_contains(input, vec!["z-1", "z-2", "z-3"]);
}
}