Skip to content

Commit b7cc74d

Browse files
committed
Fix trailing ) from interfering with extraction in keywords
In order to pre-empt any further problems from Clojure keywords, this commit inverts the previous logic: Instead of consuming everything and handling special cases, consume characters as defined by the Clojure keyword specification, and nothing else. In addition, leave string consumption as is, but—again—drop the `"`s, in order to align with the strategy of throwing away everything that is not the content of a string or a keyword, including delimiters (`"`, `:` respectively).
1 parent 2030e94 commit b7cc74d

File tree

2 files changed

+103
-32
lines changed

2 files changed

+103
-32
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111

1212
- Don't consider the global important state in `@apply` ([#18404](https://github.com/tailwindlabs/tailwindcss/pull/18404))
13+
- Fix trailing `)` from interfering with extraction in Clojure keywords ([#18345](https://github.com/tailwindlabs/tailwindcss/pull/18345))
1314

1415
## [4.1.11] - 2025-06-26
1516

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

Lines changed: 102 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ use bstr::ByteSlice;
55
#[derive(Debug, Default)]
66
pub struct Clojure;
77

8+
#[inline]
9+
fn is_keyword_character(byte: u8) -> bool {
10+
matches!(
11+
byte,
12+
b'+' | b'-' | b'/' | b'*' | b'_' | b'#' | b'.' | b':' | b'?'
13+
) | byte.is_ascii_alphanumeric()
14+
}
15+
816
impl PreProcessor for Clojure {
917
fn process(&self, content: &[u8]) -> Vec<u8> {
1018
let content = content
@@ -18,6 +26,7 @@ impl PreProcessor for Clojure {
1826
match cursor.curr {
1927
// Consume strings as-is
2028
b'"' => {
29+
result[cursor.pos] = b' ';
2130
cursor.advance();
2231

2332
while cursor.pos < len {
@@ -26,52 +35,82 @@ impl PreProcessor for Clojure {
2635
b'\\' => cursor.advance_twice(),
2736

2837
// End of the string
29-
b'"' => break,
38+
b'"' => {
39+
result[cursor.pos] = b' ';
40+
break;
41+
}
3042

3143
// Everything else is valid
3244
_ => cursor.advance(),
3345
};
3446
}
3547
}
3648

37-
// Consume comments as-is until the end of the line.
49+
// Discard line comments until the end of the line.
3850
// Comments start with `;;`
3951
b';' if matches!(cursor.next, b';') => {
4052
while cursor.pos < len && cursor.curr != b'\n' {
53+
result[cursor.pos] = b' ';
4154
cursor.advance();
4255
}
4356
}
4457

45-
// A `.` surrounded by digits is a decimal number, so we don't want to replace it.
46-
//
47-
// E.g.:
48-
// ```
49-
// gap-1.5
50-
// ^
51-
// ``
52-
b'.' if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() => {
58+
// Consume keyword until a terminating character is reached.
59+
b':' => {
60+
result[cursor.pos] = b' ';
61+
cursor.advance();
5362

54-
// Keep the `.` as-is
55-
}
63+
while cursor.pos < len {
64+
match cursor.curr {
65+
// A `.` surrounded by digits is a decimal number, so we don't want to replace it.
66+
//
67+
// E.g.:
68+
// ```
69+
// gap-1.5
70+
// ^
71+
// ```
72+
b'.' if cursor.prev.is_ascii_digit()
73+
&& cursor.next.is_ascii_digit() =>
74+
{
75+
// Keep the `.` as-is
76+
}
77+
// A `.` not surrounded by digits denotes the start of a new class name in a
78+
// dot-delimited keyword.
79+
//
80+
// E.g.:
81+
// ```
82+
// flex.gap-1.5
83+
// ^
84+
// ```
85+
b'.' => {
86+
result[cursor.pos] = b' ';
87+
}
88+
// End of keyword.
89+
_ if !is_keyword_character(cursor.curr) => {
90+
result[cursor.pos] = b' ';
91+
break;
92+
}
5693

57-
// A `:` surrounded by letters denotes a variant. Keep as is.
58-
//
59-
// E.g.:
60-
// ```
61-
// lg:pr-6"
62-
// ^
63-
// ``
64-
b':' if cursor.prev.is_ascii_alphanumeric() && cursor.next.is_ascii_alphanumeric() => {
94+
// Consume everything else.
95+
_ => {}
96+
};
6597

66-
// Keep the `:` as-is
98+
cursor.advance();
99+
}
67100
}
68101

69-
b':' | b'.' => {
102+
// Aggressively discard everything else, reducing false positives and preventing
103+
// characters surrounding keywords from producing false negatives.
104+
// E.g.:
105+
// ```
106+
// (when condition :bg-white)
107+
// ^
108+
// ```
109+
// A ')' is never a valid part of a keyword, but will nonetheless prevent 'bg-white'
110+
// from being extracted if not discarded.
111+
_ => {
70112
result[cursor.pos] = b' ';
71113
}
72-
73-
// Consume everything else
74-
_ => {}
75114
};
76115

77116
cursor.advance();
@@ -92,19 +131,23 @@ mod tests {
92131
(":div.flex-1.flex-2", " div flex-1 flex-2"),
93132
(
94133
":.flex-3.flex-4 ;defaults to div",
95-
" flex-3 flex-4 ;defaults to div",
134+
" flex-3 flex-4 ",
96135
),
97-
("{:class :flex-5.flex-6", "{ flex-5 flex-6"),
98-
(r#"{:class "flex-7 flex-8"}"#, r#"{ "flex-7 flex-8"}"#),
136+
("{:class :flex-5.flex-6", " flex-5 flex-6"),
137+
(r#"{:class "flex-7 flex-8"}"#, r#" flex-7 flex-8 "#),
99138
(
100139
r#"{:class ["flex-9" :flex-10]}"#,
101-
r#"{ ["flex-9" flex-10]}"#,
140+
r#" flex-9 flex-10 "#,
102141
),
103142
(
104143
r#"(dom/div {:class "flex-11 flex-12"})"#,
105-
r#"(dom/div { "flex-11 flex-12"})"#,
144+
r#" flex-11 flex-12 "#,
145+
),
146+
("(dom/div :.flex-13.flex-14", " flex-13 flex-14"),
147+
(
148+
r#"[:div#hello.bg-white.pr-1.5 {:class ["grid grid-cols-[auto,1fr] grid-rows-2"]}]"#,
149+
r#" div#hello bg-white pr-1.5 grid grid-cols-[auto,1fr] grid-rows-2 "#,
106150
),
107-
("(dom/div :.flex-13.flex-14", "(dom/div flex-13 flex-14"),
108151
] {
109152
Clojure::test(input, expected);
110153
}
@@ -198,8 +241,35 @@ mod tests {
198241
($ :div {:class [:flex :first:lg:pr-6 :first:2xl:pl-6 :group-hover/2:2xs:pt-6]} …)
199242
200243
:.hover:bg-white
244+
245+
[:div#hello.bg-white.pr-1.5]
246+
"#;
247+
248+
Clojure::test_extract_contains(
249+
input,
250+
vec![
251+
"flex",
252+
"first:lg:pr-6",
253+
"first:2xl:pl-6",
254+
"group-hover/2:2xs:pt-6",
255+
"hover:bg-white",
256+
"bg-white",
257+
"pr-1.5",
258+
],
259+
);
260+
}
261+
262+
// https://github.com/tailwindlabs/tailwindcss/issues/18344
263+
#[test]
264+
fn test_noninterference_of_parens_on_keywords() {
265+
let input = r#"
266+
(get props :y-padding :py-5)
267+
($ :div {:class [:flex.pr-1.5 (if condition :bg-white :bg-black)]})
201268
"#;
202269

203-
Clojure::test_extract_contains(input, vec!["flex", "first:lg:pr-6", "first:2xl:pl-6", "group-hover/2:2xs:pt-6", "hover:bg-white"]);
270+
Clojure::test_extract_contains(
271+
input,
272+
vec!["py-5", "flex", "pr-1.5", "bg-white", "bg-black"],
273+
);
204274
}
205275
}

0 commit comments

Comments
 (0)