Ensure candidate extraction works as expected in Clojure/ClojureScript (#17087)

This PR adds a Clojure/ClojureScript pre processor to make sure that
candidate extraction works as expected.


| Before | After |
| --- | --- |
| <img width="908" alt="image"
src="https://github.com/user-attachments/assets/98aba8b6-0c44-47c6-b87c-ecf955e5e007"
/> | <img width="908" alt="image"
src="https://github.com/user-attachments/assets/7a5ec3eb-1630-4b60-80bd-c07bc2381d3b"
/> |

You can see that the classes preceded by `:` are not properly extracted
in the before case, but they are in the after case. We do extract a few
more cases now like `:class` and `:className` itself, but at least we
also retrieve all the `flex-*` classes.

We could also always ignore `:class` and `:className` literals:
<img width="908" alt="image"
src="https://github.com/user-attachments/assets/f5a67cae-25d6-4811-b777-f72fdb5ef450"
/>
This commit is contained in:
Robin Malfait 2025-03-13 11:31:27 +01:00 committed by GitHub
parent 74ccde4672
commit 221855b195
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 169 additions and 7 deletions

View File

@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Do not extract candidates with JS string interpolation `${` ([#17142](https://github.com/tailwindlabs/tailwindcss/pull/17142))
- Fix extraction of variants containing `.` character ([#17153](https://github.com/tailwindlabs/tailwindcss/pull/17153))
- Fix extracting candidates in Clojure/ClojureScript ([#17087](https://github.com/tailwindlabs/tailwindcss/pull/17087))
## [4.0.13] - 2025-03-11

View File

@ -355,12 +355,6 @@ mod tests {
r#"[:is(italic):is(underline)]:flex"#,
vec!["[:is(italic):is(underline)]:flex"],
),
// Clojure syntax. See: https://github.com/tailwindlabs/tailwindcss/issues/16189#issuecomment-2642438176
(r#"[:div {:class ["p-2"]}"#, vec!["p-2"]),
(
r#"[:div {:class ["p-2" "text-green"]}"#,
vec!["p-2", "text-green"],
),
(r#"[:div {:class ["p-2""#, vec!["p-2"]),
(r#" "text-green"]}"#, vec!["text-green"]),
(r#"[:div.p-2]"#, vec!["p-2"]),
@ -668,8 +662,13 @@ mod tests {
(r#"[:div {:class ["p-2""#, vec!["p-2"]),
(r#" "text-green"]}"#, vec!["text-green"]),
(r#"[:div.p-2]"#, vec!["p-2"]),
(r#"[:div {:class ["p-2"]}"#, vec!["p-2"]),
(
r#"[:div {:class ["p-2" "text-green"]}"#,
vec!["p-2", "text-green"],
),
] {
assert_extract_sorted_candidates(input, expected);
assert_extract_candidates_contains(&pre_process_input(input, "cljs"), expected);
}
}

View File

@ -0,0 +1,159 @@
use crate::cursor;
use crate::extractor::pre_processors::pre_processor::PreProcessor;
use bstr::ByteSlice;
#[derive(Debug, Default)]
pub struct Clojure;
impl PreProcessor for Clojure {
fn process(&self, content: &[u8]) -> Vec<u8> {
let content = content
.replace(":class", " ")
.replace(":className", " ");
let len = content.len();
let mut result = content.to_vec();
let mut cursor = cursor::Cursor::new(&content);
while cursor.pos < len {
match cursor.curr {
// Consume strings as-is
b'"' => {
cursor.advance();
while cursor.pos < len {
match cursor.curr {
// Escaped character, skip ahead to the next character
b'\\' => cursor.advance_twice(),
// End of the string
b'"' => break,
// Everything else is valid
_ => cursor.advance(),
};
}
}
// Consume comments as-is until the end of the line.
// Comments start with `;;`
b';' if matches!(cursor.next, b';') => {
while cursor.pos < len && cursor.curr != b'\n' {
cursor.advance();
}
}
b':' | b'.' => {
result[cursor.pos] = b' ';
}
// Consume everything else
_ => {}
};
cursor.advance();
}
result
}
}
#[cfg(test)]
mod tests {
use super::Clojure;
use crate::extractor::pre_processors::pre_processor::PreProcessor;
#[test]
fn test_clojure_pre_processor() {
for (input, expected) in [
(":div.flex-1.flex-2", " div flex-1 flex-2"),
(
":.flex-3.flex-4 ;defaults to div",
" flex-3 flex-4 ;defaults to div",
),
("{:class :flex-5.flex-6", "{ flex-5 flex-6"),
(r#"{:class "flex-7 flex-8"}"#, r#"{ "flex-7 flex-8"}"#),
(
r#"{:class ["flex-9" :flex-10]}"#,
r#"{ ["flex-9" flex-10]}"#,
),
(
r#"(dom/div {:class "flex-11 flex-12"})"#,
r#"(dom/div { "flex-11 flex-12"})"#,
),
("(dom/div :.flex-13.flex-14", "(dom/div flex-13 flex-14"),
] {
Clojure::test(input, expected);
}
}
#[test]
fn test_extract_candidates() {
// https://github.com/luckasRanarison/tailwind-tools.nvim/issues/68#issuecomment-2660951258
let input = r#"
:div.c1.c2
:.c3.c4 ;defaults to div
{:class :c5.c6
{:class "c7 c8"}
{:class ["c9" :c10]}
(dom/div {:class "c11 c12"})
(dom/div :.c13.c14
{:className :c15.c16
{:className "c17 c18"}
{:className ["c19" :c20]}
(dom/div {:className "c21 c22"})
"#;
Clojure::test_extract_contains(
input,
vec![
"c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "c10", "c11", "c12", "c13",
"c14", "c15", "c16", "c17", "c18", "c19", "c20", "c21", "c22",
],
);
// Similar structure but using real classes
let input = r#"
:div.flex-1.flex-2
:.flex-3.flex-4 ;defaults to div
{:class :flex-5.flex-6
{:class "flex-7 flex-8"}
{:class ["flex-9" :flex-10]}
(dom/div {:class "flex-11 flex-12"})
(dom/div :.flex-13.flex-14
{:className :flex-15.flex-16
{:className "flex-17 flex-18"}
{:className ["flex-19" :flex-20]}
(dom/div {:className "flex-21 flex-22"})
"#;
Clojure::test_extract_contains(
input,
vec![
"flex-1", "flex-2", "flex-3", "flex-4", "flex-5", "flex-6", "flex-7", "flex-8",
"flex-9", "flex-10", "flex-11", "flex-12", "flex-13", "flex-14", "flex-15",
"flex-16", "flex-17", "flex-18", "flex-19", "flex-20", "flex-21", "flex-22",
],
);
}
#[test]
fn test_special_characters_are_valid_in_strings() {
// In this case the `:` and `.` should not be replaced by ` ` because they are inside a
// string.
let input = r#"
(dom/div {:class "hover:flex px-1.5"})
"#;
Clojure::test_extract_contains(input, vec!["hover:flex", "px-1.5"]);
}
#[test]
fn test_ignore_comments_with_invalid_strings() {
let input = r#"
;; This is an unclosed string: "
(dom/div {:class "hover:flex px-1.5"})
"#;
Clojure::test_extract_contains(input, vec!["hover:flex", "px-1.5"]);
}
}

View File

@ -1,3 +1,4 @@
pub mod clojure;
pub mod haml;
pub mod json;
pub mod pre_processor;
@ -7,6 +8,7 @@ pub mod ruby;
pub mod slim;
pub mod svelte;
pub use clojure::*;
pub use haml::*;
pub use json::*;
pub use pre_processor::*;

View File

@ -468,6 +468,7 @@ pub fn pre_process_input(content: &[u8], extension: &str) -> Vec<u8> {
use crate::extractor::pre_processors::*;
match extension {
"clj" | "cljs" | "cljc" => Clojure.process(content),
"cshtml" | "razor" => Razor.process(content),
"haml" => Haml.process(content),
"json" => Json.process(content),