From f369e22172dcda93ee526696aacc3e974918db47 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 19 Mar 2025 14:50:30 +0100 Subject: [PATCH] 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
``` 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
\(--my-color\)
``` # Test plan 1. Added test for the issue 2. Existing tests pass 3. Verified via the extractor tool: | Before | After | | --- | --- | | image | image | --- Fixes: #17277 --- CHANGELOG.md | 1 + .../src/extractor/pre_processors/slim.rs | 55 ++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27ccdd000..2451d937b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/crates/oxide/src/extractor/pre_processors/slim.rs b/crates/oxide/src/extractor/pre_processors/slim.rs index 6b7008863..78e1cb0f5 100644 --- a/crates/oxide/src/extractor/pre_processors/slim.rs +++ b/crates/oxide/src/extractor/pre_processors/slim.rs @@ -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#"