[インデックス 10076] ファイルの概要
このコミットは、Go言語のHTMLパーサーにおける重要な改善を導入しています。特に、HTML5の仕様で定義されている「フォスターペアレンティング (foster parenting)」アルゴリズムの実装と、アクティブフォーマット要素の再構築に関するバグ修正が主な内容です。これにより、テーブル内に不適切に配置された要素の処理が改善され、より堅牢なHTMLパースが可能になります。
コミット
commit 2aa589c843debaef249e7fbcd9dd3fa0546c9c8
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Sun Oct 23 18:36:01 2011 +1100
html: implement foster parenting
Implement the foster-parenting algorithm for content that is inside a table
but not in a cell.
Also fix a bug in reconstructing the active formatting elements.
Pass test 30 in tests1.dat:
<a><table><td><a><table></table><a></tr><a></table><b>X</b>C<a>Y
R=nigeltao
CC=golang-dev
https://golang.org/cl/5309052
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2aa589c843debaef249e7fbcd9dd3fa0546c9c8
元コミット内容
このコミットは、Go言語のhtml
パッケージにおいて、以下の2つの主要な変更を導入しています。
- フォスターペアレンティング (foster parenting) の実装: テーブル要素(
<table>
、<tbody>
、<thead>
、<tfoot>
、<tr>
)の内部に、セル(<td>
、<th>
)ではない要素が誤って配置された場合の処理を、HTML5の仕様に従って実装します。これにより、不正なマークアップに対するパーサーの挙動がより予測可能で、標準に準拠したものになります。 - アクティブフォーマット要素の再構築に関するバグ修正: パーサーがアクティブフォーマット要素のスタックを再構築する際に発生していたバグを修正します。これにより、HTMLドキュメントの解析中に適用されるテキストフォーマット(例:
<b>
,<i>
)が正しく処理されるようになります。
この変更は、tests1.dat
のテストケース30、具体的には<a><table><td><a><table></table><a></tr><a></table><b>X</b>C<a>Y
という複雑で不正なHTMLマークアップを正しくパースできるようになることを目的としています。
変更の背景
HTMLは非常に寛容な言語であり、ブラウザは不正なマークアップに対しても可能な限りレンダリングを試みます。この「エラー回復」の挙動は、HTML5の仕様で詳細に定義されており、すべてのHTMLパーサーはこれに準拠することが求められます。
このコミットの背景には、特にテーブル要素のパースにおける複雑なルールがあります。HTMLのテーブル構造は厳格であり、<table>
タグの直下に<td>
や<th>
のようなセル要素以外の要素が来ることは通常ありません。しかし、開発者が誤ってそのようなマークアップを記述した場合、ブラウザはそれを無視するのではなく、特定のルールに従ってDOMツリーの別の場所に「移動」させて処理します。このルールが「フォスターペアレンティング」です。
Go言語のhtml
パッケージは、HTML5の仕様に準拠したパーサーを提供することを目指しており、このコミットは、その準拠度を高めるための重要なステップでした。特に、不正なテーブル構造に対する堅牢性を向上させることで、より多くの種類のHTMLドキュメントを正確にパースできるようになります。
また、「アクティブフォーマット要素」の処理は、HTMLのネストされたフォーマットタグ(例: <b><b>text</b></b>
)を正しく扱うために不可欠です。この部分にバグがあると、テキストの表示が意図しないものになる可能性があります。このコミットは、そのバグを修正し、パーサーの正確性を向上させています。
前提知識の解説
HTML5パーシングアルゴリズム
HTML5のパーシングアルゴリズムは、非常に詳細かつ複雑なステートマシンとして定義されています。これは、ブラウザがどのようにHTMLドキュメントを読み込み、DOMツリーを構築するかを厳密に規定しています。主な概念には以下があります。
- トークナイゼーション (Tokenization): 入力ストリームをタグ、属性、テキストなどのトークンに分解するプロセス。
- ツリー構築 (Tree Construction): トークンを受け取り、DOMツリーを構築するプロセス。このプロセスは、現在の「挿入モード (insertion mode)」に基づいて動作します。
- 挿入モード (Insertion Mode): パーサーが現在どのHTML要素のコンテキストで動作しているかを示す状態。例えば、
in body
モード、in table
モードなどがあります。各モードには、特定のトークンが来た場合の処理ルールが定義されています。 - スタック (Stack of Open Elements): 現在開いている要素(まだ終了タグが来ていない要素)を追跡するためのスタック。DOMツリーの階層構造を管理するために使用されます。
- アクティブフォーマット要素 (List of Active Formatting Elements):
<b>
,<i>
,<a>
などのフォーマット要素が、開始タグが来たときにこのリストに追加され、対応する終了タグが来たときに削除されます。これにより、ネストされたフォーマット要素や、不正にネストされたフォーマット要素の処理が複雑なルールに基づいて行われます。
フォスターペアレンティング (Foster Parenting)
フォスターペアレンティングは、HTML5パーシングアルゴリズムの重要なエラー回復メカニズムの一つです。これは、特定の要素(特にテーブル関連の要素)が、本来あるべきではない場所に挿入されようとした場合に適用されます。
具体的には、<table>
、<tbody>
、<thead>
、<tfoot>
、<tr>
といったテーブル関連の要素の内部に、<td>
や<th>
のようなセル要素ではないコンテンツ(例: テキスト、<div>
、<img>
など)が直接挿入されようとした場合に発動します。
このアルゴリズムが発動すると、パーサーは以下のいずれかの場所を「フォスターペアレント (foster parent)」として探し、その要素をそこに挿入します。
- テーブルの直前の兄弟要素: テーブルの直前に兄弟要素が存在し、それがテキストノードでない場合、その要素の末尾に挿入されます。
- テーブルの親要素: テーブルの親要素の末尾に挿入されます。
<html>
要素: 上記のいずれも見つからない場合、最終的に<html>
要素の末尾に挿入されます。
このメカニズムにより、不正なマークアップであってもコンテンツが失われることなく、DOMツリーの論理的に適切な場所に配置されることが保証されます。
アクティブフォーマット要素の再構築 (Reconstructing the List of Active Formatting Elements)
アクティブフォーマット要素のリストは、HTMLパーサーがテキストのフォーマット(太字、斜体、リンクなど)を正しく適用するために使用する重要なデータ構造です。このリストは、開始タグが来たときに要素を追加し、終了タグが来たときに要素を削除します。
しかし、HTMLのマークアップが不正な場合(例: <b><i>テキスト</b></i>
のようにタグが正しくネストされていない場合)、このリストの整合性が失われる可能性があります。HTML5の仕様では、このような状況でリストを「再構築」するための複雑なアルゴリズムが定義されています。これは、パーサーがDOMツリーを構築する際に、フォーマット要素の正しい適用範囲を決定するために行われます。
この再構築プロセスでは、リスト内の要素がDOMツリーに正しく反映されているかを確認し、必要に応じて新しい要素をDOMツリーに追加したり、既存の要素を再利用したりします。
技術的詳細
このコミットは、src/pkg/html/parse.go
とsrc/pkg/html/parse_test.go
の2つのファイルを変更しています。
src/pkg/html/parse.go
の変更点
-
parser
構造体へのfosterParenting
フィールドの追加:type parser struct { // ... // fosterParenting is whether new elements should be inserted according to // the foster parenting rules (section 11.2.5.3). fosterParenting bool }
このブール値のフラグは、現在フォスターペアレンティングモードが有効になっているかどうかを示します。
-
addChild
メソッドの変更:func (p *parser) addChild(n *Node) { if p.fosterParenting { p.fosterParent(n) } else { p.top().Add(n) } if n.Type == ElementNode { p.oe = append(p.oe, n) } }
新しいノードを追加する際に、
fosterParenting
フラグがtrue
であれば、新しく追加されたfosterParent
関数を呼び出すように変更されました。そうでなければ、通常のtop().Add(n)
が実行されます。 -
fosterParent
関数の追加:func (p *parser) fosterParent(n *Node) { var table, parent *Node var i int for i = len(p.oe) - 1; i >= 0; i-- { if p.oe[i].Data == "table" { table = p.oe[i] break } } if table == nil { // The foster parent is the html element. parent = p.oe[0] // p.oe[0] is always the <html> element } else { parent = table.Parent } if parent == nil { // This case handles if table is the root or has no parent parent = p.oe[i-1] // Fallback to the element before the table in the stack } var child *Node for i, child = range parent.Child { if child == table { break } } if i == len(parent.Child) { parent.Add(n) } else { // Insert n into parent.Child at index i. parent.Child = append(parent.Child[:i+1], parent.Child[i:]...) parent.Child[i] = n n.Parent = parent } }
この関数は、HTML5のフォスターペアレンティングアルゴリズムを実装しています。
- まず、スタックを逆順に走査し、最も近い
<table>
要素を探します。 <table>
が見つからない場合、フォスターペアレントは<html>
要素(スタックの最初の要素)になります。<table>
が見つかった場合、その<table>
の親要素がフォスターペアレントになります。もし<table>
が親を持たない場合(例: ドキュメントのルート要素である場合)、スタック内の<table>
の直前の要素がフォスターペアレントとして選ばれます。- フォスターペアレントが決まったら、新しいノード
n
をそのフォスターペアレントの子として挿入します。挿入位置は、<table>
要素が見つかった場合はその直前、それ以外の場合は末尾になります。
- まず、スタックを逆順に走査し、最も近い
-
reconstructActiveFormattingElements
のバグ修正:// Before: // i++ // n = p.afe[i] // p.addChild(n.clone()) // p.afe[i] = n // Bug: original 'n' is kept, not the clone added to DOM // After: i++ clone := p.afe[i].clone() // Clone the element p.addChild(clone) // Add the clone to the DOM p.afe[i] = clone // Update the AFE list with the clone
以前のコードでは、アクティブフォーマット要素のリスト(
p.afe
)に格納されている要素のクローンをDOMに追加していましたが、リスト自体は元の要素への参照を保持していました。これにより、DOMとリストの間で不整合が生じる可能性がありました。修正後は、DOMに追加されたクローンをリストにも格納することで、両者の整合性を保っています。 -
inBodyIM
および関連メソッドの変更:inBodyIM
のdefault
ケースで、p.inBodyEndTagOther(p.tok.Data)
が呼び出されるようになりました。これは、特定の終了タグにマッチしない場合の一般的な処理をカプセル化しています。inBodyEndTagFormatting
内で、formattingElement == nil
の場合にp.inBodyEndTagOther(tag)
を呼び出すようになりました。inBodyEndTagFormatting
のswitch commonAncestor.Data
ブロックで、テーブル関連の要素(table
,tbody
,tfoot
,thead
,tr
)の場合にp.fosterParent(lastNode)
を呼び出すようになりました。これは、テーブル内で不正にネストされたノードをフォスターペアレンティングルールに従って処理するためのものです。
-
inBodyEndTagOther
関数の追加:func (p *parser) inBodyEndTagOther(tag string) { for i := len(p.oe) - 1; i >= 0; i-- { if p.oe[i].Data == tag { p.oe = p.oe[:i] break } if isSpecialElement[p.oe[i].Data] { break } } }
この関数は、
inBodyIM
における「その他の終了タグ」の処理を実装しています。スタックを逆順に走査し、マッチするタグが見つかるか、または特殊な要素(例:html
,body
,p
など、特定のコンテキストでスタックから削除されない要素)が見つかるまで要素をポップします。 -
inTableIM
の変更:- テーブル関連の開始タグ(
tbody
,tfoot
,thead
,td
,th
,tr
,table
)の処理が大幅に修正されました。 - 特に、
td
,th
,tr
の開始タグが来た場合、clearStackToTableContext()
を呼び出し、tbody
要素を追加してからinTableBodyIM
に遷移するようになりました。 table
開始タグが来た場合、スタックをtable
スコープの停止タグまでポップし、挿入モードをリセットするようになりました。- 最も重要な変更は、
inTableIM
の最後にフォスターペアレンティングを有効にするロジックが追加されたことです。
これにより、テーブル関連の要素が現在のトップ要素である場合、その後の要素の挿入はフォスターペアレンティングルールに従うようになります。switch p.top().Data { case "table", "tbody", "tfoot", "thead", "tr": p.fosterParenting = true defer func() { p.fosterParenting = false }() } return useTheRulesFor(p, inTableIM, inBodyIM)
defer
キーワードにより、この挿入モードが終了した後にfosterParenting
フラグが自動的にfalse
に戻されます。
- テーブル関連の開始タグ(
-
clearStackToTableContext
関数の追加:func (p *parser) clearStackToTableContext() { for i := len(p.oe) - 1; i >= 0; i-- { if x := p.oe[i].Data; x == "table" || x == "html" { p.oe = p.oe[:i+1] return } } }
このヘルパー関数は、スタックを逆順に走査し、最も近い
table
要素またはhtml
要素が見つかるまで、その要素より上のすべての要素をスタックから削除します。これは、テーブル関連の要素を挿入する前に、スタックを適切なコンテキストにリセットするために使用されます。
src/pkg/html/parse_test.go
の変更点
-
テストループの範囲変更:
// Before: // for i := 0; i < 30; i++ { // After: for i := 0; i < 31; i++ {
TestParser
関数内のテストケースを処理するループが、30番目のテストケース(インデックス29)までではなく、31番目のテストケース(インデックス30)まで実行されるように変更されました。これは、このコミットが修正する特定のテストケース30をカバーするためです。 -
テストケース30のレンダリングスキップ:
if filename == "tests1.dat" && i == 30 { // Test 30 in tests1.dat is such messed-up markup that a correct parse // results in a non-conforming tree (one <a> element nested inside another). // Therefore when it is rendered and re-parsed, it isn't the same. // So we skip rendering on that test. continue }
tests1.dat
のテストケース30は、<a><table><td><a><table></table><a></tr><a></table><b>X</b>C<a>Y
という非常に複雑で不正なマークアップを含んでいます。このマークアップは、HTML5のパースルールに従って正しくパースされたとしても、結果として生成されるDOMツリーが「非準拠」な状態(例:<a>
要素が別の<a>
要素の中にネストされている)になることがあります。このような非準拠なツリーを再度レンダリングし、それを再度パースすると、元のツリーと同一にならない可能性があるため、この特定のテストケースではレンダリングと再パースの検証ステップがスキップされるようになりました。これは、パーサーが仕様通りに動作していることを確認しつつ、テストの制約を考慮した現実的な対応です。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、src/pkg/html/parse.go
ファイル内の以下の関数とロジックに集約されます。
fosterParent
関数の追加: フォスターペアレンティングアルゴリズムの具体的な実装。addChild
メソッドの変更:fosterParenting
フラグに基づいてfosterParent
を呼び出すロジック。reconstructActiveFormattingElements
の修正: アクティブフォーマット要素のリストとDOMの整合性を保つためのバグ修正。inTableIM
の変更: テーブル挿入モードにおけるフォスターペアレンティングの有効化と、テーブル関連要素のパースロジックの改善。clearStackToTableContext
関数の追加: テーブルコンテキストへのスタッククリアのヘルパー関数。
これらの変更が連携して、HTML5の複雑なエラー回復ルール、特にテーブル内の不正なマークアップの処理と、フォーマット要素の正確な管理を実現しています。
コアとなるコードの解説
fosterParent
関数
この関数は、HTML5のフォスターペアレンティングアルゴリズムを直接実装しています。
-
フォスターペアレントの特定:
- まず、現在のオープン要素のスタック(
p.oe
)を逆順に走査し、最も近い<table>
要素を探します。 <table>
が見つからない場合、フォスターペアレントはドキュメントのルートである<html>
要素(p.oe[0]
)になります。<table>
が見つかった場合、その<table>
の親要素がフォスターペアレントになります。これは、テーブルの外部に要素を「養子縁組」させるためです。- もし
<table>
が親を持たない(例: ドキュメントのルート要素である)場合、または何らかの理由でtable.Parent
がnil
の場合、スタック内の<table>
の直前の要素(p.oe[i-1]
)がフォスターペアレントとして選ばれます。これは、テーブルの直前に要素を挿入するためのフォールバックメカニズムです。
- まず、現在のオープン要素のスタック(
-
ノードの挿入:
- フォスターペアレントの子ノードリストを走査し、見つかった
<table>
要素のインデックスi
を特定します。 - もし
<table>
がフォスターペアレントの子として見つからない場合(i == len(parent.Child)
)、新しいノードn
はフォスターペアレントの末尾に単純に追加されます。 <table>
が見つかった場合、新しいノードn
は、その<table>
の直前の位置(インデックスi
)に挿入されます。これは、スライス操作append(parent.Child[:i+1], parent.Child[i:]...)
とparent.Child[i] = n
によって実現されます。これにより、<table>
の前に要素が挿入され、<table>
自体は後続の要素として維持されます。
- フォスターペアレントの子ノードリストを走査し、見つかった
reconstructActiveFormattingElements
の修正
この修正は、アクティブフォーマット要素のリスト(p.afe
)と、実際にDOMツリーに追加されるノードの間の参照の整合性を確保するために重要です。
以前のコードでは、p.afe[i]
からクローンを作成し、そのクローンをDOMに追加していましたが、p.afe[i]
自体は元の要素への参照を保持していました。これは、もし元の要素が後で変更された場合、p.afe
リスト内の参照がDOMツリー内の実際のノードと一致しなくなるという潜在的なバグを引き起こしていました。
修正後は、クローンを作成し、そのクローンをDOMに追加した後、そのクローン自体をp.afe[i]
に代入しています。これにより、p.afe
リスト内の参照が常にDOMツリー内の対応するノードと一致するようになり、フォーマット要素の再構築がより正確かつ堅牢になります。
inTableIM
におけるフォスターペアレンティングの有効化
inTableIM
(テーブル内部の挿入モード)の変更は、テーブル内で不正なコンテンツが検出された場合に、フォスターペアレンティングを自動的に有効にするためのものです。
switch p.top().Data {
case "table", "tbody", "tfoot", "thead", "tr":
p.fosterParenting = true
defer func() { p.fosterParenting = false }()
}
return useTheRulesFor(p, inTableIM, inBodyIM)
このコードブロックは、現在のオープン要素のスタックのトップがtable
、tbody
、tfoot
、thead
、またはtr
である場合に、p.fosterParenting
フラグをtrue
に設定します。そして、defer
キーワードを使用することで、このinTableIM
関数が終了する際に自動的にp.fosterParenting
をfalse
に戻すことを保証しています。これにより、テーブル内部でのみフォスターペアレンティングが一時的に有効になり、他のコンテキストに影響を与えないようになっています。
useTheRulesFor(p, inTableIM, inBodyIM)
は、HTML5パーシングアルゴリズムの「別の挿入モードのルールを使用する」という概念を実装しており、この場合はinTableIM
のルールが適用できない場合にinBodyIM
のルールにフォールバックすることを示唆しています。
関連リンク
- Go言語のHTMLパッケージ: https://pkg.go.dev/golang.org/x/net/html (コミット当時の
src/pkg/html
は、後にgolang.org/x/net/html
に移動しました) - HTML5仕様 - 13.2.6.4.1 The "in body" insertion mode: https://html.spec.whatwg.org/multipage/parsing.html#the-in-body-insertion-mode
- HTML5仕様 - 13.2.6.4.10 The "in table" insertion mode: https://html.spec.whatwg.org/multipage/parsing.html#the-in-table-insertion-mode
- HTML5仕様 - 13.2.6.4.1 The "in body" insertion mode - A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr": https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody (このセクションにフォスターペアレンティングのトリガーが記述されています)
- HTML5仕様 - 13.2.6.1 The foster parenting algorithm: https://html.spec.whatwg.org/multipage/parsing.html#foster-parenting
- HTML5仕様 - 13.2.5.1 The list of active formatting elements: https://html.spec.whatwg.org/multipage/parsing.html#the-list-of-active-formatting-elements
参考にした情報源リンク
- HTML5 Parsing: Foster Parenting: https://www.html5rocks.com/en/tutorials/internals/howbrowserswork/#The_foster_parenting_algorithm (ブラウザの動作原理に関するHTML5 Rocksの記事。フォスターペアレンティングについて簡潔に説明されています。)
- What is the "list of active formatting elements" in HTML5 parsing?: https://stackoverflow.com/questions/10000000/what-is-the-list-of-active-formatting-elements-in-html5-parsing (Stack Overflowの質問と回答で、アクティブフォーマット要素について解説されています。)
- Go Code Review (Gerrit) for this commit: https://golang.org/cl/5309052 (元のコードレビューページ。議論や追加のコンテキストが含まれている可能性があります。)
[インデックス 10076] ファイルの概要
このコミットは、Go言語のhtml
パッケージにおけるHTMLパーサーの重要な改善を導入しています。主な目的は、HTML5の仕様で定義されている「フォスターペアレンティング (foster parenting)」アルゴリズムを実装すること、およびアクティブフォーマット要素の再構築に関する既存のバグを修正することです。これにより、テーブル内に不適切に配置された要素の処理が改善され、また、HTMLドキュメントの解析中に適用されるテキストフォーマットがより正確に処理されるようになり、結果としてより堅牢で標準に準拠したHTMLパースが可能になります。
コミット
commit 2aa589c843debaef249e7fbcd9dd3fa0546c9c8
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Sun Oct 23 18:36:01 2011 +1100
html: implement foster parenting
Implement the foster-parenting algorithm for content that is inside a table
but not in a cell.
Also fix a bug in reconstructing the active formatting elements.
Pass test 30 in tests1.dat:
<a><table><td><a><table></table><a></tr><a></table><b>X</b>C<a>Y
R=nigeltao
CC=golang-dev
https://golang.org/cl/5309052
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2aa589c843debaef249e7fbcd9dd3fa0546c9c8
元コミット内容
このコミットは、Go言語のhtml
パッケージに対して以下の主要な変更を加えています。
- フォスターペアレンティングの実装: HTML5の仕様に従い、テーブル要素(
<table>
、<tbody>
、<thead>
、<tfoot>
、<tr>
など)の内部に、セル要素(<td>
、<th>
)ではないコンテンツが誤って配置された場合の処理ロジックを導入します。これにより、不正なHTMLマークアップに対しても、ブラウザが通常行うエラー回復動作を模倣し、コンテンツが失われることなくDOMツリーの適切な場所に「養子縁組」されるようになります。 - アクティブフォーマット要素の再構築に関するバグ修正: HTMLパーサーが、
<b>
や<i>
などのフォーマット要素の適用範囲を管理する「アクティブフォーマット要素のリスト」を再構築する際に発生していたバグを修正します。この修正により、ネストされたフォーマットタグや、不正にネストされたフォーマットタグがより正確に処理され、DOMツリーの整合性が保たれます。
これらの変更は、特にtests1.dat
というテストデータセットに含まれるテストケース30(<a><table><td><a><table></table><a></tr><a></table><b>X</b>C<a>Y
)のような、複雑で意図的に不正なHTMLマークアップを正しくパースできるようになることを目的としています。
変更の背景
Webブラウザは、たとえHTMLマークアップがW3CやWHATWGの仕様に厳密に準拠していなくても、可能な限りコンテンツをレンダリングしようとします。この「エラー回復」の挙動は、HTMLの普及に大きく貢献しましたが、その一方で、ブラウザ間の互換性の問題を引き起こす原因ともなりました。HTML5の仕様は、このエラー回復の挙動を詳細に標準化することで、すべてのブラウザが同じように不正なマークアップを処理し、同じDOMツリーを構築できるようにすることを目指しました。
このコミットの背景には、Go言語のhtml
パッケージが、このHTML5の厳格なパーシングアルゴリズムに完全に準拠することを目指しているという目標があります。特に、以下の点が重要でした。
- テーブル構造の厳格さとエラー回復: HTMLのテーブル構造は非常に厳格であり、
<table>
タグの直下には<caption>
,colgroup
,<thead>
,<tbody>
,<tfoot>
のみが許可され、それ以外の要素は<tr>
や<td>
などのセル要素の中にネストされるべきです。しかし、開発者が誤って<table><div>...</div></table>
のように記述した場合、ブラウザは<div>
要素を無視するのではなく、テーブルの外に「移動」させてレンダリングします。この挙動が「フォスターペアレンティング」であり、Goのパーサーもこれを模倣する必要がありました。 - アクティブフォーマット要素の複雑な管理:
<b>
,<i>
,<a>
などのインラインフォーマット要素は、開始タグと終了タグのペアによってテキストのスタイルを定義します。しかし、<b><i>テキスト</b></i>
のようにタグが正しくネストされていない場合、ブラウザは特定のアルゴリズム(「アクティブフォーマット要素の再構築」または「養子縁組機関アルゴリズム」とも呼ばれる)に従って、DOMツリーの整合性を保ちながらフォーマットを適用します。このアルゴリズムにバグがあると、テキストの表示が崩れたり、意図しないフォーマットが適用されたりする可能性があります。
このコミットは、これらのHTML5パーシングアルゴリズムの複雑な側面をGoのパーサーに組み込むことで、より多くの種類のHTMLドキュメントを正確かつ予測可能にパースできるようにし、Go言語のHTML処理能力を向上させることを目的としています。
前提知識の解説
HTML5パーシングアルゴリズム
HTML5のパーシングアルゴリズムは、Webブラウザが生のHTMLバイトストリームを読み込み、構造化されたDOM(Document Object Model)ツリーに変換するための標準化されたプロセスです。このアルゴリズムは、非常に詳細なステートマシンとして定義されており、以下の主要な段階と概念を含みます。
- トークナイゼーション (Tokenization): 入力されたHTMLバイトストリームを、意味のある単位である「トークン」(例: 開始タグ、終了タグ、テキスト、コメント、DOCTYPEなど)に分解するプロセスです。
- ツリー構築 (Tree Construction): トークナイザーから受け取ったトークンに基づいて、DOMツリーを構築するプロセスです。この段階は、現在の「挿入モード (insertion mode)」と、オープン要素のスタック、アクティブフォーマット要素のリストなどの内部データ構造によって制御されます。
- 挿入モード (Insertion Mode): パーサーが現在どのHTML要素のコンテキストで動作しているかを示す状態です。例えば、
initial
(初期状態)、before html
、in head
、in body
、in table
など、多くのモードが存在します。各モードには、特定のトークンが来た場合の処理ルールが厳密に定義されており、これによりDOMツリーへの要素の追加方法や、エラー回復の挙動が決定されます。 - オープン要素のスタック (Stack of Open Elements): まだ終了タグが来ていない、現在開いているHTML要素を追跡するためのスタックデータ構造です。DOMツリーの階層構造を正確に反映し、要素のネスト関係を管理するために使用されます。新しい要素が追加されるとスタックにプッシュされ、対応する終了タグが来るとポップされます。
- 挿入モード (Insertion Mode): パーサーが現在どのHTML要素のコンテキストで動作しているかを示す状態です。例えば、
フォスターペアレンティング (Foster Parenting)
フォスターペアレンティングは、HTML5パーシングアルゴリズムにおける重要なエラー回復メカニズムの一つです。これは、特定の要素、特にテーブル関連の要素(<table>
, <tbody>
, <thead>
, <tfoot>
, <tr>
)の内部に、本来その場所には挿入されるべきではないコンテンツ(例: テキストノード、<div>
要素など、セル要素ではないもの)が挿入されようとした場合に発動します。
このアルゴリズムが発動すると、パーサーは以下のルールに従って、その「迷子」の要素をDOMツリー内の別の「養子縁組先(フォスターペアレント)」に移動させます。
- テーブルの直前の兄弟要素: もしテーブルの直前に兄弟要素が存在し、それがテキストノードでない場合、その兄弟要素の末尾に挿入されます。
- テーブルの親要素: 上記の条件に合致しない場合、テーブルの親要素の末尾に挿入されます。
<html>
要素: 上記のいずれの条件にも合致しない場合、最終的にドキュメントのルートである<html>
要素の末尾に挿入されます。
このメカニズムにより、不正なマークアップであってもコンテンツが失われることなく、DOMツリーの論理的に適切な場所に配置されることが保証され、ブラウザはユーザーにコンテンツを表示し続けることができます。
アクティブフォーマット要素のリストと再構築 (List of Active Formatting Elements and Reconstruction)
「アクティブフォーマット要素のリスト (List of Active Formatting Elements)」は、HTMLパーサーが<a>
, <b>
, <i>
, <strong>
, <em>
, font
, s
, strike
, u
, tt
, big
, small
, code
, nobr
といった特定のフォーマット要素の適用範囲を管理するために使用するデータ構造です。
- リストの役割: これらのフォーマット要素の開始タグが検出されると、その要素はリストに追加されます。対応する終了タグが検出されると、リストから削除されます。これにより、パーサーは現在どのフォーマットが「アクティブ」であるかを追跡し、後続のテキストノードに適切に適用することができます。
- マーカー: リストには、特定の要素(例:
p
やform
の終了タグ、template
の開始タグなど)が処理された際に挿入される「マーカー」と呼ばれる特殊なエントリも含まれることがあります。これらのマーカーは、フォーマット要素が特定の境界を超えて「養子縁組」されるのを防ぐ役割を果たします。 - 再構築アルゴリズム: HTMLマークアップが不正な場合(例:
<b><i>テキスト</b></i>
のようにタグが正しくネストされていない場合や、フォーマット要素が暗黙的に閉じられた場合など)、アクティブフォーマット要素のリストの整合性が失われる可能性があります。このような状況で、パーサーは「アクティブフォーマット要素を再構築するアルゴリズム」を実行します。このプロセスは、リスト内の要素を反復処理し、必要に応じて要素を移動したり、クローンを作成してDOMツリーに再配置したりすることで、HTMLパースルールに従ってDOMツリーが正しく構築され、フォーマットが意図通りに適用されるようにします。このアルゴリズムは、その複雑な挙動から「養子縁組機関アルゴリズム (adoption agency algorithm)」とも呼ばれます。
これらのメカニズムは、HTMLの寛容性と、ブラウザが不正なマークアップに対しても一貫した方法でエラー回復を行う能力の基盤となっています。
技術的詳細
このコミットは、Go言語のhtml
パッケージの主要なパーシングロジックを含むsrc/pkg/html/parse.go
ファイルと、そのテストケースを定義するsrc/pkg/html/parse_test.go
ファイルに修正を加えています。
src/pkg/html/parse.go
の変更点
-
parser
構造体へのfosterParenting
フィールドの追加:type parser struct { // ... // fosterParenting is whether new elements should be inserted according to // the foster parenting rules (section 11.2.5.3). fosterParenting bool }
parser
構造体にfosterParenting
というブール型のフィールドが追加されました。このフラグは、新しい要素がフォスターペアレンティングのルールに従って挿入されるべきかどうかを制御します。 -
addChild
メソッドの変更:func (p *parser) addChild(n *Node) { if p.fosterParenting { p.fosterParent(n) } else { p.top().Add(n) } if n.Type == ElementNode { p.oe = append(p.oe, n) } }
要素をDOMツリーに追加する
addChild
メソッドが修正されました。p.fosterParenting
フラグがtrue
の場合、新しく追加されたfosterParent
関数を呼び出して要素を挿入します。そうでなければ、従来のp.top().Add(n)
(現在のトップ要素の子として追加)が実行されます。これにより、フォスターペアレンティングのロジックが条件付きで適用されるようになります。 -
fosterParent
関数の追加:func (p *parser) fosterParent(n *Node) { var table, parent *Node var i int // Find the nearest "table" element in the stack of open elements for i = len(p.oe) - 1; i >= 0; i-- { if p.oe[i].Data == "table" { table = p.oe[i] break } } if table == nil { // If no "table" element is found, the foster parent is the <html> element (root) parent = p.oe[0] } else { // If a "table" element is found, the foster parent is its parent parent = table.Parent } // Fallback if table has no parent or is the root if parent == nil { parent = p.oe[i-1] // Use the element before the table in the stack } var child *Node // Find the index of the "table" element within its foster parent's children for i, child = range parent.Child { if child == table { break } } if i == len(parent.Child) { // If "table" is not found among children, add the new node at the end parent.Add(n) } else { // Insert the new node 'n' before the "table" element parent.Child = append(parent.Child[:i+1], parent.Child[i:]...) parent.Child[i] = n n.Parent = parent } }
この関数は、HTML5のフォスターペアレンティングアルゴリズムを実装しています。
- まず、オープン要素のスタックを逆順に走査し、最も近い
<table>
要素を探します。 <table>
が見つからない場合、フォスターペアレントは<html>
要素(スタックの最初の要素)になります。<table>
が見つかった場合、その<table>
の親要素がフォスターペアレントになります。もし<table>
が親を持たない場合(例: ドキュメントのルート要素である場合)、スタック内の<table>
の直前の要素がフォスターペアレントとして選ばれます。- フォスターペアレントが決まったら、新しいノード
n
をそのフォスターペアレントの子として挿入します。挿入位置は、<table>
要素が見つかった場合はその直前、それ以外の場合は末尾になります。
- まず、オープン要素のスタックを逆順に走査し、最も近い
-
reconstructActiveFormattingElements
のバグ修正:// 修正前: // i++ // n = p.afe[i] // p.addChild(n.clone()) // p.afe[i] = n // ここがバグ。クローンではなく元の要素への参照を保持していた。 // 修正後: i++ clone := p.afe[i].clone() // 要素をクローン p.addChild(clone) // クローンをDOMに追加 p.afe[i] = clone // AFEリストをクローンで更新
アクティブフォーマット要素のリストを再構築する際、以前のコードでは、DOMツリーに追加された要素のクローンではなく、元の要素への参照をリスト内に保持していました。これにより、DOMとリストの間で不整合が生じる可能性がありました。修正後は、DOMに追加されたクローン自体をリストにも格納することで、両者の整合性を保ち、フォーマット要素の正確な管理を保証します。
-
inBodyIM
および関連メソッドの変更:inBodyIM
のdefault
ケースで、p.inBodyEndTagOther(p.tok.Data)
が呼び出されるようになりました。これは、特定の終了タグにマッチしない場合の一般的な処理をカプセル化しています。inBodyEndTagFormatting
内で、formattingElement == nil
の場合にp.inBodyEndTagOther(tag)
を呼び出すようになりました。inBodyEndTagFormatting
のswitch commonAncestor.Data
ブロックで、テーブル関連の要素(table
,tbody
,tfoot
,thead
,tr
)の場合にp.fosterParent(lastNode)
を呼び出すようになりました。これは、テーブル内で不正にネストされたノードをフォスターペアレンティングルールに従って処理するためのものです。
-
inBodyEndTagOther
関数の追加:func (p *parser) inBodyEndTagOther(tag string) { for i := len(p.oe) - 1; i >= 0; i-- { if p.oe[i].Data == tag { p.oe = p.oe[:i] break } if isSpecialElement[p.oe[i].Data] { break } } }
この関数は、
inBodyIM
における「その他の終了タグ」の処理を実装しています。スタックを逆順に走査し、マッチするタグが見つかるか、または特殊な要素(例:html
,body
など、特定のコンテキストでスタックから削除されない要素)が見つかるまで要素をポップします。 -
inTableIM
の変更:- テーブル関連の開始タグ(
tbody
,tfoot
,thead
,td
,th
,tr
,table
)の処理が大幅に修正されました。特に、td
,th
,tr
の開始タグが来た場合、clearStackToTableContext()
を呼び出し、tbody
要素を追加してからinTableBodyIM
に遷移するようになりました。 table
開始タグが来た場合、スタックをtable
スコープの停止タグまでポップし、挿入モードをリセットするようになりました。- 最も重要な変更は、
inTableIM
の最後にフォスターペアレンティングを有効にするロジックが追加されたことです。
これにより、テーブル関連の要素が現在のトップ要素である場合、その後の要素の挿入はフォスターペアレンティングルールに従うようになります。switch p.top().Data { case "table", "tbody", "tfoot", "thead", "tr": p.fosterParenting = true defer func() { p.fosterParenting = false }() } return useTheRulesFor(p, inTableIM, inBodyIM)
defer
キーワードにより、この挿入モードが終了した後にfosterParenting
フラグが自動的にfalse
に戻され、フォスターペアレンティングがテーブル内部でのみ一時的に有効になることを保証します。
- テーブル関連の開始タグ(
-
clearStackToTableContext
関数の追加:func (p *parser) clearStackToTableContext() { for i := len(p.oe) - 1; i >= 0; i-- { if x := p.oe[i].Data; x == "table" || x == "html" { p.oe = p.oe[:i+1] return } } }
このヘルパー関数は、スタックを逆順に走査し、最も近い
table
要素またはhtml
要素が見つかるまで、その要素より上のすべての要素をスタックから削除します。これは、テーブル関連の要素を挿入する前に、スタックを適切なコンテキストにリセットするために使用されます。
src/pkg/html/parse_test.go
の変更点
-
テストループの範囲変更:
// 修正前: for i := 0; i < 30; i++ { // 修正後: for i := 0; i < 31; i++ {
TestParser
関数内のテストケースを処理するループが、30番目のテストケース(インデックス29)までではなく、31番目のテストケース(インデックス30)まで実行されるように変更されました。これにより、このコミットが修正する特定のテストケース30がテストスイートに含まれるようになります。 -
テストケース30のレンダリングスキップ:
if filename == "tests1.dat" && i == 30 { // Test 30 in tests1.dat is such messed-up markup that a correct parse // results in a non-conforming tree (one <a> element nested inside another). // Therefore when it is rendered and re-parsed, it isn't the same. // So we skip rendering on that test. continue }
tests1.dat
のテストケース30は、非常に複雑で不正なHTMLマークアップを含んでいます。このマークアップは、HTML5のパースルールに従って正しくパースされたとしても、結果として生成されるDOMツリーが「非準拠」な状態(例:<a>
要素が別の<a>
要素の中にネストされている)になることがあります。このような非準拠なツリーを再度レンダリングし、それを再度パースすると、元のツリーと同一にならない可能性があるため、この特定のテストケースではレンダリングと再パースの検証ステップがスキップされるようになりました。これは、パーサーが仕様通りに動作していることを確認しつつ、テストの制約を考慮した現実的な対応です。
コアとなるコードの変更箇所
このコミットのコアとなるコードの変更箇所は、主にsrc/pkg/html/parse.go
ファイル内の以下の部分です。
parser
構造体へのfosterParenting
フィールドの追加: フォスターペアレンティングの有効/無効を制御するフラグ。addChild
メソッドの条件分岐:fosterParenting
フラグに基づいて要素の挿入方法を切り替える。fosterParent
関数の新規追加: HTML5のフォスターペアレンティングアルゴリズムの具体的な実装。reconstructActiveFormattingElements
メソッドの修正: アクティブフォーマット要素のリストとDOMツリーの整合性を保つためのバグ修正。inTableIM
におけるフォスターペアレンティングの有効化ロジック: テーブル挿入モードでフォスターペアレンティングを一時的に有効にする。clearStackToTableContext
関数の新規追加: テーブルコンテキストへのスタッククリアのヘルパー関数。
これらの変更が連携して、HTML5の複雑なエラー回復ルール、特にテーブル内の不正なマークアップの処理と、フォーマット要素の正確な管理を実現しています。
コアとなるコードの解説
fosterParent
関数の詳細
fosterParent
関数は、HTML5のフォスターペアレンティングアルゴリズムの核心部分です。この関数は、不正な位置に挿入されようとしているノードn
を受け取り、それをDOMツリー内の適切な「養子縁組先」に移動させます。
-
フォスターペアレントの探索:
- 関数はまず、現在のオープン要素のスタック(
p.oe
)を逆順に走査し、最も近い<table>
要素を探します。これは、テーブルの内部で不正なコンテンツが検出された場合に、そのテーブルの外部にコンテンツを移動させるためです。 - もしスタック内に
<table>
要素が見つからない場合、フォスターペアレントはドキュメントのルート要素である<html>
(p.oe[0]
)になります。これは、テーブル以外のコンテキストでフォスターペアレンティングがトリガーされた場合のフォールバックです。 <table>
要素が見つかった場合、その<table>
の親要素がフォスターペアレントとして選ばれます。これは、テーブルの直前の兄弟要素またはテーブルの親要素にコンテンツを移動させるというHTML5の仕様に準拠しています。- もし
table.Parent
がnil
の場合(例:<table>
がドキュメントのルート要素であるか、何らかの理由で親を持たない場合)、スタック内の<table>
の直前の要素(p.oe[i-1]
)がフォスターペアレントとして選ばれます。これは、テーブルの直前に要素を挿入するためのフォールバックメカニズムであり、HTML5の仕様の複雑なケースに対応しています。
- 関数はまず、現在のオープン要素のスタック(
-
ノードの挿入:
- フォスターペアレントが特定された後、関数はフォスターペアレントの子ノードリストを走査し、見つかった
<table>
要素のインデックスi
を特定します。 - もし
<table>
がフォスターペアレントの子として見つからない場合(i == len(parent.Child)
)、新しいノードn
はフォスターペアレントの末尾に単純に追加されます。 <table>
が見つかった場合、新しいノードn
は、その<table>
の直前の位置(インデックスi
)に挿入されます。これは、Goのスライス操作append(parent.Child[:i+1], parent.Child[i:]...)
とparent.Child[i] = n
によって実現されます。この操作により、<table>
の前に要素が挿入され、<table>
自体は後続の要素として維持されます。
- フォスターペアレントが特定された後、関数はフォスターペアレントの子ノードリストを走査し、見つかった
reconstructActiveFormattingElements
のバグ修正の詳細
この修正は、アクティブフォーマット要素のリスト(p.afe
)と、実際にDOMツリーに追加されるノードの間の参照の整合性を確保するために非常に重要です。
HTML5のパーシングアルゴリズムでは、アクティブフォーマット要素のリストは、フォーマット要素の適用範囲を追跡するために使用されます。このリストには、DOMツリーに実際に追加された要素の参照が格納されるべきです。しかし、修正前のコードでは、p.afe[i]
から要素をクローンし、そのクローンをDOMに追加していましたが、p.afe[i]
自体は元の要素への参照を保持していました。
この問題は、もし元の要素が後で変更された場合(例えば、属性が追加されたり、テキストコンテンツが変更されたりした場合)、p.afe
リスト内の参照がDOMツリー内の実際のノードと一致しなくなり、結果としてフォーマットの適用が誤ったり、DOMツリーの整合性が失われたりする潜在的なバグを引き起こしていました。
修正後は、クローンを作成し、そのクローンをDOMに追加した後、そのクローン自体をp.afe[i]
に代入しています。これにより、p.afe
リスト内の参照が常にDOMツリー内の対応するノードと一致するようになり、フォーマット要素の再構築がより正確かつ堅牢になります。これは、HTMLパーサーの正確性と信頼性を向上させる上で不可欠な変更です。
inTableIM
におけるフォスターペアレンティングの有効化ロジック
inTableIM
(テーブル内部の挿入モード)の変更は、テーブル内で不正なコンテンツが検出された場合に、フォスターペアレンティングを自動的に有効にするためのものです。
switch p.top().Data {
case "table", "tbody", "tfoot", "thead", "tr":
p.fosterParenting = true
defer func() { p.fosterParenting = false }()
}
return useTheRulesFor(p, inTableIM, inBodyIM)
このコードブロックは、現在のオープン要素のスタックのトップがtable
、tbody
、tfoot
、thead
、またはtr
である場合に、p.fosterParenting
フラグをtrue
に設定します。これは、これらの要素の内部で、セル要素ではないコンテンツが挿入されようとしている状況を示唆しています。
そして、defer func() { p.fosterParenting = false }()
という行が重要です。defer
キーワードは、囲んでいる関数(この場合はinTableIM
)がリターンする直前に、指定された関数を実行することを保証します。これにより、p.fosterParenting
フラグは、テーブル内部での処理が完了した後に自動的にfalse
に戻されます。このメカニズムにより、フォスターペアレンティングはテーブル内部でのみ一時的に有効になり、パーサーの他の部分や他の挿入モードに意図しない影響を与えることを防ぎます。
useTheRulesFor(p, inTableIM, inBodyIM)
は、HTML5パーシングアルゴリズムの「別の挿入モードのルールを使用する」という概念を実装しており、この場合はinTableIM
のルールが適用できない場合にinBodyIM
のルールにフォールバックすることを示唆しています。
関連リンク
- Go言語のHTMLパッケージ: https://pkg.go.dev/golang.org/x/net/html (コミット当時の
src/pkg/html
は、後にgolang.org/x/net/html
に移動しました。現在のGo言語のHTMLパーサーの公式ドキュメントです。) - HTML Standard (WHATWG):
- 13.2.6.4.1 The "in body" insertion mode: https://html.spec.whatwg.org/multipage/parsing.html#the-in-body-insertion-mode
- 13.2.6.4.10 The "in table" insertion mode: https://html.spec.whatwg.org/multipage/parsing.html#the-in-table-insertion-mode
- 13.2.6.1 The foster parenting algorithm: https://html.spec.whatwg.org/multipage/parsing.html#foster-parenting (フォスターペアレンティングアルゴリズムの公式仕様)
- 13.2.5.1 The list of active formatting elements: https://html.spec.whatwg.org/multipage/parsing.html#the-list-of-active-formatting-elements (アクティブフォーマット要素のリストの公式仕様)
- Go Code Review (Gerrit) for this commit: https://golang.org/cl/5309052 (このコミットの元のコードレビューページ。開発者間の議論や追加のコンテキストが含まれている可能性があります。)
参考にした情報源リンク
- HTML5 Parsing: Foster Parenting: https://www.html5rocks.com/en/tutorials/internals/howbrowserswork/#The_foster_parenting_algorithm (HTML5 Rocksによるブラウザの動作原理に関する記事で、フォスターペアレンティングアルゴリズムについて簡潔に説明されています。)
- What is the "list of active formatting elements" in HTML5 parsing?: https://stackoverflow.com/questions/10000000/what-is-the-list-of-active-formatting-elements-in-html5-parsing (Stack Overflowの質問と回答で、HTML5パーシングにおけるアクティブフォーマット要素のリストとその役割について詳細に解説されています。)
- HTML Parsing Algorithm (Table): (Web検索結果から、HTMLパーシングアルゴリズムの主要な段階と概念をまとめた情報源を参考にしました。具体的なURLは特定できませんが、HTML5仕様の関連セクションが最も信頼できる情報源です。)