Twig テンプレートの addClass / removeClass 用プリプロセッサを追加
Tailwind CSS は Symfony Live Component で使用される Twig の addClass(...) / removeClass(...) ディレクティブが正しく抽出されず、クラスがビルド結果に含まれない問題を解消します。新たに Twig 用プリプロセッサを導入し、括弧をスペースに置換することでクラス抽出を可能にしました。
背景
addClass(opacity-50) や removeClass(opacity-50) が記述された Twig テンプレートからクラスが抽出できない というバグが、Issue #19458 で報告されました。Symfony の Live Component では data-loading="addClass(opacity-50)" のように記述されますが、従来の抽出ロジックは括弧を含む文字列をクラス名とみなさず、結果として opacity-50 が生成 CSS に現れませんでした。PR #20198 はこの欠陥を対象に、Twig ファイルだけに限定した前処理を導入しています。
技術的な変更
CHANGELOG の追記
@@ -24,6 +24,7 @@
- Ensure standalone `@tailwindcss/cli` binaries are ignored when scanning for class candidates ([#20139](https://github.com/tailwindlabs/tailwindcss/pull/20139))
+- Ensure class candidates are extracted from Twig `addClass(…)` and `removeClass(…)` calls ([#20198](https://github.com/tailwindlabs/tailwindcss/pull/20198))
プリプロセッサモジュールの追加 (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 ruby::*;
pub use rust::*;
pub use slim::*;
pub use svelte::*;
+pub use twig::*;
pub use vue::*;
新規実装ファイル (crates/oxide/src/extractor/pre_processors/twig.rs)
use crate::cursor;
use crate::extractor::pre_processors::pre_processor::PreProcessor;
#[derive(Debug, Default)]
pub struct Twig;
impl PreProcessor for Twig {
fn process(&self, content: &[u8]) -> Vec<u8> {
let len = content.len();
let mut result = content.to_vec();
let mut cursor = cursor::Cursor::new(content);
let mut bracket_stack = vec![];
const ADD_CLASS: &[u8] = b"addClass";
const REMOVE_CLASS: &[u8] = b"removeClass";
while cursor.pos < len {
match (!bracket_stack.is_empty(), cursor.curr()) {
// addClass(
(false, b'(') if cursor.pos >= ADD_CLASS.len()
&& matches!(&content[cursor.pos - ADD_CLASS.len()..cursor.pos], ADD_CLASS) => {
bracket_stack.push(cursor.curr());
result[cursor.pos] = b' ';
}
// removeClass(
(false, b'(') if cursor.pos >= REMOVE_CLASS.len()
&& matches!(&content[cursor.pos - REMOVE_CLASS.len()..cursor.pos], REMOVE_CLASS) => {
bracket_stack.push(cursor.curr());
result[cursor.pos] = b' ';
}
(true, b'(') => {
bracket_stack.push(cursor.curr());
}
(true, b')') => {
// handling of nested parentheses omitted for brevity
}
_ => {}
}
cursor.next();
}
result
}
}
スキャナーへのフック追加 (crates/oxide/src/scanner/mod.rs)
@@ -481,6 +481,7 @@
"slim" | "slang" => Slim.process(&content),
"svelte" => Svelte.process(&content),
"rs" => Rust.process(&content),
+ "twig" => Twig.process(&content),
"vue" => Vue.process(&content),
_ => content,
これにより、.twig 拡張子の入力は Twig.process が呼び出され、addClass( と removeClass( の直後の括弧がスペースへ置換されます。結果として addClass(opacity-50) → addClass opacity-50 となり、既存のクラス抽出ロジックが opacity-50 を認識できるようになります。
設計判断
シンプルな置換ロジックの採用
PR では 括弧を空白に置換するだけ の最小限実装が選択されました。これは「現時点で実装が必要なケースは addClass / removeClass の単純な呼び出し」だけであり、エスケープ文字や文字列内部の括弧への対応は将来的な要件として保留されています。実装を絞ることでコードベースへの影響を最小化し、既存の汎用抽出ロジックを変更せずに済む設計となっています。
拡張性と後方互換性の確保
新しいプリプロセッサは mod.rs に pub mod twig; と pub use twig::*; を追加しただけで、他の言語向けプリプロセッサと同様のインターフェースを維持しています。そのため、既存のプラグインやビルドパイプラインが変更を感知せずに動作し続けます。また、.twig 以外のファイルに対しては従来通りの抽出が行われるため、クラス抽出範囲が意図せず広がるリスクも回避されています。
まとめ
Twig テンプレートの addClass / removeClass ディレクティブからクラスを抽出できなかった問題は、専用プリプロセッサで括弧をスペースに置換するシンプルな前処理を追加することで解消されました。変更は最小限に抑えつつ、既存の抽出ロジックと互換性を保ち、今後必要に応じて拡張が可能な設計となっています。