Dev Study
← Rust で小さなブラウザを作る コース

15. 挿入モードと開いている要素のスタック — トークンを木に置く場所の決め方

は、を 1 つ受け取るたびに「今のの子として置くか、今の要素を閉じるか、無視するか」を決めます。その判断の土台が 2 つのです。(insertion mode)は HTML Standard が「構文解析の主要な動作を制御する状態変数」と定めるもので、仕様には 23 個、この実装には 9 個あります。(stack of open elements)は「最初は空。最後に追加したものが一番下(= 今の要素)」で、開始タグで積み、終了タグで下ろします。

モードは文書の位置に対応します。Initial(何も無い)→ BeforeHtml(<html> を待つ)→ BeforeHead → InHead(<title> や <style> が入る)→ AfterHead → InBody()→ AfterBody → AfterAfterBody。InHead で <style> が来ると Text モードに入り、終了タグまで文字を集めてから InHead に戻ります。モードの切り替えは、来たトークンが「このモードで予期しないもの」だったときに起きるのが基本です。

参照実装の step() は self.mode { ... } の 1 つの関数で、各モードの腕に仕様の節番号(13.2.6.4.1 など)を書いてあります。戻り値の bool は「同じトークンを新しいモードでもう一度処理するか」(仕様の reprocess)です。たとえば BeforeHtml で <p> が来たら、暗黙の <html> を作って BeforeHead に移り、<p> を reprocess します。BeforeHead でも同じ要領で <head> を作り、InHead で「head に置けないものが来た」として AfterHead へ、そこで <body> を作って InBody へ。<p> 1 つから html / head / body の 3 つが生まれる仕組みがこれです。

文字は 1 つずつ来るので、text_buffer に溜めておき、文字以外のトークンが来た時点で にして今の要素に付けます(flush_text)。下のコードを実行すると、各入力について「挿入モードの遷移の記録」と、できあがった が出ます。遷移の記録はこのコース用に足した学習用の機能で、本物にはありません。

本物のブラウザでは: の木構築は HTMLTreeBuilder(html_tree_builder.cc)が挿入モードを切り替え、開いている要素のスタックは HTMLElementStack、実際にを木へ差すのは HTMLConstructionSite です。Firefox の nsHtml5TreeBuilder.cpp も TreeBuilder.java からの生成物です。この回の 9 モードは仕様 23 モードの一部で、本物は全部を持ちます。もう 1 つの差は で、Blink の TreeScope は id → 要素の対応表(TreeOrderedMap)を持っていて木を歩きません。この実装の find_all による全走査は、正しいが遅い版です。

開発のどこに活かすか: 「モード(今どの文脈か)」と「スタック(開いているものの入れ子)」の 2 つで入れ子構造を組み立てる形は、括弧の対応、XML / JSON の構築、インデントで階層を表す言語(YAML、Python)、UI のモーダルや画面遷移の履歴に共通します。HTML の構文解析はその中で最も「壊れた入力を直す規則」が厚い例で、次のレッスンでその部分を見ます。

点線の付いた語はタップすると説明が出ます。用語集を開く →

① <p>x</p> だけの入力が 9 モードをどう通るか

矢印のラベルは「来たトークン」。括弧は副作用。reprocess は同じトークンを次のモードでもう一度
Initial
<p>: BeforeHtml へ、reprocess
BeforeHtml
<p>: (暗黙の <html> を作って積む) BeforeHead へ、reprocess
BeforeHead
<p>: (暗黙の <head> を作って積む) InHead へ、reprocess
InHead
<p>: (head を下ろす) AfterHead へ、reprocess
AfterHead
<p>: (暗黙の <body> を作って積む) InBody へ、reprocess
InBody<p> を積む → 'x' を溜める → </p> で p を下ろす
EOF: 溜めた文字を流して終わり
#document > html > (head, body > p > "x")

スタックは html → head → (下ろす) → body → p → (下ろす) と動きます。「今の要素」は常にスタックの一番下です。

▶ サンプルコードを実行してみよう(自由に書き換えてOK)

Rust

手元で動かす(配布物)

図やコードに出てくる用語(タップで説明)

サンプルコードで身につける(3本) →

学んだ概念を、素朴な例から実務寄りの例まで3つの具体例で確認します。

公式ドキュメントで詳しく ↗