Zum Inhalt springen

CSS-Resolver: Kaskade und Spezifität

CssResolver gleicht Selektoren anhand des Token-Streams ab, ordnet die passenden Regeln nach Cascade-Layer, Spezifität und Dokumentreihenfolge und wendet danach in einem zweiten Durchlauf !important an.

Terminal-Fenster
composer require nextpdf/core:^3

CssResolver ist die Komponente von Layer 1 (gemäß ADR-010). Sie hält die geparsten CSS-Regeln vor und ermittelt, welche Deklarationen auf welches Element zutreffen. Sie wurde aus HtmlParser herausgelöst, um die Struktur klarer zu trennen. Sie ist eine interne Klasse und nicht Teil der öffentlichen API.

Der Resolver arbeitet ohne Dokumentbaum. Der Selektor-Abgleich liest den flachen Token-Stream und nutzt außerdem die Index-Maps, die HtmlChildScanner in Stage 3 der Pipeline aufbaut: Anzahl der Kinder, Zählwerte gleicher Tags und Leerheitsinformationen. Strukturelle Pseudoklassen werden anhand dieser Maps aufgelöst. Der relationale Selektor :has() stützt sich auf den begrenzten Pre-Scan, der in Streaming-Constraints beschrieben ist.

Die Kaskadenauflösung erfolgt in CssResolver::resolveMatchingProperties() in zwei Durchläufen. Durchlauf 1 wendet die normalen Deklarationen in Kaskadenreihenfolge an: zuerst nach dem Gewicht des Cascade-Layers, dann nach Spezifität und danach nach Dokumentreihenfolge. Durchlauf 2 wendet anschließend die !important-Deklarationen in Spezifitätsreihenfolge an; eine !important-Deklaration überschreibt jede normale Deklaration unabhängig von der Spezifität. Diese Aufteilung in zwei Durchläufe ist die Implementierungsstrategie und erzeugt die aufgelöste Eigenschaftsmenge, die der Layout-Layer anschließend verarbeitet.

Die Kaskadenreihenfolge, die der Resolver umsetzt, deckt sich mit der W3C-Spezifikation „CSS Cascading and Inheritance“. Deklarationen werden zuerst nach Ursprung und Wichtigkeit und anschließend nach der Spezifität des Selektors sortiert. Bei gleicher Spezifität gewinnt die letzte Deklaration in Dokumentreihenfolge (CSS Cascade 5 §6.4; siehe Konformität). Der Kommentar im Quellcode von CssResolver verweist auf dieselbe Klausel und gibt Ihnen damit neben der Spezifikation und dem Glossar einen dritten Belegpfad für dieses Verhalten.

Die Spezifität wird als Tripel (A, B, C) aus der Anzahl der ID-, Klassen- und Typkomponenten berechnet, und die Tripel werden Komponente für Komponente verglichen (Selectors Level 4 §16). NextPDF berechnet die Spezifität pro passender Regel vor der Kaskadensortierung.

Eine Einschränkung sollte klar benannt werden: Die Layer-Inversion-Regel aus §6.4.3 gilt für !important-Deklarationen über Cascade-Layer hinweg; der Quellcode vermerkt sie als offenen Punkt für das Arbeitscluster zu den Cascade-Layern. Wenn Cascade-Layer deklariert sind und !important Layer-Grenzen überschreitet, kann die aufgelöste Reihenfolge vom vollständigen Spezifikationsverhalten abweichen. Die CSS-Support-Matrix ist die maßgebliche Quelle für den Support-Status je Feature; diese Seite wiederholt den Support je Eigenschaft nicht.

SymbolSpeicherortRolle
CssResolver::parseStyleBlock(string $css, bool $nestingEnabled = false): voidsrc/Html/CssResolver.phpParst einen <style>-Block in Regeln.
CssResolver::resolveMatchingProperties(...)src/Html/CssResolver.phpGleicht Selektoren ab und löst die Kaskade in zwei Durchläufen auf.
CssResolver::resolveHasSelectors(array $tokens): arraysrc/Html/CssResolver.phpBegrenzter Pre-Scan für :has() (gegated).
CssResolver::resolveFirstLetterProperties(...)src/Html/CssResolver.phpLöst die Eigenschaften für ::first-letter auf.
CssResolver::resolvePseudoElementProperties(...)src/Html/CssResolver.phpLöst die Eigenschaften für ::before / ::after auf.
CssResolver::getLayerRegistry(): LayerRegistrysrc/Html/CssResolver.phpDeklarierte Cascade-Layer.

Aufrufer verwenden den Resolver nicht direkt; sie schreiben CSS, und der Resolver läuft innerhalb von writeHtml(). Die folgende Kaskade löst p zu Rot auf, weil die Klassenregel eine höhere Spezifität hat als die Typregel.

<?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');

Das Beispiel zeigt den zweiten Durchlauf für !important. Die inline-äquivalente Klassendeklaration wird von der !important-Typdeklaration überschrieben, obwohl der Klassenselektor eine höhere Spezifität hat.

<?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 ignoriert die Spezifität. Durchlauf 2 wendet die !important-Deklarationen in Spezifitätsreihenfolge an, und sie überschreiben immer die normalen Deklarationen.
  • Cascade-Layer + !important über Layer hinweg. Die Layer-Inversion-Regel aus §6.4.3 für wichtige Deklarationen ist im Quellcode als offen vermerkt. Prüfen Sie das Verhalten gegen die CSS-Support-Matrix, bevor Sie sich darauf verlassen.
  • Keine deklarierten Layer bedeuten den schnellen Pfad. Ohne @layer reduziert sich die Ordnung auf reine Spezifität und ist bitgenau identisch mit dem Verhalten vor der Layer-Einführung.
  • :has() ist gegated. Der relationale Pre-Scan läuft nur, wenn das experimentelle Feature css.has aktiviert ist.
  • Der Selektor-Abgleich ist Stream-basiert. Strukturelle Selektoren nutzen Index-Maps, keinen Baumdurchlauf. Ein Selektor, der eine beliebige Baumnavigation jenseits der Index-Maps benötigen würde, ist in diesem Modell nicht auflösbar.

Der Selektor-Abgleich ist im schlechtesten Fall O(Regeln × Elemente), begrenzt durch die Streaming-Obergrenzen. Die beiden Kaskadensortierungen sind O(passende Regeln · log passende Regeln) pro Element. Der ungelayerte Pfad überspringt die Layer-Auflösung vollständig. Das performance_budget pro Seite (wall_ms: 1500, peak_mb: 64) ist die operative Obergrenze. Regressionen werden durch den Benchmark der HTML-Render-Pipeline abgesichert (zusammengeführt in PR #564).

Der Resolver sieht nur das CSS, das DefaultHtmlSecurityPolicy::isCssPropertyAllowed() zulässt. Die Allowlist ist die Sicherheitsobergrenze, und die Laufzeit-Support-Tabelle bildet eine separate Fähigkeitsobergrenze. Eine durch die Policy blockierte Eigenschaft erreicht die Kaskade nie. Siehe das Sicherheitsmodell des HTML-Moduls.

VerhaltenSpezifikationKlauselreference_id
Kaskadensortierung: origin/importance → Spezifität → Reihenfolge des AuftretensW3C CSS Cascading and Inheritance Level 5§6.4 (css_cascade_5#x1.x7.x1.p21)
Spezifität als Tripel (A,B,C) aus ID-/Klassen-/TypzählungenW3C Selectors Level 4§16 (selectors_4#x1.x16.p2)
Deterministisches Parsen und Wiederherstellung nach Parse-FehlernW3C CSS Syntax Level 3§4 (css_syntax_3#x1.x4.p2)

W3C-Material steht unter CC-BY 4.0. Die Aussagen oben sind paraphrasiert. Klausel- und Chunk-Bezeichner sind zur Verifikation angegeben. NextPDF beansprucht keine vollständige Konformität mit diesen Modulen – siehe die CSS-Support-Matrix für den verifizierten Status je Modul.

Enterprise-Fähigkeit. Premium erweitert die Menge der abgeglichenen und angewendeten Eigenschaften. Der Kaskadenalgorithmus und das Zwei-Durchlauf-Modell für !important sind in allen Editionen identisch. Siehe die CSS-Support-Matrix.