Ir al contenido

Resolutor de CSS: cascada y especificidad

CssResolver empareja los selectores con el flujo de tokens, ordena las reglas coincidentes por capa de cascada, especificidad y orden del documento, y después aplica !important en una segunda pasada.

Ventana de terminal
composer require nextpdf/core:^3

CssResolver es el componente de la capa 1 (según ADR-010). Gestiona las reglas de CSS analizadas y resuelve qué declaraciones se aplican a cada elemento. Se extrajo de HtmlParser para mejorar la claridad estructural, y es una clase interna, no una API pública.

El resolutor funciona sin un árbol del documento. El emparejamiento de selectores lee el flujo plano de tokens y también se apoya en los mapas de índice que HtmlChildScanner construye en la etapa 3 de la pipeline: recuentos de hijos, recuentos de la misma etiqueta y vacuidad. Las pseudoclases estructurales se resuelven a partir de esos mapas. El selector relacional :has() utiliza el preanálisis acotado descrito en restricciones de streaming.

La resolución de la cascada se ejecuta en dos pasadas dentro de CssResolver::resolveMatchingProperties(). La pasada 1 aplica las declaraciones normales en orden de cascada: primero el peso de la capa de cascada, luego la especificidad y luego el orden del documento. La pasada 2 aplica después las declaraciones !important en orden de especificidad, y una declaración !important anula cualquier declaración normal independientemente de la especificidad. Esta división en dos pasadas es la estrategia de implementación y produce el conjunto de propiedades resueltas que después consume la capa de maquetación.

El orden de cascada que implementa el resolutor se alinea con la especificación W3C CSS Cascading and Inheritance. Las declaraciones se ordenan primero por origen e importancia, y luego por especificidad del selector. A igual especificidad, gana la última declaración en orden del documento (CSS Cascade 5 §6.4; consulta Conformidad). El comentario en el código fuente de CssResolver cita la misma cláusula, de modo que aporta una tercera fuente de evidencia para este comportamiento, junto con la especificación y el glosario.

La especificidad se calcula como una terna (A, B, C) a partir de los recuentos de los componentes de ID, clase y tipo, y las ternas se comparan componente por componente (Selectors Level 4 §16). NextPDF calcula la especificidad para cada regla coincidente antes de la ordenación de la cascada.

Conviene explicitar una restricción. La regla de inversión de capas de §6.4.3 se aplica a las declaraciones !important a través de las capas de cascada, y el código fuente la registra como pendiente para el grupo de trabajo de capas de cascada. Cuando se declaran capas de cascada y !important atraviesa capas, el orden resuelto puede diferir del comportamiento completo de la especificación. La matriz de compatibilidad de CSS es la autoridad sobre el estado de compatibilidad por característica, y esta página no reitera la compatibilidad por propiedad.

SímboloUbicaciónFunción
CssResolver::parseStyleBlock(string $css, bool $nestingEnabled = false): voidsrc/Html/CssResolver.phpAnaliza un bloque <style> y lo transforma en reglas.
CssResolver::resolveMatchingProperties(...)src/Html/CssResolver.phpEmpareja los selectores y resuelve la cascada en dos pasadas.
CssResolver::resolveHasSelectors(array $tokens): arraysrc/Html/CssResolver.phpPreanálisis acotado de :has() (controlado por bandera).
CssResolver::resolveFirstLetterProperties(...)src/Html/CssResolver.phpResuelve las propiedades de ::first-letter.
CssResolver::resolvePseudoElementProperties(...)src/Html/CssResolver.phpResuelve las propiedades de ::before / ::after.
CssResolver::getLayerRegistry(): LayerRegistrysrc/Html/CssResolver.phpDevuelve las capas de cascada declaradas.

El código llamador no invoca el resolutor directamente; escribe CSS, y el resolutor se ejecuta dentro de writeHtml(). La cascada siguiente resuelve p como rojo, porque la regla de clase tiene mayor especificidad que la regla de tipo.

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

Este ejemplo muestra la segunda pasada de !important. La declaración de clase, equivalente a una declaración en línea, queda anulada por la declaración de tipo !important aunque el selector de clase tenga mayor especificidad.

<?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 ignora la especificidad. La pasada 2 aplica las declaraciones !important en orden de especificidad, y siempre anulan las declaraciones normales.
  • Capas de cascada + !important a través de capas. La regla de inversión de capas de §6.4.3 para declaraciones important está registrada como pendiente en el código fuente. Verifica el comportamiento con la matriz de compatibilidad de CSS antes de basarte en él.
  • No declarar capas es la vía rápida. Sin @layer, la ordenación se reduce solo a la especificidad y es idéntica bit a bit al comportamiento anterior a las capas.
  • :has() está controlado por bandera. El preanálisis relacional se ejecuta solo cuando la característica experimental css.has está habilitada.
  • El emparejamiento de selectores se basa en el flujo. Los selectores estructurales usan mapas de índice, no un recorrido del árbol. Un selector que necesite navegación arbitraria del árbol más allá de los mapas de índice no puede resolverse en este modelo.

El emparejamiento de selectores es O(reglas × elementos) en el peor caso, acotado por los límites de streaming. Las dos ordenaciones de la cascada son O(reglas coincidentes · log reglas coincidentes) por elemento. La vía sin capas omite por completo la resolución de capas. El performance_budget por página (wall_ms: 1500, peak_mb: 64) es el límite operativo. Las regresiones están cubiertas por el benchmark de la pipeline de renderizado de HTML (trabajo fusionado, PR #564).

El resolutor solo ve el CSS que admite DefaultHtmlSecurityPolicy::isCssPropertyAllowed(). La lista de permitidos es el límite superior de seguridad, y la tabla de compatibilidad en tiempo de ejecución es un límite superior de capacidad independiente. Una propiedad bloqueada por la política nunca llega a la cascada. Consulta el modelo de seguridad del módulo HTML.

ComportamientoEspecificaciónCláusulareference_id
Ordenación de la cascada: origin/importance → especificidad → orden de apariciónW3C CSS Cascading and Inheritance Level 5§6.4 (css_cascade_5#x1.x7.x1.p21)
Especificidad como una terna (A,B,C) a partir de los recuentos de ID/clase/tipoW3C Selectors Level 4§16 (selectors_4#x1.x16.p2)
Análisis determinista y recuperación ante errores de análisisW3C CSS Syntax Level 3§4 (css_syntax_3#x1.x4.p2)

El material del W3C tiene licencia CC-BY 4.0. Las afirmaciones anteriores están parafraseadas. Se proporcionan identificadores de cláusula y de fragmento para su verificación. NextPDF no declara conformidad total con estos módulos; consulta la matriz de compatibilidad de CSS para ver el estado verificado de cada módulo.

Capacidad Enterprise. Premium amplía el conjunto de propiedades que coinciden y se aplican. El algoritmo de cascada y el modelo de dos pasadas de !important son idénticos en todas las ediciones. Consulta la matriz de compatibilidad de CSS.