HTML 渲染管线
writeHtml() 通过单次向前遍历完成:tokenize、resolve(解析) @page 与样式、布局,并绘制 PDF 运算符。各阶段之间不会保留任何元素树。
composer require nextpdf/core:^3概念总览
标题为“概念总览”的章节HTML 渲染管线通过单次向前遍历,将 HTML+CSS 转换为 PDF 内容流(content stream)运算符。它不会构建持久化的文档树。以下阶段顺序反映了 HtmlParser::parse() 在 main 上的实现。
阶段 1 — 清理与规范化。 HtmlParser::parse() 会拒绝超过 10 MB 的输入、移除控制字符,并规范化换行:CRLF 与单独的 CR 都会变成 LF,遵循 HTML 对源文本的换行规范化方式。随后它会重置每个实例字段,因此不会有任何状态从先前调用中残留。
阶段 2 — 提取 @page 与样式块。 parser(解析器)会先提取 <style> 块,再应用发现的 @page 规则来重新配置页面几何。这一步发生在处理任何 token 之前,因为页面尺寸会影响后续每一个布局决策。
阶段 3 — Tokenize。 HtmlTokenizer::cleanHtml() 会规范化空白,同时保留 <pre> 内容。随后 tokenize() 会生成一个扁平的 list<HtmlToken>。这是一份 token 列表,而不是节点图。仅包含空白的文本 token 会立即被丢弃。HtmlChildScanner::scan() 会基于这份扁平列表构建 Index(索引)映射(子节点数、标签数、是否为空),让结构性选择器无需树也能运行。
阶段 4 — 可选的 :has() 预扫描。 当启用 css.has 实验性功能时,CssResolver::resolveHasSelectors() 会对 token 列表执行一次有界预扫描,用于解析这个关系型选择器。这是一个已记录且有界的单遍历规则例外。
阶段 5 — 处理 token(样式、布局、绘制)。 HtmlParser::processTokens() 会遍历 token 列表一次。它会为每个元素解析层叠(cascade)(第 1 层应用器会写入 HtmlStyleState)、计算几何(第 3 层布局),并输出 PDF 运算符(第 4 层绘制)。样式继承使用一个 push-and-pop 的 HtmlStyleState 栈。光标(x、y、边距、流偏移量)会通过 HtmlBlockCursor 快照在各处理程序之间传递。
阶段 6 — 返回结果。 parse() 会返回一个不可变的 HtmlRenderResult,携带输出内容流、结束时的光标位置以及使用到的字体键。调用方(writeHtml())会把光标交还给页面坐标 Framework(框架)。
层契约 页面说明阶段 5 内部执行的四层分离。流式限制 页面说明不保留树的特性及其上限。
API 接口
标题为“API 接口”的章节| 符号 | 位置 | 阶段 |
|---|---|---|
Document::writeHtml(string $html): static | src/Core/Concerns/HasTextOutput.php | 公开入口 |
HtmlParser::parse(string $html): HtmlRenderResult | src/Html/HtmlParser.php | 统筹所有阶段 |
HtmlTokenizer::cleanHtml() / tokenize() | src/Html/HtmlTokenizer.php | 阶段 3 |
HtmlChildScanner::scan() | src/Html/HtmlChildScanner.php | 阶段 3 索引映射 |
CssResolver::resolveHasSelectors() | src/Html/CssResolver.php | 阶段 4(受功能标志控制) |
HtmlRenderResult (stream, endX, endY, usedFontKeys) | src/Html/HtmlRenderResult.php | 阶段 6 |
代码示例 — 快速上手
标题为“代码示例 — 快速上手”的章节取自 examples/08-html-basic.php。
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();$doc->setTitle('HTML Basic');$doc->addPage();$doc->writeHtml('<h1 style="color:#1E3A8A;">HTML Rendering</h1><p>One pass.</p>');$doc->save(__DIR__ . '/output/08-html-basic.pdf');代码示例 — 生产环境
标题为“代码示例 — 生产环境”的章节渲染一份带有内嵌 <style> 块的样式化报表。管线会先提取并应用样式块,然后才处理任何 token。
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Exception\HtmlParsingException;
function renderInvoice(string $bodyHtml, string $out): void{ $doc = Document::createStandalone(); $doc->setTitle('Invoice'); $doc->addPage();
$html = '<style>@page { margin: 20mm; } ' . 'h1 { color: #1E3A8A; } ' . 'table { width: 100%; }</style>' . $bodyHtml;
try { $doc->writeHtml($html); } catch (HtmlParsingException $e) { // Sanitize/cap failures surface here. Do not retry. throw $e; }
$doc->save($out);}边界情况与陷阱
标题为“边界情况与陷阱”的章节@page会在 token 之前读取。 即使@page规则放在内容之后也会生效,因为样式提取先于 tokenize。页面几何会在阶段 5 之前就固定下来。<pre>的空白会被保留。cleanHtml()会保护<pre>内容;其他位置的空白会被折叠。:has()受功能标志控制。 若未启用css.has实验性功能,阶段 4 不会执行,:has()选择器也不会匹配成功。- 单一流缓冲区。 管线会写入单一字符串缓冲区。已写入的内容绝不会被移动,也不会有重新布局。
- 上限会在遍历途中生效。 元素数与嵌套深度的上限会在阶段 5 期间触发异常,而不是在此之前。文档可能会在中途失败。
管线的遍历为 O(token 数)。表格列宽计算会额外增加一次有界、以表格为单位的逐行扫描(阶段 5,TableParser)。启用时,:has() 预扫描会额外增加一次有界的 token 列表遍历(阶段 4)。就样式栈而言,内存用量是 O(嵌套深度) 而非 O(元素数)——详见 流式限制。HTML 渲染管线的性能基准测试以 5% 的 gate 防止退化(已合并,PR #564)。本页的 performance_budget(wall_ms: 1500、peak_mb: 64)是其运行上限。
安全性说明
标题为“安全性说明”的章节阶段 1 是第一道安全边界:10 MB 输入上限、控制字符移除以及换行规范化都在 tokenize 之前执行。随后,DefaultHtmlSecurityPolicy 会在阶段 5 期间管控允许的标签、属性、CSS 属性与 URL scheme。请参阅 HTML 模块安全模型一节。
符合性
标题为“符合性”的章节换行规范化遵循 HTML 标准的换行处理(CRLF 与单独的 CR 都会变成 LF)。逐属性的 CSS 符合性记载于 CSS 支持对照表,层叠行为则记载于 css-resolver一节。本页不重述逐属性支持情况。
商业情境
标题为“商业情境”的章节企业版能力。 Premium 在同一条管线上扩大 CSS 覆盖范围。这六个阶段的顺序在各版本之间都不会改变。请参阅 CSS 支持对照表一节。