`calc()` 正規化処理で演算子周辺の空白が失われるバグを修正
任意値における負値正規化(canonicalization)処理で、calc() 式内の数学演算子周辺の空白が失われ、無効なCSSが生成されるバグが修正されました。
背景
Tailwind CSS には、冗長なクラス記述を簡潔な等価表現へ正規化する仕組みがあります。たとえば left-[calc(-1*(var(--my-var1)+var(--my-var2)))] は、-left-[(var(--my-var1)+var(--my-var2))] という短い形式に正規化されます。この正規化処理では calc(-1 * <value>) を取り除き、代わりにクラス名の先頭に - を付けることで同等の意味を表現します。
しかし #20010 で報告されたように、この正規化後の形式を直接記述すると、生成されるCSSに問題が生じていました。任意値のパース時には calc() コンテキストを検出して演算子周辺に空白を補完する処理が走りますが、calc(<value> * -1) のラッピングはパース後に追加されるため、補完処理が適用されないまま出力されていたのです。
正規化前の left-[calc(-1*(var(--my-var1)+var(--my-var2)))] は意図通りのCSSを生成していたのに対し、等価なはずの正規化後の記述 -left-[(var(--my-var1)+var(--my-var2))] は無効なCSSを出力していました。正規化後の形式を推奨するツールの指示に従うと壊れたCSSが生成される状況でもありました。
技術的な変更
packages/tailwindcss/src/utilities.ts の負値処理箇所に、addWhitespaceAroundMathOperators の呼び出しが追加されました。
変更前:
return desc.handle(negative ? `calc(${value} * -1)` : value, dataType)
変更後:
return desc.handle(
negative ? addWhitespaceAroundMathOperators(`calc(${value} * -1)`) : value,
dataType,
)
この変更により、-left-[(var(--my-var1)+var(--my-var2))] に対して生成されるCSSが以下のように修正されます。
修正前(無効なCSS):
.-left-\[\(var\(--my-var1\)\+var\(--my-var2\)\)\] {
left: calc((var(--my-var1)+var(--my-var2)) * -1);
}
修正後(有効なCSS):
.-left-\[\(var\(--my-var1\)\+var\(--my-var2\)\)\] {
left: calc((var(--my-var1) + var(--my-var2)) * -1);
}
addWhitespaceAroundMathOperators は ./utils/math-operators から新たにインポートされており、+ や - などの演算子の前後に適切な空白を挿入します。負値でない通常のケース(negative が false)は変更の影響を受けず、既存の挙動はそのまま維持されます。
リグレッションテストとして、packages/tailwindcss/src/utilities.test.ts の left テストケースに -left-[(var(--my-var1)+var(--my-var2))] が追加されました。修正前はこのテストが失敗し、修正後に正しい出力を示すことで変更の妥当性を確認しています。
設計判断
空白補完の責務を「calc() ラッピング時」に移すアプローチが採用されました。
PRの説明では、正規化後の値に対して空白補完が行われていない根本原因として、パースのタイミングと calc() ラッピングのタイミングのずれが挙げられています。修正はパース処理には手を加えず、ラッピング処理の出口で空白を補完する方式を採用しており、変更箇所を最小限に抑えています。
PRでは今後のフォローアップとして2点が言及されています。一点目は、正規化時の余分な括弧の除去(-left-[(var(--a)+var(--b))] → -left-[var(--a)+var(--b)])です。現時点では値が単独で式になりえるため calc((<value>) * -1) の形式を維持する必要があります。二点目は、正規化処理が同等性の判定にシグネチャを用いる仕組みの問題で、空白を除去したシグネチャの比較では本来異なるケースが同一視されうると指摘されています。いずれもより広範なテストを要するとして、今回のPRの対象からは意図的に除外されています。
まとめ
本PRは、パース後に適用される calc() ラッピング処理に空白補完を組み込むことで、正規化形式の任意値クラスから有効なCSSが生成されない問題を解消しました。根本的な正規化ロジックの問題は残されており、フォローアップPRへの道筋も示されていますが、まず実際のユーザー影響を修正する実用的な判断が取られています。