セレクターコンビネーターの型付けと空白処理の統一
セレクターパーサーの combinator ノードに明示的な型を導入し、空白処理をパーサー内部に集約することで、利用側コードでの .trim() 呼び出しを不要にしました。
背景
#20088 によって list・complex・compound ノードが追加されたことで、セレクターのAST構造が整理されましたが、コンビネーターの値には依然として空白が混入する可能性がありました。具体的には、.foo + .bar のような入力を解析すると combinator ノードの value が ' + '(前後にスペース付き)として格納されるケースがあり、利用側のコードは ast[1].value.trim() === '>' のように .trim() を呼び出してから比較する必要がありました。
この問題は canonicalize-candidates.ts の複数箇所に散らばっており、コメントで「// space, but trimmed because there could be multiple spaces」と注釈を付けなければ意図が伝わらない状態でした。本PRはこの根本原因をパーサー側で解消するフォローアップです。
技術的な変更
Combinator 型を導入し、コンビネーター値を正規化したことが本PRの核心です。selector-parser.ts にユニオン型 Combinator を新設し、SelectorCombinatorNode.value の型を string から Combinator に変更しました。
type Combinator =
| ' ' // Descendant combinator
| '>' // Child combinator
| '+' // Next-sibling combinator
| '~' // Subsequent-sibling combinator
パーサー内部では、.foo \n\t .bar のように複数の空白や改行が混在した子孫コンビネーターも、単一のスペース ' ' に正規化して格納します。新たに追加されたテストケースがこの挙動を明文化しています。
expect(parse('.foo \n\t .bar')).toEqual([
{
kind: 'complex',
nodes: [
{ kind: 'selector', value: '.foo' },
{ kind: 'combinator', value: ' ' },
{ kind: 'selector', value: '.bar' },
],
},
])
正規化によって空白情報が失われるため、AST を CSS 文字列に戻す際の制御が必要になります。toCss 関数には minify フラグ(デフォルト false)が追加されました。
変更前:
export function toCss(ast: SelectorAstNode[]) {
// ...
case 'combinator': {
if (node.value === ' ') {
css += node.value
} else {
css += ` ${node.value} `
}
}
}
変更後:
export function toCss(ast: SelectorAstNode[], minify = false) {
// ...
case 'combinator': {
if (minify || node.value === ' ') {
css += node.value
} else {
css += ` ${node.value} `
}
}
}
minify = false(デフォルト)では > や + などのコンビネーターを > や + のように前後にスペースを付けて出力します。minify = true を指定するとすべてのコンビネーター値をそのまま出力し、空白を一切挿入しません。正規化(canonicalization)用途では toCss(ast, true) が使用されます。
この変更を受けて canonicalize-candidates.ts では、.trim() を伴う比較がすべて直接比較に置き換えられました。
変更前:
ast[1].kind === 'combinator' &&
ast[1].value.trim() === '>' &&
// ...
ast[1].value.trim() === '' && // space, but trimmed because there could be multiple spaces
変更後:
ast[1].kind === 'combinator' &&
ast[1].value === '>' &&
// ...
ast[1].value === ' ' &&
設計判断
パーサーが正規化の責務を持ち、利用側は正規化された値のみを扱うという設計方針が採用されました。
toCss に minify フラグを追加する代替案として、別途 minify 専用の関数を用意する方法も考えられますが、既存のシグネチャを最小限の変更で拡張するアプローチが選ばれています。デフォルトを false(空白あり)にすることで、既存の呼び出し元への影響をゼロに抑えつつ、正規化が必要な箇所だけ true を渡せるようになっています。
TypeScriptの型システムを活用して Combinator をリテラル型のユニオンとして定義することで、コンパイル時に不正な値の混入を防ぐ効果もあります。コードのドキュメントとしての役割も兼ねており、CSS仕様における4種類のコンビネーターが型定義に直接反映されています。
まとめ
コンビネーターの正規化責務をパーサー内部に集約したことで、利用側コードから .trim() と説明コメントが一掃され、AST操作の意図がコードから直接読み取れるようになりました。Combinator 型の導入はこの設計をTypeScriptの型システムで保証するものであり、今後のセレクター処理の拡張においても不正な値の混入をコンパイル時に検出できる基盤となっています。