Skip to content

Commit af132fb

Browse files
authored
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
1 parent 617b7ab commit af132fb

File tree

5 files changed

+131
-2
lines changed

5 files changed

+131
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222
- Ensure classes containing `--` are extracted correctly ([#16972](https://github.com/tailwindlabs/tailwindcss/pull/16972))
2323
- Ensure classes containing numbers followed by dash or underscore are extracted correctly ([#16980](https://github.com/tailwindlabs/tailwindcss/pull/16980))
2424
- Ensure arbitrary container queries are extracted correctly ([#16984](https://github.com/tailwindlabs/tailwindcss/pull/16984))
25+
- Ensure classes ending in `[` are extracted in Slim templating language ([#16985](https://github.com/tailwindlabs/tailwindcss/pull/16985))
2526

2627
## [4.0.10] - 2025-03-05
2728

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
pub mod pre_processor;
22
pub mod pug;
33
pub mod ruby;
4+
pub mod slim;
45
pub mod svelte;
56

67
pub use pre_processor::*;
78
pub use pug::*;
89
pub use ruby::*;
10+
pub use slim::*;
911
pub use svelte::*;

crates/oxide/src/extractor/pre_processors/pug.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::cursor;
2+
use crate::extractor::bracket_stack::BracketStack;
23
use crate::extractor::machine::Machine;
34
use crate::extractor::pre_processors::pre_processor::PreProcessor;
45
use crate::StringMachine;
@@ -12,6 +13,7 @@ impl PreProcessor for Pug {
1213
let mut result = content.to_vec();
1314
let mut cursor = cursor::Cursor::new(content);
1415
let mut string_machine = StringMachine;
16+
let mut bracket_stack = BracketStack::default();
1517

1618
while cursor.pos < len {
1719
match cursor.curr {
@@ -21,10 +23,18 @@ impl PreProcessor for Pug {
2123
}
2224

2325
// Replace dots with spaces
24-
b'.' => {
26+
b'.' if bracket_stack.is_empty() => {
2527
result[cursor.pos] = b' ';
2628
}
2729

30+
b'(' | b'[' | b'{' => {
31+
bracket_stack.push(cursor.curr);
32+
}
33+
34+
b')' | b']' | b'}' if !bracket_stack.is_empty() => {
35+
bracket_stack.pop(cursor.curr);
36+
}
37+
2838
// Consume everything else
2939
_ => {}
3040
};
@@ -49,6 +59,11 @@ mod tests {
4959
(".flex.bg-red-500", " flex bg-red-500"),
5060
// Keep dots in strings
5161
(r#"div(class="px-2.5")"#, r#"div(class="px-2.5")"#),
62+
// Nested brackets
63+
(
64+
"bg-[url(https://example.com/?q=[1,2])]",
65+
"bg-[url(https://example.com/?q=[1,2])]",
66+
),
5267
] {
5368
Pug::test(input, expected);
5469
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
use crate::cursor;
2+
use crate::extractor::bracket_stack::BracketStack;
3+
use crate::extractor::machine::Machine;
4+
use crate::extractor::pre_processors::pre_processor::PreProcessor;
5+
use crate::StringMachine;
6+
7+
#[derive(Debug, Default)]
8+
pub struct Slim;
9+
10+
impl PreProcessor for Slim {
11+
fn process(&self, content: &[u8]) -> Vec<u8> {
12+
let len = content.len();
13+
let mut result = content.to_vec();
14+
let mut cursor = cursor::Cursor::new(content);
15+
let mut string_machine = StringMachine;
16+
let mut bracket_stack = BracketStack::default();
17+
18+
while cursor.pos < len {
19+
match cursor.curr {
20+
// Consume strings as-is
21+
b'\'' | b'"' => {
22+
string_machine.next(&mut cursor);
23+
}
24+
25+
// Replace dots with spaces
26+
b'.' if bracket_stack.is_empty() => {
27+
result[cursor.pos] = b' ';
28+
}
29+
30+
// Any `[` preceded by an alphanumeric value will not be part of a candidate.
31+
//
32+
// E.g.:
33+
//
34+
// ```
35+
// .text-xl.text-red-600[
36+
// ^ not part of the `text-red-600` candidate
37+
// data-foo="bar"
38+
// ]
39+
// | This line should be red
40+
// ```
41+
//
42+
// We know that `-[` is valid for an arbitrary value and that `:[` is valid as a
43+
// variant. However `[color:red]` is also valid, in this case `[` will be preceded
44+
// by nothing or a boundary character.
45+
// Instead of listing all boundary characters, let's list the characters we know
46+
// will be invalid instead.
47+
b'[' if bracket_stack.is_empty()
48+
&& matches!(cursor.prev, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9') =>
49+
{
50+
result[cursor.pos] = b' ';
51+
bracket_stack.push(cursor.curr);
52+
}
53+
54+
b'(' | b'[' | b'{' => {
55+
bracket_stack.push(cursor.curr);
56+
}
57+
58+
b')' | b']' | b'}' if !bracket_stack.is_empty() => {
59+
bracket_stack.pop(cursor.curr);
60+
}
61+
62+
// Consume everything else
63+
_ => {}
64+
};
65+
66+
cursor.advance();
67+
}
68+
69+
result
70+
}
71+
}
72+
73+
#[cfg(test)]
74+
mod tests {
75+
use super::Slim;
76+
use crate::extractor::pre_processors::pre_processor::PreProcessor;
77+
78+
#[test]
79+
fn test_slim_pre_processor() {
80+
for (input, expected) in [
81+
// Convert dots to spaces
82+
("div.flex.bg-red-500", "div flex bg-red-500"),
83+
(".flex.bg-red-500", " flex bg-red-500"),
84+
// Keep dots in strings
85+
(r#"div(class="px-2.5")"#, r#"div(class="px-2.5")"#),
86+
// Replace top-level `(a-z0-9)[` with `$1 `. E.g.: `.flex[x]` -> `.flex x]`
87+
(".text-xl.text-red-600[", " text-xl text-red-600 "),
88+
// But keep important brackets:
89+
(".text-[#0088cc]", " text-[#0088cc]"),
90+
// Arbitrary value and arbitrary modifier
91+
(
92+
".text-[#0088cc].bg-[#0088cc]/[20%]",
93+
" text-[#0088cc] bg-[#0088cc]/[20%]",
94+
),
95+
// Start of arbitrary property
96+
("[color:red]", "[color:red]"),
97+
// Arbitrary container query
98+
("@[320px]:flex", "@[320px]:flex"),
99+
// Nested brackets
100+
(
101+
"bg-[url(https://example.com/?q=[1,2])]",
102+
"bg-[url(https://example.com/?q=[1,2])]",
103+
),
104+
// Nested brackets, with "invalid" syntax but valid due to nesting
105+
("content-['50[]']", "content-['50[]']"),
106+
] {
107+
Slim::test(input, expected);
108+
}
109+
}
110+
}

crates/oxide/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,8 +469,9 @@ pub fn pre_process_input(content: &[u8], extension: &str) -> Vec<u8> {
469469
use crate::extractor::pre_processors::*;
470470

471471
match extension {
472+
"pug" => Pug.process(content),
472473
"rb" | "erb" => Ruby.process(content),
473-
"slim" | "pug" => Pug.process(content),
474+
"slim" => Slim.process(content),
474475
"svelte" => Svelte.process(content),
475476
_ => content.to_vec(),
476477
}

0 commit comments

Comments
 (0)