跳转到内容

HTML 引擎分层契约(ADR-010)

HTML 子系统将 CSS 解析、样式状态、布局与绘制拆分为四层,各层之间的契约按单向流动。 ADR-010 规定了这些边界和扩展规则。

Terminal window
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 进行的两次有界重构,把代码移到正确的层。 PageBorderPainterHtmlParser 中抽离出来,让绘制操作符不再留在协调器里。 HtmlStyleState 类的 docblock 现在带有正式的分层契约,说明每一层可写入或读取哪些字段。

有一道边界是明文写出的,而不是隐藏起来的。 FormattingContextFactory::startTable() 仍然直接读取五个原始 CSS 键。 ADR-010 将这一点记为已知且延期处理的技术债,留给未来的 TableApplicator,并没有把它当作预期中的契约。 把这项例外记录下来,本身就是契约的一部分。

文件(代表性)写入读取不可
1 — CSS 解析与应用器CssValueParserCssResolverHtmlCssApplicatorsrc/Html/Applicator/*HtmlStyleState 的 CSS 字段原始 CSS 文本计算几何;输出操作符
2 — 样式状态HtmlStyleStateState/ComputedStyleState/LayoutState——(被动的值容器)解析 CSS;决定布局;输出操作符
3 — 布局与排版FormattingContextFactoryHtmlBlockHandlerFlexLayoutEngineTableParserFloatContext游标几何HtmlStyleState 字段读取原始 $css[...];输出绘制操作符
4 — 绘制与渲染BorderRendererBackgroundImageRenderersrc/Html/Paint/*src/Html/Gradient/*PDF 操作符流ComputedStyle(不可变)+ 几何计算几何;解析 CSS;决定分页
文件(代表性)角色
5 — 分页媒体PageBreakControllerPageBorderPainterPageRulePageRuleParserParserConfiguratorresolve(解析)@page 规则;评估分页与 orphan/widow 限制;把页面装饰委派给绘制层。
6 — 测量与载体WPT 分类器脚本、tests/Support/*分类测试结果;生成回归快照;提供断言辅助工具。 不包含任何绘制逻辑。

这份契约通过类的放置位置以及 HtmlStyleState 的 docblock 来落实。 请对照 src/Html/ 进行检查。

符号契约角色
PropertyApplicatorInterface1策略接口;唯一写入 CSS 计算字段的位置。
ParserConfigurator::buildCssApplicator()1(接线)注册每一个应用器。 新的 CSS 属性在这里注册。
HtmlStyleState2双分组容器;类的 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_budgetwall_ms: 1500peak_mb: 64)是运行层面的上限。

分层分离支撑起整个安全模型。 Layer 1 会在任何布局或绘制代码看到 CSS 值之前,先解析这些值并通过策略过滤,因此 DefaultHtmlSecurityPolicy::isCssPropertyAllowed() 就是那道唯一的 gate。 绘制层永远不会读取由攻击者控制的原始 CSS。 请参阅 HTML 模块安全模型

本页未引用任何外部标准。 分层边界源自 ADR-010,以及把契约写入源码的 HtmlStyleState 类 docblock。 CSS 的行为符合性记录于 css-resolver 一节。

Enterprise 功能。 Premium 的 CSS 功能通过同一套有文档记录的扩展点,扩展同样的四层。 并不存在独立的 Premium 管线。 请参阅 CSS 支持矩阵