Fix class extraction followed by ( in Slim (#17278)

This PR fixes an issue where using the class shorthand in Slim
templates, followed by an `(` results in the last class being ignored.

E.g.:

```slim
body.border-t-4.p-8(class="#{body_classes}" data-hotwire-native="#{hotwire_native_app?}" data-controller="update-time-zone")
```

This is because we will eventually extract `p-8` but it's followed by an
invalid boundary character `(`.

To solve this, we make sure to replace the `(` with a space. We already
do a similar thing when the classes are followed by an `[`.

One caveat, we _can_ have `(` in our classes, like `bg-(--my-color)`.
But in my testing this is not something that can be used in the
shorthand version.

E.g.:
```slim
div.bg-(--my-color)
```

Compiles to:
```html
<div --my-color="" class="bg-"></div>
```

So I didn't add any special handling for this. Even when trying to
escape the `(`, `-` and `)` characters, it still doesn't work. E.g.:

```slim
div.bg-\(--my-color\)
```

Compiles to:
```html
<div class="bg-">\(--my-color\)</div>
```

# Test plan

1. Added test for the issue
2. Existing tests pass
3. Verified via the extractor tool:

| Before | After |
| --- | --- |
| <img width="958" alt="image"
src="https://github.com/user-attachments/assets/f72c420e-5429-424f-a01d-12f724062bf2"
/> | <img width="958" alt="image"
src="https://github.com/user-attachments/assets/b0cc8f2f-97a8-4fca-8813-3bb1da8d99a8"
/> |

---

Fixes: #17277
This commit is contained in:
Robin Malfait 2025-03-19 14:50:30 +01:00 committed by GitHub
parent d698c10836
commit f369e22172
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 55 additions and 1 deletions

View File

@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Remove redundant `line-height: initial` from Preflight ([#15212](https://github.com/tailwindlabs/tailwindcss/pull/15212))
- Prevent segfault when loaded in a worker thread on Linux ([#17276](https://github.com/tailwindlabs/tailwindcss/pull/17276))
- Ensure multiple `--value(…)` or `--modifier(…)` calls don't delete subsequent declarations ([#17273](https://github.com/tailwindlabs/tailwindcss/pull/17273))
- Fix class extraction followed by `(` in Slim ([#17278](https://github.com/tailwindlabs/tailwindcss/pull/17278))
## [4.0.14] - 2025-03-13

View File

@ -80,6 +80,23 @@ impl PreProcessor for Slim {
bracket_stack.push(cursor.curr);
}
// In slim the class name shorthand can be followed by a parenthesis. E.g.:
//
// ```slim
// body.border-t-4.p-8(attr=value)
// ^ Not part of the p-8 class
// ```
//
// This means that we need to replace all these `(` and `)` with spaces to make
// sure that we can extract the `p-8`.
//
// However, we also need to make sure that we keep the parens that are part of the
// utility class. E.g.: `bg-(--my-color)`.
b'(' if bracket_stack.is_empty() && !matches!(cursor.prev, b'-' | b'/') => {
result[cursor.pos] = b' ';
bracket_stack.push(cursor.curr);
}
b'(' | b'[' | b'{' => {
bracket_stack.push(cursor.curr);
}
@ -116,7 +133,7 @@ mod tests {
" bg-red-500 2xl:flex bg-green-200 3xl:flex",
),
// Keep dots in strings
(r#"div(class="px-2.5")"#, r#"div(class="px-2.5")"#),
(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:
@ -194,6 +211,42 @@ mod tests {
Slim::test_extract_contains(input, vec!["text-red-500", "text-3xl"]);
}
// https://github.com/tailwindlabs/tailwindcss/issues/17277
#[test]
fn test_class_shorthand_followed_by_parens() {
let input = r#"
body.border-t-4.p-8(class="\#{body_classes}" data-hotwire-native="\#{hotwire_native_app?}" data-controller="update-time-zone")
"#;
Slim::test_extract_contains(input, vec!["border-t-4", "p-8"]);
// Additional test with CSS Variable shorthand syntax in the attribute itself because `(`
// and `)` are not valid in the class shorthand version.
//
// Also included an arbitrary value including `(` and `)` to make sure that we don't
// accidentally remove those either.
let input = r#"
body.p-8(class="bg-(--my-color) bg-(--my-color)/(--my-opacity) bg-[url(https://example.com)]")
"#;
Slim::test_extract_contains(
input,
vec![
"p-8",
"bg-(--my-color)",
"bg-(--my-color)/(--my-opacity)",
"bg-[url(https://example.com)]",
],
);
// Top-level class shorthand with parens
let input = r#"
div class="bg-(--my-color) bg-(--my-color)/(--my-opacity)"
"#;
Slim::test_extract_contains(
input,
vec!["bg-(--my-color)", "bg-(--my-color)/(--my-opacity)"],
);
}
#[test]
fn test_strings_only_occur_when_nested() {
let input = r#"