"#, vec!["class", "px-6"]),
(
r#"view! {
}"#,
vec!["class", "px-6", "view!"],
diff --git a/crates/oxide/src/extractor/pre_processors/clojure.rs b/crates/oxide/src/extractor/pre_processors/clojure.rs
index c6a458139a9b..c53afd552b19 100644
--- a/crates/oxide/src/extractor/pre_processors/clojure.rs
+++ b/crates/oxide/src/extractor/pre_processors/clojure.rs
@@ -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 {
diff --git a/crates/oxide/src/extractor/pre_processors/elixir.rs b/crates/oxide/src/extractor/pre_processors/elixir.rs
new file mode 100644
index 000000000000..87b89a2a9dda
--- /dev/null
+++ b/crates/oxide/src/extractor/pre_processors/elixir.rs
@@ -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 {
+ 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",
+ ],
+ );
+ }
+}
diff --git a/crates/oxide/src/extractor/pre_processors/mod.rs b/crates/oxide/src/extractor/pre_processors/mod.rs
index 4609f10b9359..7435c349c4d7 100644
--- a/crates/oxide/src/extractor/pre_processors/mod.rs
+++ b/crates/oxide/src/extractor/pre_processors/mod.rs
@@ -1,4 +1,5 @@
pub mod clojure;
+pub mod elixir;
pub mod haml;
pub mod json;
pub mod pre_processor;
@@ -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::*;
diff --git a/crates/oxide/src/scanner/mod.rs b/crates/oxide/src/scanner/mod.rs
index 0bd2b6574b3c..0d2fceba60bb 100644
--- a/crates/oxide/src/scanner/mod.rs
+++ b/crates/oxide/src/scanner/mod.rs
@@ -482,6 +482,7 @@ pub fn pre_process_input(content: &[u8], extension: &str) -> Vec {
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),