Skip to content

Fix trailing ) from interfering with extraction in Clojure keywords #18345

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

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

## [4.1.11] - 2025-06-26

Expand Down
134 changes: 102 additions & 32 deletions crates/oxide/src/extractor/pre_processors/clojure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ use bstr::ByteSlice;
#[derive(Debug, Default)]
pub struct Clojure;

#[inline]
fn is_keyword_character(byte: u8) -> bool {
matches!(
byte,
b'+' | b'-' | b'/' | b'*' | b'_' | b'#' | b'.' | b':' | b'?'
) | byte.is_ascii_alphanumeric()
}

impl PreProcessor for Clojure {
fn process(&self, content: &[u8]) -> Vec<u8> {
let content = content
Expand All @@ -18,6 +26,7 @@ impl PreProcessor for Clojure {
match cursor.curr {
// Consume strings as-is
b'"' => {
result[cursor.pos] = b' ';
cursor.advance();

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

// End of the string
b'"' => break,
b'"' => {
result[cursor.pos] = b' ';
break;
}

// Everything else is valid
_ => cursor.advance(),
};
}
}

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

// A `.` surrounded by digits is a decimal number, so we don't want to replace it.
//
// E.g.:
// ```
// gap-1.5
// ^
// ``
b'.' if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() => {
// Consume keyword until a terminating character is reached.
b':' => {
result[cursor.pos] = b' ';
cursor.advance();

// Keep the `.` as-is
}
while cursor.pos < len {
match cursor.curr {
// A `.` surrounded by digits is a decimal number, so we don't want to replace it.
//
// E.g.:
// ```
// gap-1.5
// ^
// ```
b'.' if cursor.prev.is_ascii_digit()
&& cursor.next.is_ascii_digit() =>
{
// Keep the `.` as-is
}
// A `.` not surrounded by digits denotes the start of a new class name in a
// dot-delimited keyword.
//
// E.g.:
// ```
// flex.gap-1.5
// ^
// ```
b'.' => {
result[cursor.pos] = b' ';
}
// End of keyword.
_ if !is_keyword_character(cursor.curr) => {
result[cursor.pos] = b' ';
break;
}

// A `:` surrounded by letters denotes a variant. Keep as is.
//
// E.g.:
// ```
// lg:pr-6"
// ^
// ``
b':' if cursor.prev.is_ascii_alphanumeric() && cursor.next.is_ascii_alphanumeric() => {
// Consume everything else.
_ => {}
};

// Keep the `:` as-is
cursor.advance();
}
}

b':' | b'.' => {
// Aggressively discard everything else, reducing false positives and preventing
// characters surrounding keywords from producing false negatives.
// E.g.:
// ```
// (when condition :bg-white)
// ^
// ```
// A ')' is never a valid part of a keyword, but will nonetheless prevent 'bg-white'
// from being extracted if not discarded.
_ => {
result[cursor.pos] = b' ';
}

// Consume everything else
_ => {}
};

cursor.advance();
Expand All @@ -92,19 +131,23 @@ mod tests {
(":div.flex-1.flex-2", " div flex-1 flex-2"),
(
":.flex-3.flex-4 ;defaults to div",
" flex-3 flex-4 ;defaults to div",
" flex-3 flex-4 ",
),
("{:class :flex-5.flex-6", "{ flex-5 flex-6"),
(r#"{:class "flex-7 flex-8"}"#, r#"{ "flex-7 flex-8"}"#),
("{: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#" flex-9 flex-10 "#,
),
(
r#"(dom/div {:class "flex-11 flex-12"})"#,
r#"(dom/div { "flex-11 flex-12"})"#,
r#" flex-11 flex-12 "#,
),
("(dom/div :.flex-13.flex-14", " flex-13 flex-14"),
(
r#"[:div#hello.bg-white.pr-1.5 {:class ["grid grid-cols-[auto,1fr] grid-rows-2"]}]"#,
r#" div#hello bg-white pr-1.5 grid grid-cols-[auto,1fr] grid-rows-2 "#,
),
("(dom/div :.flex-13.flex-14", "(dom/div flex-13 flex-14"),
] {
Clojure::test(input, expected);
}
Expand Down Expand Up @@ -198,8 +241,35 @@ mod tests {
($ :div {:class [:flex :first:lg:pr-6 :first:2xl:pl-6 :group-hover/2:2xs:pt-6]} …)

:.hover:bg-white

[:div#hello.bg-white.pr-1.5]
"#;

Clojure::test_extract_contains(
input,
vec![
"flex",
"first:lg:pr-6",
"first:2xl:pl-6",
"group-hover/2:2xs:pt-6",
"hover:bg-white",
"bg-white",
"pr-1.5",
],
);
}

// https://github.com/tailwindlabs/tailwindcss/issues/18344
#[test]
fn test_noninterference_of_parens_on_keywords() {
let input = r#"
(get props :y-padding :py-5)
($ :div {:class [:flex.pr-1.5 (if condition :bg-white :bg-black)]})
"#;

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