Aller au contenu

Résolveur CSS : cascade et spécificité

CssResolver fait correspondre les sélecteurs au flux de tokens, ordonne les règles correspondantes selon la couche de cascade, la spécificité et l’ordre du document, puis applique !important lors d’une seconde passe.

Fenêtre de terminal
composer require nextpdf/core:^3

CssResolver est le composant de la couche 1 (selon ADR-010). Il détient les règles CSS analysées et détermine quelles déclarations s’appliquent à chaque élément. Il a été extrait de HtmlParser pour clarifier la structure ; il s’agit d’une classe interne plutôt que d’une API publique.

Le résolveur fonctionne sans arbre de document. La correspondance des sélecteurs lit le flux de tokens à plat et s’appuie aussi sur les cartes d’index que HtmlChildScanner construit à l’étape 3 du pipeline : nombre d’enfants, nombre de balises identiques et vacuité. Les pseudo-classes structurelles sont résolues à partir de ces cartes. Le sélecteur relationnel :has() repose sur le pré-balayage borné décrit dans les contraintes de streaming.

La résolution de la cascade s’exécute en deux passes à l’intérieur de CssResolver::resolveMatchingProperties(). La passe 1 applique les déclarations normales dans l’ordre de la cascade, c’est-à-dire d’abord le poids de la couche de cascade, puis la spécificité, puis l’ordre du document. La passe 2 applique ensuite les déclarations !important dans l’ordre de spécificité, et une déclaration !important l’emporte sur toute déclaration normale, quelle que soit sa spécificité. Cette séparation en deux passes est la stratégie d’implémentation ; elle produit l’ensemble de propriétés résolu que la couche de mise en page consomme ensuite.

L’ordre de cascade implémenté par le résolveur est aligné sur la spécification W3C CSS Cascading and Inheritance. Les déclarations sont triées d’abord par origine et importance, puis par spécificité du sélecteur. À spécificité égale, la dernière déclaration dans l’ordre du document l’emporte (CSS Cascade 5 §6.4 ; voir Conformité). Le commentaire dans le code source de CssResolver cite la même clause, ce qui te donne une troisième piste de vérification pour ce comportement, en plus de la spécification et du glossaire.

La spécificité est calculée sous forme de triplet (A, B, C) à partir du nombre de composants ID, classe et type, et les triplets sont comparés composant par composant (Selectors Level 4 §16). NextPDF calcule la spécificité de chaque règle correspondante avant le tri de la cascade.

Une contrainte mérite d’être formulée clairement. La règle d’inversion de couche du §6.4.3 s’applique aux déclarations !important à travers les couches de cascade, et le code source la consigne comme en suspens pour le lot de travail sur les couches de cascade. Lorsque des couches de cascade sont déclarées et que !important traverse les couches, l’ordre résolu peut différer du comportement complet de la spécification. La matrice de prise en charge CSS fait autorité sur l’état de prise en charge par fonctionnalité, et cette page ne reprend pas la prise en charge propriété par propriété.

SymboleEmplacementRôle
CssResolver::parseStyleBlock(string $css, bool $nestingEnabled = false): voidsrc/Html/CssResolver.phpAnalyse un bloc <style> en règles.
CssResolver::resolveMatchingProperties(...)src/Html/CssResolver.phpFait correspondre les sélecteurs et résout la cascade en deux passes.
CssResolver::resolveHasSelectors(array $tokens): arraysrc/Html/CssResolver.phpPré-balayage borné de :has() (sous condition).
CssResolver::resolveFirstLetterProperties(...)src/Html/CssResolver.phpRésout les propriétés ::first-letter.
CssResolver::resolvePseudoElementProperties(...)src/Html/CssResolver.phpRésout les propriétés ::before / ::after.
CssResolver::getLayerRegistry(): LayerRegistrysrc/Html/CssResolver.phpCouches de cascade déclarées.

Les appelants n’invoquent pas directement le résolveur ; ils écrivent du CSS, et le résolveur s’exécute à l’intérieur de writeHtml(). La cascade ci-dessous résout p en rouge, car la règle de classe a une spécificité plus élevée que la règle de type.

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

Démontre la seconde passe !important. La déclaration de classe équivalente à l’inline est remplacée par la déclaration de type !important, même si le sélecteur de classe a une spécificité plus élevée.

<?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 ignore la spécificité. La passe 2 applique les déclarations !important dans l’ordre de spécificité, et elles l’emportent toujours sur les déclarations normales.
  • Couches de cascade + !important à travers les couches. La règle d’inversion de couche du §6.4.3 pour les déclarations importantes est enregistrée comme en suspens dans le code source. Vérifie le comportement avec la matrice de prise en charge CSS avant de t’y fier.
  • L’absence de couche déclarée est le chemin rapide. Sans @layer, l’ordonnancement se réduit à la seule spécificité et est identique bit pour bit au comportement antérieur aux couches.
  • :has() est sous condition. Le pré-balayage relationnel ne s’exécute que lorsque la fonctionnalité expérimentale css.has est activée.
  • La correspondance des sélecteurs est basée sur le flux. Les sélecteurs structurels utilisent des cartes d’index, pas un parcours d’arbre. Un sélecteur qui nécessiterait une navigation arbitraire dans l’arbre au-delà des cartes d’index n’est pas résolvable dans ce modèle.

La correspondance des sélecteurs est en O(règles × éléments) dans le pire des cas, bornée par les plafonds de streaming. Les deux tris de cascade sont en O(règles correspondantes · log règles correspondantes) par élément. Le chemin sans couche saute entièrement la résolution des couches. Le performance_budget par page (wall_ms: 1500, peak_mb: 64) est le plafond opérationnel. Les régressions sont protégées par le benchmark du pipeline de rendu HTML (travail fusionné, PR #564).

Le résolveur ne voit que le CSS admis par DefaultHtmlSecurityPolicy::isCssPropertyAllowed(). La liste d’autorisation est le plafond de sécurité, et la table de prise en charge à l’exécution est un plafond de capacité distinct. Une propriété bloquée par la politique n’atteint jamais la cascade. Voir le modèle de sécurité du module HTML.

ComportementSpécificationClausereference_id
Tri de cascade : origin/importance → spécificité → ordre d’apparitionW3C CSS Cascading and Inheritance Level 5§6.4 (css_cascade_5#x1.x7.x1.p21)
Spécificité sous forme de triplet (A,B,C) à partir du nombre d’ID/classes/typesW3C Selectors Level 4§16 (selectors_4#x1.x16.p2)
Analyse déterministe et récupération sur erreur d’analyseW3C CSS Syntax Level 3§4 (css_syntax_3#x1.x4.p2)

Le matériel W3C est sous licence CC-BY 4.0. Les affirmations ci-dessus sont paraphrasées. Les identifiants de clause et de chunk sont fournis à des fins de vérification. NextPDF ne revendique pas une conformité complète à ces modules — voir la matrice de prise en charge CSS pour l’état vérifié par module.

Capacité Enterprise. Premium élargit l’ensemble des propriétés correspondantes et appliquées. L’algorithme de cascade et le modèle !important en deux passes sont identiques entre les éditions. Voir la matrice de prise en charge CSS.