diff --git a/crates/oxide/src/extractor/mod.rs b/crates/oxide/src/extractor/mod.rs
index a37d6a0a49b3..19ee491e77b9 100644
--- a/crates/oxide/src/extractor/mod.rs
+++ b/crates/oxide/src/extractor/mod.rs
@@ -906,6 +906,17 @@ mod tests {
r#"
"#,
vec!["flex", "block"],
);
+
+ // Symfony Live Components loading directives.
+ //
+ // https://github.com/tailwindlabs/tailwindcss/issues/19458
+ assert_extract_candidates_contains(
+ &pre_process_input(
+ r#"
"#,
+ "twig",
+ ),
+ vec!["opacity-50", "pointer-events-none", "hidden"],
+ );
}
// https://github.com/tailwindlabs/tailwindcss/issues/17050
diff --git a/crates/oxide/src/extractor/pre_processors/mod.rs b/crates/oxide/src/extractor/pre_processors/mod.rs
index efcbc53d86d2..1876ec6aaac1 100644
--- a/crates/oxide/src/extractor/pre_processors/mod.rs
+++ b/crates/oxide/src/extractor/pre_processors/mod.rs
@@ -10,6 +10,7 @@ pub mod ruby;
pub mod rust;
pub mod slim;
pub mod svelte;
+pub mod twig;
pub mod vue;
pub use clojure::*;
@@ -24,4 +25,5 @@ pub use ruby::*;
pub use rust::*;
pub use slim::*;
pub use svelte::*;
+pub use twig::*;
pub use vue::*;
diff --git a/crates/oxide/src/extractor/pre_processors/twig.rs b/crates/oxide/src/extractor/pre_processors/twig.rs
new file mode 100644
index 000000000000..7eb4859cad6d
--- /dev/null
+++ b/crates/oxide/src/extractor/pre_processors/twig.rs
@@ -0,0 +1,111 @@
+use crate::extractor::pre_processors::pre_processor::PreProcessor;
+
+#[derive(Debug, Default)]
+pub struct Twig;
+
+impl PreProcessor for Twig {
+ fn process(&self, content: &[u8]) -> Vec
{
+ let mut result = content.to_vec();
+ let mut cursor = 0;
+
+ while cursor < content.len() {
+ let Some(directive_len) = directive_at(content, cursor) else {
+ cursor += 1;
+ continue;
+ };
+
+ result[cursor..cursor + directive_len].fill(b' ');
+
+ let mut depth = 1;
+ let mut end = cursor + directive_len;
+
+ while end < content.len() {
+ match content[end] {
+ b'\\' => {
+ end += 2;
+ continue;
+ }
+ b'(' => depth += 1,
+ b')' => {
+ depth -= 1;
+
+ if depth == 0 {
+ result[end] = b' ';
+ cursor = end + 1;
+ break;
+ }
+ }
+ _ => {}
+ }
+
+ end += 1;
+ }
+
+ if end >= content.len() {
+ break;
+ }
+ }
+
+ result
+ }
+}
+
+fn directive_at(content: &[u8], offset: usize) -> Option {
+ if !is_directive_boundary(content, offset) {
+ return None;
+ }
+
+ for directive in [b"addClass(".as_slice(), b"removeClass(".as_slice()] {
+ if content[offset..].starts_with(directive) {
+ return Some(directive.len());
+ }
+ }
+
+ None
+}
+
+fn is_directive_boundary(content: &[u8], offset: usize) -> bool {
+ if offset == 0 {
+ return true;
+ }
+
+ matches!(
+ content[offset - 1],
+ b'\t' | b'\n' | b'\x0C' | b'\r' | b' ' | b'"' | b'\'' | b'`' | b'|' | b'='
+ )
+}
+
+#[cfg(test)]
+mod tests {
+ use super::Twig;
+ use crate::extractor::pre_processors::pre_processor::PreProcessor;
+
+ #[test]
+ fn test_live_component_loading_directives() {
+ Twig::test("addClass(opacity-50)", " opacity-50 ");
+ Twig::test(
+ "data-loading=\"delay|addClass(opacity-50)|removeClass(hidden)\"",
+ "data-loading=\"delay| opacity-50 | hidden \"",
+ );
+ Twig::test(
+ "data-loading=\"model(user.email)|addClass(bg-(--loading-color))\"",
+ "data-loading=\"model(user.email)| bg-(--loading-color) \"",
+ );
+ }
+
+ #[test]
+ fn test_extract_live_component_classes() {
+ Twig::test_extract_contains(
+ r#""#,
+ vec!["opacity-50"],
+ );
+ Twig::test_extract_contains(
+ r#""#,
+ vec!["opacity-50", "pointer-events-none", "hidden"],
+ );
+ Twig::test_extract_contains(
+ r#""#,
+ vec!["bg-(--loading-color)", "bg-[url(https://example.com)]"],
+ );
+ }
+}
diff --git a/crates/oxide/src/scanner/mod.rs b/crates/oxide/src/scanner/mod.rs
index 30724f162188..a9b611fc0e22 100644
--- a/crates/oxide/src/scanner/mod.rs
+++ b/crates/oxide/src/scanner/mod.rs
@@ -480,6 +480,7 @@ pub fn pre_process_input(content: Vec, extension: &str) -> Vec {
"rb" | "erb" => Ruby.process(&content),
"slim" | "slang" => Slim.process(&content),
"svelte" => Svelte.process(&content),
+ "twig" => Twig.process(&content),
"rs" => Rust.process(&content),
"vue" => Vue.process(&content),
_ => content,