Skip to content

Commit 274be93

Browse files
authored
Handle ' syntax in ClojureScript when extracting classes (#18888)
This PR fixes an issue where the `'` syntax in ClojureScript was not handled properly, resulting in missing extracted classes. This PR now supports the following ClojureScript syntaxes: ```cljs ; Keyword (print 'text-red-500) ; List (print '(flex flex-col underline)) ; Vector (print '[flex flex-col underline]) ``` ### Test plan 1. Added regression tests 2. Verified that we extract classes correctly now in various scenarios: Top is before, bottom is with this PR: <img width="1335" height="1862" alt="image" src="https://github.com/user-attachments/assets/746aa073-25f8-41f8-b71c-ba83a33065aa" /> Fixes: #18882
1 parent 1334c99 commit 274be93

File tree

2 files changed

+155
-1
lines changed

2 files changed

+155
-1
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
- Nothing yet!
10+
### Fixed
11+
12+
- Handle `'` syntax in ClojureScript when extracting classes ([#18888](https://github.com/tailwindlabs/tailwindcss/pull/18888))
1113

1214
## [4.1.13] - 2025-09-03
1315

crates/oxide/src/extractor/pre_processors/clojure.rs

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,75 @@ impl PreProcessor for Clojure {
108108
}
109109
}
110110

111+
// Handle quote with a list, e.g.: `'(…)`
112+
// and with a vector, e.g.: `'[…]`
113+
b'\'' if matches!(cursor.next, b'[' | b'(') => {
114+
result[cursor.pos] = b' ';
115+
cursor.advance();
116+
result[cursor.pos] = b' ';
117+
let end = match cursor.curr {
118+
b'[' => b']',
119+
b'(' => b')',
120+
_ => unreachable!(),
121+
};
122+
123+
// Consume until the closing `]`
124+
while cursor.pos < len {
125+
match cursor.curr {
126+
x if x == end => {
127+
result[cursor.pos] = b' ';
128+
break;
129+
}
130+
131+
// Consume strings as-is
132+
b'"' => {
133+
result[cursor.pos] = b' ';
134+
cursor.advance();
135+
136+
while cursor.pos < len {
137+
match cursor.curr {
138+
// Escaped character, skip ahead to the next character
139+
b'\\' => cursor.advance_twice(),
140+
141+
// End of the string
142+
b'"' => {
143+
result[cursor.pos] = b' ';
144+
break;
145+
}
146+
147+
// Everything else is valid
148+
_ => cursor.advance(),
149+
};
150+
}
151+
}
152+
_ => {}
153+
};
154+
155+
cursor.advance();
156+
}
157+
}
158+
159+
// Handle quote with a keyword, e.g.: `'bg-white`
160+
b'\'' if !cursor.next.is_ascii_whitespace() => {
161+
result[cursor.pos] = b' ';
162+
cursor.advance();
163+
164+
while cursor.pos < len {
165+
match cursor.curr {
166+
// End of keyword.
167+
_ if !is_keyword_character(cursor.curr) => {
168+
result[cursor.pos] = b' ';
169+
break;
170+
}
171+
172+
// Consume everything else.
173+
_ => {}
174+
};
175+
176+
cursor.advance();
177+
}
178+
}
179+
111180
// Aggressively discard everything else, reducing false positives and preventing
112181
// characters surrounding keywords from producing false negatives.
113182
// E.g.:
@@ -281,4 +350,87 @@ mod tests {
281350
vec!["py-5", "flex", "pr-1.5", "bg-white", "bg-black"],
282351
);
283352
}
353+
354+
// https://github.com/tailwindlabs/tailwindcss/issues/18882
355+
#[test]
356+
fn test_extract_from_symbol_list() {
357+
let input = r#"
358+
[:div {:class '[z-1 z-2
359+
z-3 z-4]}]
360+
"#;
361+
Clojure::test_extract_contains(input, vec!["z-1", "z-2", "z-3", "z-4"]);
362+
363+
// https://github.com/tailwindlabs/tailwindcss/pull/18345#issuecomment-3253403847
364+
let input = r#"
365+
(def hl-class-names '[ring ring-blue-500])
366+
367+
[:div
368+
{:class (cond-> '[input w-full]
369+
textarea? (conj 'textarea)
370+
(seq errors) (concat '[border-red-500 bg-red-100])
371+
highlight? (concat hl-class-names))}]
372+
"#;
373+
Clojure::test_extract_contains(
374+
input,
375+
vec![
376+
"ring",
377+
"ring-blue-500",
378+
"input",
379+
"w-full",
380+
"textarea",
381+
"border-red-500",
382+
"bg-red-100",
383+
],
384+
);
385+
386+
let input = r#"
387+
[:div
388+
{:class '[h-100 lg:h-200 max-w-32 mx-auto py-60
389+
flex flex-col justify-end items-center
390+
lg:flex-row lg:justify-between
391+
bg-cover bg-center bg-no-repeat rounded-3xl overflow-hidden
392+
font-semibold text-gray-900]}]
393+
"#;
394+
Clojure::test_extract_contains(
395+
input,
396+
vec![
397+
"h-100",
398+
"lg:h-200",
399+
"max-w-32",
400+
"mx-auto",
401+
"py-60",
402+
"flex",
403+
"flex-col",
404+
"justify-end",
405+
"items-center",
406+
"lg:flex-row",
407+
"lg:justify-between",
408+
"bg-cover",
409+
"bg-center",
410+
"bg-no-repeat",
411+
"rounded-3xl",
412+
"overflow-hidden",
413+
"font-semibold",
414+
"text-gray-900",
415+
],
416+
);
417+
418+
// `/` is invalid and requires explicit quoting
419+
let input = r#"
420+
'[p-32 "text-black/50"]
421+
"#;
422+
Clojure::test_extract_contains(input, vec!["p-32", "text-black/50"]);
423+
424+
// `[…]` is invalid and requires explicit quoting
425+
let input = r#"
426+
(print '[ring ring-blue-500 "bg-[#0088cc]"])
427+
"#;
428+
Clojure::test_extract_contains(input, vec!["ring", "ring-blue-500", "bg-[#0088cc]"]);
429+
430+
// `'(…)` looks similar to `[…]` but uses parentheses instead of brackets
431+
let input = r#"
432+
(print '(ring ring-blue-500 "bg-[#0088cc]"))
433+
"#;
434+
Clojure::test_extract_contains(input, vec!["ring", "ring-blue-500", "bg-[#0088cc]"]);
435+
}
284436
}

0 commit comments

Comments
 (0)