Резолвер 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 является авторитетным источником по состоянию поддержки каждой возможности; эта страница не повторяет сведения о поддержке по отдельным свойствам.
Поверхность API
Заголовок раздела «Поверхность API»| Символ | Расположение | Назначение |
|---|---|---|
CssResolver::parseStyleBlock(string $css, bool $nestingEnabled = false): void | src/Html/CssResolver.php | Разбирает блок <style> на правила. |
CssResolver::resolveMatchingProperties(...) | src/Html/CssResolver.php | Сопоставляет селекторы и разрешает каскад в два прохода. |
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. Объявление типа с !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.