Skip to content

Commit 85c6e04

Browse files
authored
Ensure strings in Pug and Slim templates are handled correctly (#17000)
This PR fixes an issue where strings in the Pug and Slim pre-processor were handled using the `string_machine`. However, the `string_machine` is not for strings inside of Tailwind CSS classes which means that whitespace is invalid. This means that parts of the code that _are_ inside strings will not be inside strings and parts of the code that are not inside strings will be part of a potential string. This is a bit confusing to wrap your head around, but here is a visual representation of the problem: ``` .join(' ') ^ 3. start of new string, which means that the `)` _could_ be part of a string if a new `'` occurs later. ^ 2. whitespace is not allowed, stop string ^ 1. start of string ``` Fixes: #16998 # Test plan 1. Added new test 2. Existing tests still pass 3. Added a simple test helper to make sure that we can extract the correct candidates _after_ pre-processing
1 parent 3d0606b commit 85c6e04

File tree

4 files changed

+103
-8
lines changed

4 files changed

+103
-8
lines changed

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
### Fixed
2121

2222
- Ensure utilities are sorted based on their actual property order ([#16995](https://github.com/tailwindlabs/tailwindcss/pull/16995))
23+
- Ensure strings in Pug and Slim templates are handled correctly ([#17000](https://github.com/tailwindlabs/tailwindcss/pull/17000))
2324

2425
## [4.0.11] - 2025-03-06
2526

Diff for: crates/oxide/src/extractor/pre_processors/pre_processor.rs

+34
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,38 @@ pub trait PreProcessor: Sized + Default {
2525

2626
assert_eq!(actual, expected);
2727
}
28+
29+
#[cfg(test)]
30+
fn test_extract_contains(input: &str, items: Vec<&str>) {
31+
use crate::extractor::{Extracted, Extractor};
32+
33+
let input = input.as_bytes();
34+
35+
let processor = Self::default();
36+
let transformed = processor.process(input);
37+
38+
let extracted = Extractor::new(&transformed).extract();
39+
40+
// Extract all candidates and css variables.
41+
let candidates = extracted
42+
.iter()
43+
.filter_map(|x| match x {
44+
Extracted::Candidate(bytes) => std::str::from_utf8(bytes).ok(),
45+
Extracted::CssVariable(bytes) => std::str::from_utf8(bytes).ok(),
46+
})
47+
.collect::<Vec<_>>();
48+
49+
// Ensure all items are present in the candidates.
50+
let mut missing = vec![];
51+
for item in &items {
52+
if !candidates.contains(item) {
53+
missing.push(item);
54+
}
55+
}
56+
57+
if !missing.is_empty() {
58+
dbg!(&candidates, &missing);
59+
panic!("Missing some items");
60+
}
61+
}
2862
}

Diff for: crates/oxide/src/extractor/pre_processors/pug.rs

+17-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
use crate::cursor;
22
use crate::extractor::bracket_stack::BracketStack;
3-
use crate::extractor::machine::Machine;
43
use crate::extractor::pre_processors::pre_processor::PreProcessor;
5-
use crate::StringMachine;
64

75
#[derive(Debug, Default)]
86
pub struct Pug;
@@ -12,14 +10,29 @@ impl PreProcessor for Pug {
1210
let len = content.len();
1311
let mut result = content.to_vec();
1412
let mut cursor = cursor::Cursor::new(content);
15-
let mut string_machine = StringMachine;
1613
let mut bracket_stack = BracketStack::default();
1714

1815
while cursor.pos < len {
1916
match cursor.curr {
2017
// Consume strings as-is
2118
b'\'' | b'"' => {
22-
string_machine.next(&mut cursor);
19+
let len = cursor.input.len();
20+
let end_char = cursor.curr;
21+
22+
cursor.advance();
23+
24+
while cursor.pos < len {
25+
match cursor.curr {
26+
// Escaped character, skip ahead to the next character
27+
b'\\' => cursor.advance_twice(),
28+
29+
// End of the string
30+
b'\'' | b'"' if cursor.curr == end_char => break,
31+
32+
// Everything else is valid
33+
_ => cursor.advance(),
34+
};
35+
}
2336
}
2437

2538
// Replace dots with spaces

Diff for: crates/oxide/src/extractor/pre_processors/slim.rs

+51-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
use crate::cursor;
22
use crate::extractor::bracket_stack::BracketStack;
3-
use crate::extractor::machine::Machine;
43
use crate::extractor::pre_processors::pre_processor::PreProcessor;
5-
use crate::StringMachine;
64

75
#[derive(Debug, Default)]
86
pub struct Slim;
@@ -12,14 +10,29 @@ impl PreProcessor for Slim {
1210
let len = content.len();
1311
let mut result = content.to_vec();
1412
let mut cursor = cursor::Cursor::new(content);
15-
let mut string_machine = StringMachine;
1613
let mut bracket_stack = BracketStack::default();
1714

1815
while cursor.pos < len {
1916
match cursor.curr {
2017
// Consume strings as-is
2118
b'\'' | b'"' => {
22-
string_machine.next(&mut cursor);
19+
let len = cursor.input.len();
20+
let end_char = cursor.curr;
21+
22+
cursor.advance();
23+
24+
while cursor.pos < len {
25+
match cursor.curr {
26+
// Escaped character, skip ahead to the next character
27+
b'\\' => cursor.advance_twice(),
28+
29+
// End of the string
30+
b'\'' | b'"' if cursor.curr == end_char => break,
31+
32+
// Everything else is valid
33+
_ => cursor.advance(),
34+
};
35+
}
2336
}
2437

2538
// Replace dots with spaces
@@ -103,8 +116,42 @@ mod tests {
103116
),
104117
// Nested brackets, with "invalid" syntax but valid due to nesting
105118
("content-['50[]']", "content-['50[]']"),
119+
// Escaped string
120+
("content-['a\'b\'c\'']", "content-['a\'b\'c\'']"),
106121
] {
107122
Slim::test(input, expected);
108123
}
109124
}
125+
126+
#[test]
127+
fn test_nested_slim_syntax() {
128+
let input = r#"
129+
.text-black[
130+
data-controller= ['foo', ('bar' if rand.positive?)].join(' ')
131+
]
132+
.bg-green-300
133+
| BLACK on GREEN - OK
134+
135+
.bg-red-300[
136+
data-foo= 42
137+
]
138+
| Should be BLACK on RED - FAIL
139+
"#;
140+
141+
let expected = r#"
142+
text-black
143+
data-controller= ['foo', ('bar' if rand.positive?)].join(' ')
144+
]
145+
bg-green-300
146+
| BLACK on GREEN - OK
147+
148+
bg-red-300
149+
data-foo= 42
150+
]
151+
| Should be BLACK on RED - FAIL
152+
"#;
153+
154+
Slim::test(input, expected);
155+
Slim::test_extract_contains(input, vec!["text-black", "bg-green-300", "bg-red-300"]);
156+
}
110157
}

0 commit comments

Comments
 (0)