HTML 引擎分层契约(ADR-010)
HTML 子系统将 CSS 解析、样式状态、布局与绘制拆分为四层,各层之间的契约按单向流动。 ADR-010 规定了这些边界和扩展规则。
composer require nextpdf/core:^3概念总览
标题为“概念总览”的章节ADR-010(《引擎分层契约、热路径归属与扩展规则》,于 2026-04-12 通过)正式定义了 HTML 子系统的分层方式。 核心绘制契约有四层:CSS 解析与应用器、样式状态、布局与排版,以及绘制。 ADR-010 还记录了两个辅助层——分页媒体与测量载体——它们包覆四层核心,但不改变数据流向。 核心标准术语表词条是「HTML pipeline」,即一条四层管线。
数据是单向流动的。 CSS 文本在 Layer 1 变成带类型的值。 Layer 1 把这些值写入 Layer 2 的 HtmlStyleState 字段。 Layer 3 读取样式状态字段并计算几何。 Layer 4 读取不可变的 ComputedStyle 快照和几何信息,并输出 PDF 操作符。 任何一层都不会读取排在它后面的层。
这种四层分离不只是文档里的说法。 ADR-010 记录了在 v1.2.0 进行的两次有界重构,把代码移到正确的层。 PageBorderPainter 从 HtmlParser 中抽离出来,让绘制操作符不再留在协调器里。 HtmlStyleState 类的 docblock 现在带有正式的分层契约,说明每一层可写入或读取哪些字段。
有一道边界是明文写出的,而不是隐藏起来的。 FormattingContextFactory::startTable() 仍然直接读取五个原始 CSS 键。 ADR-010 将这一点记为已知且延期处理的技术债,留给未来的 TableApplicator,并没有把它当作预期中的契约。 把这项例外记录下来,本身就是契约的一部分。
四个核心层
标题为“四个核心层”的章节| 层 | 文件(代表性) | 写入 | 读取 | 不可 |
|---|---|---|---|---|
| 1 — CSS 解析与应用器 | CssValueParser、CssResolver、HtmlCssApplicator、src/Html/Applicator/* | HtmlStyleState 的 CSS 字段 | 原始 CSS 文本 | 计算几何;输出操作符 |
| 2 — 样式状态 | HtmlStyleState、State/ComputedStyle、State/LayoutState | ——(被动的值容器) | — | 解析 CSS;决定布局;输出操作符 |
| 3 — 布局与排版 | FormattingContextFactory、HtmlBlockHandler、FlexLayoutEngine、TableParser、FloatContext | 游标几何 | HtmlStyleState 字段 | 读取原始 $css[...];输出绘制操作符 |
| 4 — 绘制与渲染 | BorderRenderer、BackgroundImageRenderer、src/Html/Paint/*、src/Html/Gradient/* | PDF 操作符流 | ComputedStyle(不可变)+ 几何 | 计算几何;解析 CSS;决定分页 |
两个辅助层
标题为“两个辅助层”的章节| 层 | 文件(代表性) | 角色 |
|---|---|---|
| 5 — 分页媒体 | PageBreakController、PageBorderPainter、PageRule、PageRuleParser、ParserConfigurator | resolve(解析)@page 规则;评估分页与 orphan/widow 限制;把页面装饰委派给绘制层。 |
| 6 — 测量与载体 | WPT 分类器脚本、tests/Support/* | 分类测试结果;生成回归快照;提供断言辅助工具。 不包含任何绘制逻辑。 |
API 接口
标题为“API 接口”的章节这份契约通过类的放置位置以及 HtmlStyleState 的 docblock 来落实。 请对照 src/Html/ 进行检查。
| 符号 | 层 | 契约角色 |
|---|---|---|
PropertyApplicatorInterface | 1 | 策略接口;唯一写入 CSS 计算字段的位置。 |
ParserConfigurator::buildCssApplicator() | 1(接线) | 注册每一个应用器。 新的 CSS 属性在这里注册。 |
HtmlStyleState | 2 | 双分组容器;类的 docblock 说明每个字段归属的层。 |
HtmlStyleState::toComputedStyle() | 2 | 为绘制层生成不可变的 ComputedStyle。 |
FormattingContextFactory::dispatchOpenTag() | 3 | 新布局行为的单一路由点。 |
PageBorderPainter::buildStream() | 4 | 页面装饰;由 Layer 5 调用,而不是内嵌在 HtmlParser 中。 |
代码示例——快速开始
标题为“代码示例——快速开始”的章节调用方永远不会直接接触这些层。 四层流程在单次调用内完成。
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();$doc->addPage();$doc->writeHtml('<p style="color:#1E3A8A;border:1px solid #999;">Layered render.</p>');$doc->save(__DIR__ . '/output/layers.pdf');代码示例——正式环境
标题为“代码示例——正式环境”的章节这份契约对贡献者很重要,对调用方则不然。 要新增一个 CSS 属性,请遵循 Layer 1 的扩展点:创建一个应用器、添加一个带有分层 docblock 的类型化 HtmlStyleState 字段,并在 ParserConfigurator 中注册该应用器。 下面的示例展示了应用器契约的形态。 请参考 src/Html/Applicator/ 中可直接复制的具体类。
<?php
declare(strict_types=1);
// Layer 1 extension contract (see ADR-010 §C "New CSS property").// A new property group ships as a PropertyApplicatorInterface// implementation registered in ParserConfigurator::buildCssApplicator().// It writes a typed HtmlStyleState field and never computes geometry// or emits PDF operators — those belong to Layers 3 and 4.边界情况与陷阱
标题为“边界情况与陷阱”的章节FormattingContextFactory::startTable()读取原始 CSS。 这是唯一一个有文档记录的契约例外,延后到未来的TableApplicator处理。 请勿仿照这种模式。- 六层,四层核心。 ADR-010 把层编号为六层。 数据流契约是这四层核心;分页媒体与测量则是辅助层。
HtmlStyleState是双分组的。 它同时带有 CSS 计算字段与布局跟踪字段。 只有应用器会写入 CSS 分组。 绘制层读取ComputedStyle,绝不读取布局跟踪字段。HtmlParser不属于任何一层。 它是协调器。 CSS 解析、几何运算与绘制输出都不可放在它里面。
分层契约是结构性的,不会增加任何运行时成本。 HtmlStyleState::toComputedStyle() 为每个需要绘制的元素生成一份不可变快照。 这份快照让绘制代码不必读取可变的状态容器。 绘制成本由 流式模型 管控,而不是由分层决定。 每页的 performance_budget(wall_ms: 1500、peak_mb: 64)是运行层面的上限。
安全性注意事项
标题为“安全性注意事项”的章节分层分离支撑起整个安全模型。 Layer 1 会在任何布局或绘制代码看到 CSS 值之前,先解析这些值并通过策略过滤,因此 DefaultHtmlSecurityPolicy::isCssPropertyAllowed() 就是那道唯一的 gate。 绘制层永远不会读取由攻击者控制的原始 CSS。 请参阅 HTML 模块安全模型。
符合性
标题为“符合性”的章节本页未引用任何外部标准。 分层边界源自 ADR-010,以及把契约写入源码的 HtmlStyleState 类 docblock。 CSS 的行为符合性记录于 css-resolver 一节。
商业情境
标题为“商业情境”的章节Enterprise 功能。 Premium 的 CSS 功能通过同一套有文档记录的扩展点,扩展同样的四层。 并不存在独立的 Premium 管线。 请参阅 CSS 支持矩阵。