Fix Slim attributes (#16985)

This PR fixes an issue in Slim templates where the start of attributes
causes some candidates to be missing.


```slim
.text-xl.text-red-600[
  data-foo="bar"
]
  | This line should be red
```

Because of the `[` attached to the `text-red-600`, the `text-red-600`
was not extracted because `[` is not a valid boundary character.

To solve this, we copied the Pug pre processor and created a dedicated
Slim pre processor. Next, we ensure that we replace `[` with ` ` in this
scenario (by also making sure that we don't replace `[` where it's
important).

Additionally, we noticed that `.` was also replaced inside of arbitrary
values such as URLs. This has been fixed for both Pug and Slim.

Fixes: #16975

# Test plan

1. Added failing tests
2. Existing tests still pass
This commit is contained in:
Robin Malfait 2025-03-06 11:59:14 +01:00 • committed by GitHub
parent 617b7abb81
commit af132fbedb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 131 additions and 2 deletions

View file

@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ensure classes containing `--` are extracted correctly ([#16972](https://github.com/tailwindlabs/tailwindcss/pull/16972))
- Ensure classes containing numbers followed by dash or underscore are extracted correctly ([#16980](https://github.com/tailwindlabs/tailwindcss/pull/16980))
- Ensure arbitrary container queries are extracted correctly ([#16984](https://github.com/tailwindlabs/tailwindcss/pull/16984))
- Ensure classes ending in `[` are extracted in Slim templating language ([#16985](https://github.com/tailwindlabs/tailwindcss/pull/16985))
## [4.0.10] - 2025-03-05

View file

@ -1,9 +1,11 @@
pub mod pre_processor;
pub mod pug;
pub mod ruby;
pub mod slim;
pub mod svelte;
pub use pre_processor::*;
pub use pug::*;
pub use ruby::*;
pub use slim::*;
pub use svelte::*;

View file

@ -1,4 +1,5 @@
use crate::cursor;
use crate::extractor::bracket_stack::BracketStack;
use crate::extractor::machine::Machine;
use crate::extractor::pre_processors::pre_processor::PreProcessor;
use crate::StringMachine;
@ -12,6 +13,7 @@ impl PreProcessor for Pug {
let mut result = content.to_vec();
let mut cursor = cursor::Cursor::new(content);
let mut string_machine = StringMachine;
let mut bracket_stack = BracketStack::default();
while cursor.pos < len {
match cursor.curr {
@ -21,10 +23,18 @@ impl PreProcessor for Pug {
}
// Replace dots with spaces
b'.' => {
b'.' if bracket_stack.is_empty() => {
result[cursor.pos] = b' ';
}
b'(' | b'[' | b'{' => {
bracket_stack.push(cursor.curr);
}
b')' | b']' | b'}' if !bracket_stack.is_empty() => {
bracket_stack.pop(cursor.curr);
}
// Consume everything else
_ => {}
};
@ -49,6 +59,11 @@ mod tests {
(".flex.bg-red-500", " flex bg-red-500"),
// Keep dots in strings
(r#"div(class="px-2.5")"#, r#"div(class="px-2.5")"#),
// Nested brackets
(
"bg-[url(https://example.com/?q=[1,2])]",
"bg-[url(https://example.com/?q=[1,2])]",
),
] {
Pug::test(input, expected);
}

View file

@ -0,0 +1,110 @@
use crate::cursor;
use crate::extractor::bracket_stack::BracketStack;
use crate::extractor::machine::Machine;
use crate::extractor::pre_processors::pre_processor::PreProcessor;
use crate::StringMachine;
#[derive(Debug, Default)]
pub struct Slim;
impl PreProcessor for Slim {
fn process(&self, content: &[u8]) -> Vec<u8> {
let len = content.len();
let mut result = content.to_vec();
let mut cursor = cursor::Cursor::new(content);
let mut string_machine = StringMachine;
let mut bracket_stack = BracketStack::default();
while cursor.pos < len {
match cursor.curr {
// Consume strings as-is
b'\'' | b'"' => {
string_machine.next(&mut cursor);
}
// Replace dots with spaces
b'.' if bracket_stack.is_empty() => {
result[cursor.pos] = b' ';
}
// Any `[` preceded by an alphanumeric value will not be part of a candidate.
//
// E.g.:
//
// ```
// .text-xl.text-red-600[
// ^ not part of the `text-red-600` candidate
// data-foo="bar"
// ]
// | This line should be red
// ```
//
// We know that `-[` is valid for an arbitrary value and that `:[` is valid as a
// variant. However `[color:red]` is also valid, in this case `[` will be preceded
// by nothing or a boundary character.
// Instead of listing all boundary characters, let's list the characters we know
// will be invalid instead.
b'[' if bracket_stack.is_empty()
&& matches!(cursor.prev, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9') =>
{
result[cursor.pos] = b' ';
bracket_stack.push(cursor.curr);
}
b'(' | b'[' | b'{' => {
bracket_stack.push(cursor.curr);
}
b')' | b']' | b'}' if !bracket_stack.is_empty() => {
bracket_stack.pop(cursor.curr);
}
// Consume everything else
_ => {}
};
cursor.advance();
}
result
}
}
#[cfg(test)]
mod tests {
use super::Slim;
use crate::extractor::pre_processors::pre_processor::PreProcessor;
#[test]
fn test_slim_pre_processor() {
for (input, expected) in [
// Convert dots to spaces
("div.flex.bg-red-500", "div flex bg-red-500"),
(".flex.bg-red-500", " flex bg-red-500"),
// Keep dots in strings
(r#"div(class="px-2.5")"#, r#"div(class="px-2.5")"#),
// Replace top-level `(a-z0-9)[` with `$1 `. E.g.: `.flex[x]` -> `.flex x]`
(".text-xl.text-red-600[", " text-xl text-red-600 "),
// But keep important brackets:
(".text-[#0088cc]", " text-[#0088cc]"),
// Arbitrary value and arbitrary modifier
(
".text-[#0088cc].bg-[#0088cc]/[20%]",
" text-[#0088cc] bg-[#0088cc]/[20%]",
),
// Start of arbitrary property
("[color:red]", "[color:red]"),
// Arbitrary container query
("@[320px]:flex", "@[320px]:flex"),
// Nested brackets
(
"bg-[url(https://example.com/?q=[1,2])]",
"bg-[url(https://example.com/?q=[1,2])]",
),
// Nested brackets, with "invalid" syntax but valid due to nesting
("content-['50[]']", "content-['50[]']"),
] {
Slim::test(input, expected);
}
}
}

View file

@ -469,8 +469,9 @@ pub fn pre_process_input(content: &[u8], extension: &str) -> Vec<u8> {
use crate::extractor::pre_processors::*;
match extension {
"pug" => Pug.process(content),
"rb" | "erb" => Ruby.process(content),
"slim" | "pug" => Pug.process(content),
"slim" => Slim.process(content),
"svelte" => Svelte.process(content),
_ => content.to_vec(),
}