Перейти к содержимому

Резолвер CSS: каскад и специфичность

Класс CssResolver сопоставляет селекторы с потоком токенов, упорядочивает совпавшие правила по весу каскадного слоя, специфичности и порядку в документе, а затем на втором проходе применяет !important.

Окно терминала
composer require nextpdf/core:^3

CssResolver — это компонент слоя 1 (согласно ADR-010). Он хранит разобранные правила Cascading Style Sheets (CSS) и определяет, какие объявления применяются к каждому элементу. Этот класс был выделен из HtmlParser, чтобы упростить структуру, и является внутренним, а не публичным application programming interface (API).

Резолверу не требуется дерево документа. При сопоставлении селекторов он читает плоский поток токенов и использует индексные карты, которые HtmlChildScanner строит на этапе 3 конвейера: число дочерних элементов, число элементов с тем же тегом и признак пустоты. По этим картам обрабатываются структурные псевдоклассы. Реляционный селектор :has() использует ограниченное предварительное сканирование, описанное в ограничениях потоковой обработки.

Каскад разрешается в два прохода внутри CssResolver::resolveMatchingProperties(). Проход 1 применяет обычные объявления в каскадном порядке: сначала вес каскадного слоя, затем специфичность, затем порядок в документе. Проход 2 применяет объявления !important по специфичности. Объявление !important переопределяет любое обычное объявление независимо от специфичности. Это разделение на два прохода — стратегия реализации; оно формирует разрешённый набор свойств для слоя макета.

Реализованный в резолвере порядок каскада соответствует спецификации World Wide Web Consortium (W3C) CSS Cascading and Inheritance. Объявления сортируются сначала по источнику и важности, затем по специфичности селектора. При равной специфичности побеждает последнее объявление в порядке документа (CSS Cascade 5 §6.4; см. раздел “Соответствие”). Комментарий в исходном коде CssResolver ссылается на тот же пункт, поэтому это поведение можно проверить третьим способом — наряду со спецификацией и глоссарием.

Специфичность вычисляется как тройка (A, B, C) на основе количества компонентов ID, класса и типа; тройки сравниваются покомпонентно (Selectors Level 4 §16). NextPDF вычисляет специфичность для каждого совпавшего правила перед сортировкой каскада.

Есть важное ограничение. Правило инверсии слоёв §6.4.3 применяется к объявлениям !important между каскадными слоями, и в исходном коде оно зафиксировано как нерешённое в кластере работ по каскадным слоям. Когда каскадные слои объявлены и !important пересекает слои, разрешённый порядок может отличаться от полного поведения по спецификации. Матрица поддержки CSS является авторитетным источником по состоянию поддержки каждой возможности; эта страница не повторяет сведения о поддержке по отдельным свойствам.

СимволРасположениеНазначение
CssResolver::parseStyleBlock(string $css, bool $nestingEnabled = false): voidsrc/Html/CssResolver.phpРазбирает блок <style> на правила.
CssResolver::resolveMatchingProperties(...)src/Html/CssResolver.phpСопоставляет селекторы и разрешает каскад в два прохода.
CssResolver::resolveHasSelectors(array $tokens): arraysrc/Html/CssResolver.phpОграниченное предварительное сканирование :has() (под флагом).
CssResolver::resolveFirstLetterProperties(...)src/Html/CssResolver.phpРазрешает свойства ::first-letter.
CssResolver::resolvePseudoElementProperties(...)src/Html/CssResolver.phpРазрешает свойства ::before / ::after.
CssResolver::getLayerRegistry(): LayerRegistrysrc/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. Объявление типа с !important переопределяет inline-эквивалентное объявление класса, даже если селектор класса имеет более высокую специфичность.

<?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 между слоями. В исходном коде правило инверсии слоёв §6.4.3 для important-объявлений зафиксировано как нерешённое. Проверьте поведение по матрице поддержки CSS, прежде чем полагаться на него.
  • Отсутствие объявленных слоёв — это быстрый путь. Без @layer упорядочивание сводится к поведению, основанному только на специфичности, и побитово идентично поведению до появления слоёв.
  • :has() работает под флагом. Реляционное предварительное сканирование выполняется только при включённой экспериментальной возможности css.has.
  • Сопоставление селекторов основано на потоке. Структурные селекторы используют индексные карты, а не обход дерева. Селектор, которому потребовалась бы произвольная навигация по дереву за пределами индексных карт, в этой модели нельзя разрешить.

В худшем случае сопоставление селекторов имеет сложность O(правила × элементы), ограниченную лимитами потоковой обработки. Две сортировки каскада имеют сложность O(совпавшие правила · log совпавших правил) на элемент. Путь без слоёв полностью пропускает разрешение слоёв. Постраничный performance_budget (wall_ms: 1500, peak_mb: 64) задаёт эксплуатационный потолок. Бенчмарк конвейера отрисовки HTML защищает от регрессий (работа вмёржена, PR #564).

Резолвер видит только тот CSS, который пропускает DefaultHtmlSecurityPolicy::isCssPropertyAllowed(). Список разрешений задаёт предел по безопасности, а таблица поддержки во время выполнения — отдельный предел возможностей. Свойство, заблокированное политикой, никогда не попадает в каскад. См. модель безопасности модуля HTML.

ПоведениеСпецификацияПунктИдентификатор (reference_id)
Сортировка каскада: origin/importance → специфичность → порядок появленияСпецификация W3C CSS Cascading and Inheritance Level 5§6.4 (css_cascade_5#x1.x7.x1.p21)
Специфичность как тройка (A,B,C) на основе количества ID/классов/типов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 расширяет набор сопоставляемых и применяемых свойств. Алгоритм каскада и двухпроходная модель !important идентичны во всех редакциях. См. матрицу поддержки CSS.