跳到內容

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 支援矩陣