CSS リゾルバー: カスケードと詳細度
CssResolver はセレクターをトークンストリームと照合し、一致したルールをカスケードレイヤー、詳細度、ドキュメント順で並べ替えたうえで、2 番目のパスで !important を適用します。
インストール
「インストール」という見出しのセクションcomposer require nextpdf/core:^3概念の概要
「概念の概要」という見出しのセクションCssResolver は(ADR-010 に基づく)レイヤー 1 のコンポーネントです。これはパース済みの CSS ルールを保持し、各要素にどの宣言が適用されるかを resolve(解決)します。構造を明確にするために HtmlParser から切り出されたもので、公開 API ではなく内部クラスです。
リゾルバーはドキュメントツリーを使用せずに動作します。セレクターのマッチングでは、フラットなトークンストリームを読み取り、さらに HtmlChildScanner が パイプラインのステージ 3 で構築するインデックスマップ(子要素数、同一タグ数、空かどうか)も利用します。構造擬似クラスは、これらのマップに基づいて解決されます。関係セレクター :has() は、ストリーミング制約で説明されている境界付きの事前スキャンに依存します。
カスケードの解決は CssResolver::resolveMatchingProperties() の内部で 2 つのパスとして実行されます。パス 1 は通常の宣言をカスケード順、つまり最初にカスケードレイヤーの重み、次に詳細度、その次にドキュメント順で適用します。続いてパス 2 は !important 宣言を詳細度順で適用し、!important 宣言は詳細度にかかわらず任意の通常宣言を上書きします。この 2 パス分割は実装上の戦略であり、解決済みのプロパティセットを生成し、それをレイアウトレイヤーが利用します。
リゾルバーが実装するカスケード順は、W3C CSS Cascading and Inheritance 仕様に準拠しています。宣言は、まずオリジンと重要度で、次にセレクターの詳細度で並べ替えられます。詳細度が同じ場合は、ドキュメント順で最後の宣言が優先されます(CSS Cascade 5 §6.4。「準拠」を参照)。CssResolver 内のソースコメントも同じ条項を引用しているため、仕様と用語集に加えて、この挙動を裏付ける 3 つ目の根拠として確認できます。
詳細度は ID、クラス、タイプの各構成要素の数から (A, B, C) の 3 つ組として計算され、3 つ組同士は構成要素ごとに比較されます(Selectors Level 4 §16)。NextPDF はカスケードを並べ替える前に、マッチしたルールごとに詳細度を計算します。
ここで明示しておくべき制約が 1 つあります。§6.4.3 のレイヤー反転ルールはカスケードレイヤーをまたぐ !important 宣言に適用され、ソースではこれをカスケードレイヤー作業クラスターの未完了事項として記録しています。カスケードレイヤーが宣言され、かつ !important がレイヤーをまたぐ場合、解決される順序が完全な仕様の挙動と異なることがあります。機能ごとのサポート状態については CSS サポートマトリクスが正式な情報源であり、このページではプロパティごとのサポート状況を改めて記載しません。
API サーフェス
「API サーフェス」という見出しのセクション| シンボル | 場所 | 役割 |
|---|---|---|
CssResolver::parseStyleBlock(string $css, bool $nestingEnabled = false): void | src/Html/CssResolver.php | 指定された <style> ブロックのルールへのパース |
CssResolver::resolveMatchingProperties(...) | src/Html/CssResolver.php | セレクターのマッチングと 2 パスのカスケード解決 |
CssResolver::resolveHasSelectors(array $tokens): array | src/Html/CssResolver.php | 境界付きの :has() 事前スキャン(ゲート付き)。 |
CssResolver::resolveFirstLetterProperties(...) | src/Html/CssResolver.php | 指定された ::first-letter プロパティの解決 |
CssResolver::resolvePseudoElementProperties(...) | src/Html/CssResolver.php | 指定された ::before / ::after プロパティの解決 |
CssResolver::getLayerRegistry(): LayerRegistry | src/Html/CssResolver.php | 宣言済みのカスケードレイヤー |
コードサンプル — クイックスタート
「コードサンプル — クイックスタート」という見出しのセクション呼び出し元はリゾルバーを直接呼び出しません。CSS を記述すると、リゾルバーは writeHtml() の内部で実行されます。クラスルールはタイプルールより詳細度が高いため、以下のカスケードでは p が赤として解決されます。
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();$doc->addPage();$doc->writeHtml( '<style>p { color: blue; } .lead { color: red; }</style>' . '<p class="lead">Higher-specificity class wins.</p>');$doc->save(__DIR__ . '/output/cascade.pdf');コードサンプル — 本番
「コードサンプル — 本番」という見出しのセクションここでは !important の 2 番目のパスを示します。クラスセレクターの方が詳細度が高いにもかかわらず、インライン相当のクラス宣言は !important のタイプ宣言によって上書きされます。
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();$doc->addPage();$doc->writeHtml( '<style>p { color: green !important; } .lead { color: red; }</style>' . '<p class="lead">!important overrides higher specificity.</p>');$doc->save(__DIR__ . '/output/important.pdf');エッジケースと注意点
「エッジケースと注意点」という見出しのセクション!importantは詳細度を無視します。 パス 2 は!important宣言を詳細度順で適用し、これらは常に通常宣言を上書きします。- カスケードレイヤー + レイヤーをまたぐ
!important。 important 宣言に対する §6.4.3 のレイヤー反転ルールは、ソースで未完了として記録されています。この挙動に依存する前に、CSS サポートマトリクスで挙動を検証してください。 - レイヤー未宣言が高速パスです。
@layerがない場合、並べ替えは詳細度のみに簡略化され、レイヤー導入前の挙動とビット単位で同一になります。 :has()はゲート付きです。 関係性の事前スキャンは、css.has実験的機能が有効な場合にのみ実行されます。- セレクターのマッチングはストリームベースです。 構造セレクターはツリーの走査ではなくインデックスマップを使用します。インデックスマップを超える任意のツリーナビゲーションを必要とするセレクターは、このモデルでは解決できません。
パフォーマンス
「パフォーマンス」という見出しのセクションセレクターのマッチングは最悪ケースで O(rules × elements) であり、ストリーミング上限によって境界付けられます。2 つのカスケードの並べ替えは、要素ごとに O(matched rules · log matched rules) です。レイヤーなしのパスではレイヤー解決を完全にスキップします。ページごとの performance_budget(wall_ms: 1500、peak_mb: 64)が運用上の上限です。リグレッションは HTML レンダリングパイプラインのベンチマーク(マージ済みの作業、PR #564)によって防止されます。
セキュリティに関する注意
「セキュリティに関する注意」という見出しのセクションリゾルバーは DefaultHtmlSecurityPolicy::isCssPropertyAllowed() が許可した CSS のみを参照します。許可リストはセキュリティの上限であり、ランタイムのサポートテーブルはそれとは別の機能の上限です。ポリシーでブロックされたプロパティがカスケードに到達することはありません。HTML モジュールのセキュリティモデルを参照してください。
| 挙動 | 仕様 | 条項 | リファレンス ID |
|---|---|---|---|
| カスケードの並べ替え: origin/importance → 詳細度 → 出現順 | W3C CSS Cascading and Inheritance Level 5 の仕様 | §6.4 (css_cascade_5#x1.x7.x1.p21) | |
| ID/クラス/タイプの数から求めた (A,B,C) の 3 つ組としての詳細度 | W3C Selectors Level 4 の仕様 | §16 (selectors_4#x1.x16.p2) | |
| 決定論的なパースとパースエラーからの回復 | W3C CSS Syntax Level 3 の仕様 | §4 (css_syntax_3#x1.x4.p2) |
W3C の資料は CC-BY 4.0 です。上記の記述は言い換えたものです。条項とチャンクの識別子は検証用に提供されています。NextPDF はこれらのモジュールへの完全な準拠を主張しません。検証済みのモジュール単位のステータスについては、CSS サポートマトリクスを参照してください。
商用コンテキスト
「商用コンテキスト」という見出しのセクションEnterprise の機能。 Premium は、マッチして適用されるプロパティの集合を拡張します。カスケードアルゴリズムと 2 パスの
!importantモデルは、エディションをまたいで同一です。CSS サポートマトリクスを参照してください。