コンテンツにスキップ

HTML シングルパス・ストリーミングの制約(ADR-001)

NextPDF は HTML を 1 回の前方パスでレンダリングし、要素ツリーをメモリ内に保持しません。ADR-001 は、この決定と、それによってすべての CSS 機能に課される制約を記録しています。

Terminal window
composer require nextpdf/core:^3

HTML サブシステムは、HTML+CSS を PDF へ変換する、シングルパスのストリーミング型レンダラーです。ADR-001(「Stream-based Rendering Pipeline Retention」、2026-04-06 に承認)は、このモデルを確定するアーキテクチャ決定です。このページでは、このモデルが何であり、何を行わず、コントリビューターにどのような制約を課すのかを説明します。

ストリーミングモデルでは、トークナイザー(HtmlTokenizer)が入力を 1 回読み取り、フラットなトークンリストを生成します。HtmlParser::processTokens() は、そのリストを左から右へたどります。各要素に到達するたびに、PDF のコンテンツストリーム演算子を文字列バッファーに書き込みます。エンジンは、呼び出しをまたいで永続する要素グラフを構築しません。ハンドラー呼び出しをまたいで保持する必要がある状態は、共有ノードではなく、スナップショット化された値オブジェクト(HtmlBlockCursor)を介して受け渡されます。スタイル継承には、親ポインターによるツリーではなく、フラットな HtmlStyleState インスタンスをプッシュ/ポップするスタックを使用します。

これは、ドキュメントを保持するモデルではありません。エンジンはドキュメントツリーを保持せず、すでに書き込んだコンテンツを再レイアウトせず、解析開始後の入力変更を許可しません。境界は明確です。NextPDF は最初から最後までストリーミングで処理します。保持型のレンダラーは、まずドキュメント全体をメモリ内に構築しますが、NextPDF はそれを行いません。

2 つの操作では限定的な先読みが必要であり、どちらも明示的で境界の定まった例外です。テーブルの列幅を決定する際は、セルを配置する前にすべての行をスキャンします。これらの行は TableParser 内の一時的なテーブルバッファーにバッファリングされます。これは ADR-001 が名指しで認めている例外です。:has() 関係セレクター、ならびに :last-child および :last-of-type セレクターは、ツリー走査ではなく、フラットなトークンリストに対する境界付きの事前スキャンを使用します。ADR-001 はこれら両方の例外を記録し、その境界を定めています。

このモデルはワーカーセーフです。HtmlParser はリクエストごとに 1 回構築され、シングルトンとして構築されることは決してありません。HtmlParser::parse() は、各呼び出しの開始時にすべてのフィールドをリセットします。レンダリングパスには静的な可変状態が存在しないため、RoadRunner、Swoole、Laravel Octane は、ドキュメント間で状態を漏らすことなくプロセスを再利用できます。

次のシンボルが、以下の制約を強制します。それぞれを src/Html/ と照合して確認してください。

シンボル場所役割
HtmlParser::parse(string $html): HtmlRenderResultsrc/Html/HtmlParser.phpエントリーポイント。すべての状態をリセットした後にシングルパスを実行。
HtmlParser::MAX_ELEMENT_COUNT50_000src/Html/HtmlParser.php処理する要素数のハードキャップ。
HtmlParser::MAX_NESTING_DEPTH100src/Html/HtmlParser.phpネスト深さのハードキャップ。
HtmlBlockCursorsrc/Html/HtmlBlockCursor.phpカーソルのスナップショット。唯一の共有状態メカニズム。
HtmlStyleStatesrc/Html/HtmlStyleState.phpスタックにプッシュされるスタイルフレーム。親ポインターなし。
TableParser::reset()src/Html/TableParser.phpテーブル間での一時的なテーブルバッファーの必須リセット。

ストリーミングモデルは呼び出し側には見えません。1 回の呼び出しで、サポート対象の任意のドキュメントをレンダリングします。

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();
$doc->setTitle('Streaming render');
$doc->addPage();
$doc->writeHtml('<h1>One forward pass</h1><p>No retained tree.</p>');
$doc->save(__DIR__ . '/output/streaming.pdf');

固定されたメモリ予算の下で大きなドキュメントをレンダリングします。要素数の上限が安全境界です。呼び出し前に入力サイズを見積もってください。

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;
use NextPDF\Exception\HtmlParsingException;
/**
* Render trusted HTML, surfacing the streaming-model limits as typed errors.
*
* @param non-empty-string $html
*/
function renderReport(string $html, string $out): void
{
$doc = Document::createStandalone();
$doc->addPage();
try {
$doc->writeHtml($html);
} catch (HtmlParsingException $e) {
// Thrown on the 10 MB input cap, the 50,000-element cap,
// or the 100-level nesting cap. These are model boundaries,
// not transient faults — do not retry.
throw $e;
}
$doc->save($out);
}
  • 要素数の上限はハードストップです。 上限は MAX_ELEMENT_COUNT = 50_000 で、到達するとエンジンは HtmlParsingException をスローします。非常に大きなレポートは、複数の writeHtml() 呼び出しまたは複数のドキュメントに分割してください。
  • ネスト深さの上限はハードストップです。 MAX_NESTING_DEPTH = 100 を超える深さは例外をスローします。深くネストしたラッパーが典型的な原因です。
  • 入力サイズの上限。 HtmlParser::parse() は、トークン化の前に 10 MB を超える入力を拒否します。
  • :has() はゲートされています。 :has() の事前スキャンは、css.has 実験的機能が有効な場合にのみ実行されます。有効でない場合、:has() セレクターはマッチしません。
  • テーブルのバッファリングは唯一の一時的なツリーです。 非常に幅が広い、または非常に縦に長い単一のテーブルは、render() まで行をメモリ内に保持します。TableParser はこのバッファーをテーブルごとに制限し、テーブル間でリセットします。これはドキュメント全体にわたるツリーではありません。
  • 再レイアウトはありません。 すでに書き込まれたコンテンツが移動されることは決してありません。後から現れたスタイルが、それ以前の出力をさかのぼって変更することはできません。

ストリーミングモデルは、ネストレベルごとに最大 1 つの HtmlStyleState を保持し(MAX_NESTING_DEPTH = 100 によって制限されます)、加えてアクティブなカーソルフィールドを保持します。スタイル状態とカーソルが使うメモリは O(element count) ではなく O(depth) です。ADR-001 は、同じ入力で保持型のオブジェクトグラフを十分に下回る状態を保つという設計意図を記録しています。制御された 50,000 要素のピーク RSS ベンチマークは、ADR-001 で名指しされている経験的な検証目標です。これは、HTML レンダリングパイプラインのパフォーマンスベンチマークとその 5% リグレッションゲート(マージ済みの作業、PR #564)を通じて追跡されます。ページごとの performance_budgetwall_ms: 1500peak_mb: 64)を運用上の上限として扱ってください。

このページで示す上限は、サービス拒否(DoS)対策でもあります。DefaultHtmlSecurityPolicy は、パーサーとは独立して 10 MB の入力上限と 100 レベルのネスト上限を強制するため、悪意のあるドキュメントが深さやサイズによってメモリを枯渇させることはできません。ストリーミングモデル自体も、構造上メモリに上限を設けています。攻撃者が膨張させられる要素グラフは存在しません。ポリシーサーフェス全体については、HTML モジュールのセキュリティモデル およびレイヤー契約を参照してください。

このページは外部標準を引用していません。これらの制約は、ADR-001 と、API サーフェスに記載された強制を担うソースシンボルに由来します。動作面での CSS 仕様マッピングは、ここではなく css-resolver に記載されています。

Enterprise の機能。 ストリーミングアーキテクチャは Core と Premium で同一です。Premium は CSS のカバレッジを広げますが、シングルパスモデルを変更したり、これらの上限を緩和したりするものではありません。CSS サポートマトリクスを参照してください。