SelectorParserにCSS仕様準拠の構造化ノードを追加
Tailwind CSSの内部 SelectorParser に list・complex・compound の3種類の新しいノード型が追加されました。これにより、セレクタのASTがCSS仕様に沿った階層構造を持つようになり、複数セレクタの判定や解析処理が大幅に簡潔になります。
背景
これまでの SelectorParser は、セレクタを単純なフラットなノード配列として返していたため、複数セレクタの判定に全ノードを走査する必要がありました。たとえば #a.b > .c, .d は次のように解析されていました:
[
{ kind: 'selector', value: '#a' },
{ kind: 'selector', value: '.b' },
{ kind: 'combinator', value: ' > ' },
{ kind: 'selector', value: '.c' },
{ kind: 'separator', value: ', ' },
{ kind: 'selector', value: '.d' }
]
「これが複数セレクタかどうか」を知るには、配列全体をスキャンして separator ノードの有無を確認するしかありませんでした。isSingleSelector() 関数が !ast.some((node) => node.kind === 'separator' && node.value.trim() === ',') という実装になっていたことが、この問題を端的に示しています。
セレクタの構造はパース時点で既にわかっているにもかかわらず、その情報がASTに反映されていなかった点が、実装上の課題でした。
技術的な変更
selector-parser.ts に3つの新しいノード型が追加され、フラットな配列構造が階層的なASTへと変わりました。
新しいノード型の定義:
export type SelectorListNode = {
kind: 'list'
nodes: SelectorAstNode[]
}
export type SelectorComplexNode = {
kind: 'complex'
nodes: SelectorAstNode[]
}
export type SelectorCompoundNode = {
kind: 'compound'
nodes: SelectorAstNode[]
}
同じ #a.b > .c, .d は、今後次のように解析されます:
[
{
kind: 'list',
nodes: [
{
kind: 'complex',
nodes: [
{
kind: 'compound',
nodes: [
{ kind: 'selector', value: '#a' },
{ kind: 'selector', value: '.b' }
]
},
{ kind: 'combinator', value: '>' },
{ kind: 'selector', value: '.c' }
]
},
{ kind: 'selector', value: '.d' }
]
}
]
list ノードが存在すること自体が「複数のセレクタが存在する」という事実を表すため、isSingleSelector() の実装は !ast.some(...) によるノード全走査から、ast[0].kind === 'list' の1回チェックへと簡略化されました。
// 変更前
function isSingleSelector(ast: SelectorParser.SelectorAstNode[]): boolean {
return !ast.some((node) => node.kind === 'separator' && node.value.trim() === ',')
}
// 変更後
function isSingleSelector(ast: SelectorParser.SelectorAstNode[]): boolean {
if (ast.length === 1 && ast[0].kind === 'list') return false
return true
}
あわせて、isAttributeSelector() の型シグネチャが SelectorAstNode から SelectorNode に絞り込まれ、node.value.trim() の前処理も不要になっています。また、既存のアービトラリバリアント処理は complex ノードを直接扱わず ast[0].nodes へフラット化するワークアラウンドが導入され、後方互換性を維持しています。
設計判断
既存の walk 機構との一貫性 を最優先に、complex ノードの表現方式が選ばれています。他のCSS関連ライブラリでは complex を { combinator, lhs, rhs } のような二分木形式で表現するのが一般的ですが、本PRではあえて nodes 配列形式を採用しています。PR本文で明示されているとおり、walk が .nodes を基準に動作する設計上、特殊なハンドリングなしに既存のトラバーサルコードがそのまま利用できるためです。
また、separator ノードを廃止し list ノードに置き換えた 点も重要な判断です。list ノードの存在そのものが「複数セレクタである」という意味論を持つため、子ノードを探索してカンマを探すという冗長な処理が不要になります。この設計はCSS Selector Level 4のセレクタ構造の定義に準拠したものです。
なお、PR本文には「内部的なリファクタリングであり、このパーサーは公開APIとして公開されていない」と明記されており、外部への破壊的変更はありません。
まとめ
本PRは、フラットな配列として表現されていたセレクタASTを、CSS仕様に準拠した list・complex・compound の階層構造へと整理した内部リファクタリングです。構造自体に意味論を持たせることで、複数セレクタの判定がノードの型チェック1回で完結するようになり、今後の解析処理の拡張や整理に向けた基盤が整いました。