Detect classes inside Elixir charlist, word list, and string sigils (#18432)
Fixes #18431 The `~W(…)` syntax in Elixir is a _sigil_. It's basically another way to write strings, arrays of strings, etc… The syntax lets you use a handful of surrounding brackets like `~W(…)`, `~W[…]`, `~W{…}`, `~W"…"`, etc… to let you write lists without necessarily having to escape characters. In v3 our extractor was able to pick these up but in v4 Oxide does not. I've added a preprocessor for Elixir files so we can modify the code before our main extractor sees it. Now things like this: `~W(text-white bg-gray-600)` will get turned into ` ~W text-white bg-gray-600 ` which can easily be processed by our extractor. The sigils we support are: - `~s` / `~S` (strings) - `~w` / `~W` (word lists) - `~c` / `~C` (charlists) We're specifically detecting the use of `(…)`, `[…]`, and `{…}` as using quotes already works today. --------- Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
parent
3cab801761
commit
8c8033017d
6 changed files with 161 additions and 6 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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!"],
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
154
crates/oxide/src/extractor/pre_processors/elixir.rs
Normal file
154
crates/oxide/src/extractor/pre_processors/elixir.rs
Normal file
|
|
@ -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",
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue