Skip to content
Merged
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 @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- 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))
- Detect classes inside Elixir charlist, word list, and string sigils ([#18432](https://github.com/tailwindlabs/tailwindcss/pull/18432))

## [4.1.11] - 2025-06-26

Expand Down
5 changes: 1 addition & 4 deletions crates/oxide/src/extractor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1058,10 +1058,7 @@ mod tests {
#[test]
fn test_leptos_rs_view_class_colon_syntax() {
for (input, expected) in [
(
r#"<div class:px-6=true>"#,
vec!["class", "px-6"],
),
(r#"<div class:px-6=true>"#, vec!["class", "px-6"]),
(
r#"view! { <div class:px-6=true> }"#,
vec!["class", "px-6", "view!"],
Expand Down
4 changes: 2 additions & 2 deletions crates/oxide/src/extractor/pre_processors/clojure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ pub struct Clojure;
/// can simplify this list quite a bit.
#[inline]
fn is_keyword_character(byte: u8) -> bool {
return matches!(
(matches!(
byte,
b'!' | b'#' | b'%' | b'*' | b'+' | b'-' | b'.' | b'/' | b':' | b'_'
) | byte.is_ascii_alphanumeric();
) | byte.is_ascii_alphanumeric())
}

impl PreProcessor for Clojure {
Expand Down
154 changes: 154 additions & 0 deletions crates/oxide/src/extractor/pre_processors/elixir.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
use crate::cursor;
use crate::extractor::bracket_stack::BracketStack;
use crate::extractor::pre_processors::pre_processor::PreProcessor;

#[derive(Debug, Default)]
pub struct Elixir;

impl PreProcessor for Elixir {
fn process(&self, content: &[u8]) -> Vec<u8> {
let mut cursor = cursor::Cursor::new(content);
let mut result = content.to_vec();
let mut bracket_stack = BracketStack::default();

while cursor.pos < content.len() {
// Look for a sigil marker
if cursor.curr != b'~' {
cursor.advance();
continue;
}

// Scan charlists, strings, and wordlists
if !matches!(cursor.next, b'c' | b'C' | b's' | b'S' | b'w' | b'W') {
cursor.advance();
continue;
}

cursor.advance_twice();

// Match the opening for a sigil
if !matches!(cursor.curr, b'(' | b'[' | b'{') {
continue;
}

// Replace the opening bracket with a space
result[cursor.pos] = b' ';

// Scan until we find a balanced closing one and replace it too
bracket_stack.push(cursor.curr);

while cursor.pos < content.len() {
cursor.advance();

match cursor.curr {
// Escaped character, skip ahead to the next character
b'\\' => cursor.advance_twice(),
b'(' | b'[' | b'{' => {
bracket_stack.push(cursor.curr);
}
b')' | b']' | b'}' if !bracket_stack.is_empty() => {
bracket_stack.pop(cursor.curr);

if bracket_stack.is_empty() {
// Replace the closing bracket with a space
result[cursor.pos] = b' ';
break;
}
}
_ => {}
}
}
}

result
}
}

#[cfg(test)]
mod tests {
use super::Elixir;
use crate::extractor::pre_processors::pre_processor::PreProcessor;

#[test]
fn test_elixir_pre_processor() {
for (input, expected) in [
// Simple sigils
("~W(flex underline)", "~W flex underline "),
("~W[flex underline]", "~W flex underline "),
("~W{flex underline}", "~W flex underline "),
// Sigils with nested brackets
(
"~W(text-(--my-color) bg-(--my-color))",
"~W text-(--my-color) bg-(--my-color) ",
),
("~W[text-[red] bg-[red]]", "~W text-[red] bg-[red] "),
// Word sigils with modifiers
("~W(flex underline)a", "~W flex underline a"),
("~W(flex underline)c", "~W flex underline c"),
("~W(flex underline)s", "~W flex underline s"),
// Other sigil types
("~w(flex underline)", "~w flex underline "),
("~c(flex underline)", "~c flex underline "),
("~C(flex underline)", "~C flex underline "),
("~s(flex underline)", "~s flex underline "),
("~S(flex underline)", "~S flex underline "),
] {
Elixir::test(input, expected);
}
}

#[test]
fn test_extract_candidates() {
let input = r#"
~W(c1 c2)
~W[c3 c4]
~W{c5 c6}
~W(text-(--c7) bg-(--c8))
~W[text-[c9] bg-[c10]]
~W(c13 c14)a
~W(c15 c16)c
~W(c17 c18)s
~w(c19 c20)
~c(c21 c22)
~C(c23 c24)
~s(c25 c26)
~S(c27 c28)
~W"c29 c30"
~W'c31 c32'
"#;

Elixir::test_extract_contains(
input,
vec![
"c1",
"c2",
"c3",
"c4",
"c5",
"c6",
"text-(--c7)",
"bg-(--c8)",
"c13",
"c14",
"c15",
"c16",
"c17",
"c18",
"c19",
"c20",
"c21",
"c22",
"c23",
"c24",
"c25",
"c26",
"c27",
"c28",
"c29",
"c30",
"c31",
"c32",
],
);
}
}
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,4 +1,5 @@
pub mod clojure;
pub mod elixir;
pub mod haml;
pub mod json;
pub mod pre_processor;
Expand All @@ -10,6 +11,7 @@ pub mod svelte;
pub mod vue;

pub use clojure::*;
pub use elixir::*;
pub use haml::*;
pub use json::*;
pub use pre_processor::*;
Expand Down
1 change: 1 addition & 0 deletions crates/oxide/src/scanner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,7 @@ pub fn pre_process_input(content: &[u8], extension: &str) -> Vec<u8> {

match extension {
"clj" | "cljs" | "cljc" => Clojure.process(content),
"heex" | "eex" | "ex" | "exs" => Elixir.process(content),
"cshtml" | "razor" => Razor.process(content),
"haml" => Haml.process(content),
"json" => Json.process(content),
Expand Down