Skip to content

Commit 221855b

Browse files
authored
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" />
1 parent 74ccde4 commit 221855b

File tree

5 files changed

+169
-7
lines changed

5 files changed

+169
-7
lines changed

Diff for: CHANGELOG.md

+1
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

2323
- Do not extract candidates with JS string interpolation `${` ([#17142](https://github.com/tailwindlabs/tailwindcss/pull/17142))
2424
- Fix extraction of variants containing `.` character ([#17153](https://github.com/tailwindlabs/tailwindcss/pull/17153))
25+
- Fix extracting candidates in Clojure/ClojureScript ([#17087](https://github.com/tailwindlabs/tailwindcss/pull/17087))
2526

2627
## [4.0.13] - 2025-03-11
2728

Diff for: crates/oxide/src/extractor/mod.rs

+6-7
Original file line numberDiff line numberDiff line change
@@ -355,12 +355,6 @@ mod tests {
355355
r#"[:is(italic):is(underline)]:flex"#,
356356
vec!["[:is(italic):is(underline)]:flex"],
357357
),
358-
// Clojure syntax. See: https://github.com/tailwindlabs/tailwindcss/issues/16189#issuecomment-2642438176
359-
(r#"[:div {:class ["p-2"]}"#, vec!["p-2"]),
360-
(
361-
r#"[:div {:class ["p-2" "text-green"]}"#,
362-
vec!["p-2", "text-green"],
363-
),
364358
(r#"[:div {:class ["p-2""#, vec!["p-2"]),
365359
(r#" "text-green"]}"#, vec!["text-green"]),
366360
(r#"[:div.p-2]"#, vec!["p-2"]),
@@ -668,8 +662,13 @@ mod tests {
668662
(r#"[:div {:class ["p-2""#, vec!["p-2"]),
669663
(r#" "text-green"]}"#, vec!["text-green"]),
670664
(r#"[:div.p-2]"#, vec!["p-2"]),
665+
(r#"[:div {:class ["p-2"]}"#, vec!["p-2"]),
666+
(
667+
r#"[:div {:class ["p-2" "text-green"]}"#,
668+
vec!["p-2", "text-green"],
669+
),
671670
] {
672-
assert_extract_sorted_candidates(input, expected);
671+
assert_extract_candidates_contains(&pre_process_input(input, "cljs"), expected);
673672
}
674673
}
675674

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

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
use crate::cursor;
2+
use crate::extractor::pre_processors::pre_processor::PreProcessor;
3+
use bstr::ByteSlice;
4+
5+
#[derive(Debug, Default)]
6+
pub struct Clojure;
7+
8+
impl PreProcessor for Clojure {
9+
fn process(&self, content: &[u8]) -> Vec<u8> {
10+
let content = content
11+
.replace(":class", " ")
12+
.replace(":className", " ");
13+
let len = content.len();
14+
let mut result = content.to_vec();
15+
let mut cursor = cursor::Cursor::new(&content);
16+
17+
while cursor.pos < len {
18+
match cursor.curr {
19+
// Consume strings as-is
20+
b'"' => {
21+
cursor.advance();
22+
23+
while cursor.pos < len {
24+
match cursor.curr {
25+
// Escaped character, skip ahead to the next character
26+
b'\\' => cursor.advance_twice(),
27+
28+
// End of the string
29+
b'"' => break,
30+
31+
// Everything else is valid
32+
_ => cursor.advance(),
33+
};
34+
}
35+
}
36+
37+
// Consume comments as-is until the end of the line.
38+
// Comments start with `;;`
39+
b';' if matches!(cursor.next, b';') => {
40+
while cursor.pos < len && cursor.curr != b'\n' {
41+
cursor.advance();
42+
}
43+
}
44+
45+
b':' | b'.' => {
46+
result[cursor.pos] = b' ';
47+
}
48+
49+
// Consume everything else
50+
_ => {}
51+
};
52+
53+
cursor.advance();
54+
}
55+
56+
result
57+
}
58+
}
59+
60+
#[cfg(test)]
61+
mod tests {
62+
use super::Clojure;
63+
use crate::extractor::pre_processors::pre_processor::PreProcessor;
64+
65+
#[test]
66+
fn test_clojure_pre_processor() {
67+
for (input, expected) in [
68+
(":div.flex-1.flex-2", " div flex-1 flex-2"),
69+
(
70+
":.flex-3.flex-4 ;defaults to div",
71+
" flex-3 flex-4 ;defaults to div",
72+
),
73+
("{:class :flex-5.flex-6", "{ flex-5 flex-6"),
74+
(r#"{:class "flex-7 flex-8"}"#, r#"{ "flex-7 flex-8"}"#),
75+
(
76+
r#"{:class ["flex-9" :flex-10]}"#,
77+
r#"{ ["flex-9" flex-10]}"#,
78+
),
79+
(
80+
r#"(dom/div {:class "flex-11 flex-12"})"#,
81+
r#"(dom/div { "flex-11 flex-12"})"#,
82+
),
83+
("(dom/div :.flex-13.flex-14", "(dom/div flex-13 flex-14"),
84+
] {
85+
Clojure::test(input, expected);
86+
}
87+
}
88+
89+
#[test]
90+
fn test_extract_candidates() {
91+
// https://github.com/luckasRanarison/tailwind-tools.nvim/issues/68#issuecomment-2660951258
92+
let input = r#"
93+
:div.c1.c2
94+
:.c3.c4 ;defaults to div
95+
{:class :c5.c6
96+
{:class "c7 c8"}
97+
{:class ["c9" :c10]}
98+
(dom/div {:class "c11 c12"})
99+
(dom/div :.c13.c14
100+
{:className :c15.c16
101+
{:className "c17 c18"}
102+
{:className ["c19" :c20]}
103+
(dom/div {:className "c21 c22"})
104+
"#;
105+
106+
Clojure::test_extract_contains(
107+
input,
108+
vec![
109+
"c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "c10", "c11", "c12", "c13",
110+
"c14", "c15", "c16", "c17", "c18", "c19", "c20", "c21", "c22",
111+
],
112+
);
113+
114+
// Similar structure but using real classes
115+
let input = r#"
116+
:div.flex-1.flex-2
117+
:.flex-3.flex-4 ;defaults to div
118+
{:class :flex-5.flex-6
119+
{:class "flex-7 flex-8"}
120+
{:class ["flex-9" :flex-10]}
121+
(dom/div {:class "flex-11 flex-12"})
122+
(dom/div :.flex-13.flex-14
123+
{:className :flex-15.flex-16
124+
{:className "flex-17 flex-18"}
125+
{:className ["flex-19" :flex-20]}
126+
(dom/div {:className "flex-21 flex-22"})
127+
"#;
128+
129+
Clojure::test_extract_contains(
130+
input,
131+
vec![
132+
"flex-1", "flex-2", "flex-3", "flex-4", "flex-5", "flex-6", "flex-7", "flex-8",
133+
"flex-9", "flex-10", "flex-11", "flex-12", "flex-13", "flex-14", "flex-15",
134+
"flex-16", "flex-17", "flex-18", "flex-19", "flex-20", "flex-21", "flex-22",
135+
],
136+
);
137+
}
138+
139+
#[test]
140+
fn test_special_characters_are_valid_in_strings() {
141+
// In this case the `:` and `.` should not be replaced by ` ` because they are inside a
142+
// string.
143+
let input = r#"
144+
(dom/div {:class "hover:flex px-1.5"})
145+
"#;
146+
147+
Clojure::test_extract_contains(input, vec!["hover:flex", "px-1.5"]);
148+
}
149+
150+
#[test]
151+
fn test_ignore_comments_with_invalid_strings() {
152+
let input = r#"
153+
;; This is an unclosed string: "
154+
(dom/div {:class "hover:flex px-1.5"})
155+
"#;
156+
157+
Clojure::test_extract_contains(input, vec!["hover:flex", "px-1.5"]);
158+
}
159+
}

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

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub mod clojure;
12
pub mod haml;
23
pub mod json;
34
pub mod pre_processor;
@@ -7,6 +8,7 @@ pub mod ruby;
78
pub mod slim;
89
pub mod svelte;
910

11+
pub use clojure::*;
1012
pub use haml::*;
1113
pub use json::*;
1214
pub use pre_processor::*;

Diff for: crates/oxide/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,7 @@ pub fn pre_process_input(content: &[u8], extension: &str) -> Vec<u8> {
468468
use crate::extractor::pre_processors::*;
469469

470470
match extension {
471+
"clj" | "cljs" | "cljc" => Clojure.process(content),
471472
"cshtml" | "razor" => Razor.process(content),
472473
"haml" => Haml.process(content),
473474
"json" => Json.process(content),

0 commit comments

Comments
 (0)