@@ -5,6 +5,14 @@ use bstr::ByteSlice;
55#[ derive( Debug , Default ) ]
66pub 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+
816impl 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