Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Mar 13, 2025
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 6 additions & 7 deletions crates/oxide/src/extractor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
Expand Down Expand Up @@ -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);
}
}

Expand Down
159 changes: 159 additions & 0 deletions crates/oxide/src/extractor/pre_processors/clojure.rs
Original file line number Diff line number Diff line change
@@ -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"]);
}
}
2 changes: 2 additions & 0 deletions crates/oxide/src/extractor/pre_processors/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod clojure;
pub mod haml;
pub mod json;
pub mod pre_processor;
Expand All @@ -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::*;
Expand Down
1 change: 1 addition & 0 deletions crates/oxide/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down