[インデックス 10162] ファイルの概要
このコミットは、Go言語のHTMLパーサーにおける重要なバグ修正と改善を目的としています。具体的には、</td>
タグの処理をHTML5の仕様に準拠させ、テーブルセルが正しく閉じられるようにします。また、アクティブなフォーマット要素の再構築によって単一のトークンから複数のノードが生成される場合に、それらのノード間の親子関係が正しく構築されるように、フォスターペアレンティングのロジックを修正しています。これにより、不正なHTMLマークアップに対するパーサーの堅牢性が向上し、より正確なDOMツリーの構築が可能になります。
コミット
- コミットハッシュ:
9db3f78c392643769fd46fc7900a6deb1fd2692f
- 作者: Andrew Balholm andybalholm@gmail.com
- コミット日時: Tue Nov 1 11:42:54 2011 +1100
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9db3f78c392643769fd46fc7900a6deb1fd2692f
元コミット内容
html: process </td> tags; foster parent at most one node per token
Correctly close table cell when </td> is read.
Because of reconstructing the active formatting elements, more than one
node may be created when reading a single token.
If both nodes are foster parented, they will be siblings, but the first
node should be the parent of the second.
Pass tests1.dat, test 77:
<a href="blah">aba<table><a href="foo">br<tr><td></td></tr>x</table>aoe
| <html>
| <head>
| <body>
| <a>
| href="blah"
| "aba"
| <a>
| href="foo"
| "br"
| <a>
| href="foo"
| "x"
| <table>
| <tbody>
| <tr>
| <td>
| <a>
| href="foo"
| "aoe"
R=nigeltao
CC=golang-dev
https://golang.org/cl/5305074
変更の背景
このコミットは、HTMLパーサーが特定の不正なHTML構造を処理する際の挙動を修正するために行われました。主な問題点は以下の2点です。
</td>
タグの不適切な処理: HTMLのテーブル構造において、<td>
(テーブルデータセル) は<tr>
(テーブル行) の子要素として存在します。しかし、HTMLの仕様では、</td>
の終了タグが省略されたり、予期せぬ場所に出現したりした場合でも、パーサーは適切なDOM構造を構築する必要があります。このコミット以前のパーサーは、</td>
タグが読み込まれた際にテーブルセルを正しく閉じない、または不適切な処理を行っていました。これにより、DOMツリーが期待通りに構築されず、レンダリング結果に影響を与える可能性がありました。- フォスターペアレンティングにおけるノードの親子関係の誤り: HTMLパーシングの複雑な側面の一つに「フォスターペアレンティング (foster parenting)」があります。これは、特定の要素(特にテーブル関連の要素)が、本来の親要素のスコープ外に配置された場合に、DOMツリーの別の場所に「養子」として挿入されるメカニズムです。コミットメッセージによると、アクティブなフォーマット要素(例えば
<a>
タグなど)を再構築する過程で、単一の入力トークンから複数のノードが生成されることがありました。この際、もし両方のノードがフォスターペアレンティングの対象となった場合、それらが兄弟関係として扱われてしまい、本来あるべき親子関係が失われるというバグが存在しました。これは、特にネストされたアンカータグのような複雑なケースで問題を引き起こしていました。
これらの問題を解決し、HTML5のパーシング仕様にさらに準拠させることで、パーサーの堅牢性と正確性を向上させることが、このコミットの背景にあります。コミットメッセージに記載されているtests1.dat
のテストケース77は、この問題を示す具体的な例として挙げられています。
前提知識の解説
このコミットを理解するためには、HTML5のパーシングアルゴリズムに関するいくつかの重要な概念を理解しておく必要があります。
-
HTML5パーシングアルゴリズム: HTML5のパーシングは、非常に複雑なステートマシンとして定義されています。これは、ブラウザが不正なHTMLマークアップであっても、一貫した方法でDOMツリーを構築できるようにするためです。パーサーは入力ストリームをトークン化し、それぞれのトークン(開始タグ、終了タグ、テキストなど)に基づいて、現在の「挿入モード (insertion mode)」に従ってDOMツツリーを構築します。
-
挿入モード (Insertion Mode): パーサーの現在の状態を示すもので、次にどのトークンをどのように処理するかを決定します。例えば、
inBody
モード、inTable
モード、inCell
モードなどがあります。各モードには、特定のタグが来た場合の処理ルールが詳細に定義されています。このコミットでは、特にinCellIM
(in cell insertion mode) が関連しています。 -
アクティブなフォーマット要素 (Active Formatting Elements): HTMLパーサーは、
<a>
,<b>
,<i>
,<span>
などのフォーマット要素のリストを「アクティブなフォーマット要素」として保持しています。これは、これらの要素が適切に閉じられていない場合でも、後続のコンテンツがそのフォーマットの影響を受けるようにするために使用されます。例えば、<b>テキスト</b>
のように正しく閉じられていない場合でも、パーサーは<b>
の効果を維持しようとします。新しい要素が挿入される際に、このリストに基づいて要素が再構築されることがあります。 -
フォスターペアレンティング (Foster Parenting): HTML5パーシングの特殊なルールの一つで、主にテーブル関連の要素(
<table>
,<tbody>
,<thead>
,<tfoot>
,<tr>
,<td>
,<th>
)が、本来あるべき場所ではないスコープ(例えば、<table>
の外側)に挿入されようとした場合に適用されます。このような場合、要素はDOMツリーの別の場所(通常は直近の<table>
要素の直前、または<body>
の最後)に「養子」として挿入されます。これは、ブラウザが不正なテーブルマークアップに対しても、可能な限り意味のあるDOM構造を構築しようとするためのメカニズムです。 -
スコープ (Scope): HTMLパーシングにおけるスコープは、要素が特定のコンテキスト内で有効であるかどうかを判断するために使用されます。例えば、「テーブルスコープ (table scope)」は、テーブル関連の要素が期待される場所を指します。
popUntil
のような操作は、特定のスコープ内の要素をスタックからポップするために使用されます。
これらの概念は、HTMLパーサーがどのようにして複雑でしばしば不正なHTMLマークアップを解釈し、一貫したDOMツリーを構築するかを理解する上で不可欠です。
技術的詳細
このコミットは、HTML5パーシングアルゴリズムの特定の側面、特にテーブル要素の処理とフォスターペアレンティングの挙動に焦点を当てています。
</td>
タグの処理の改善
HTML5の仕様では、<td>
や <th>
の終了タグ (</td>
, </th>
) が出現した場合、パーサーは現在のテーブルセルを閉じ、その親である <tr>
要素のスコープに戻る必要があります。このコミット以前は、inCellIM
(in cell insertion mode) において </td>
または </th>
タグが読み込まれた際の処理が // TODO.
となっており、適切に実装されていませんでした。
変更後、inCellIM
で </td>
または </th>
が検出されると、以下の処理が行われます。
p.popUntil(tableScopeStopTags, p.tok.Data)
: これは、パーサーの要素スタックを、tableScopeStopTags
に含まれるタグ(<table>
,<tbody>
,<thead>
,<tfoot>
,<tr>
など、テーブル関連のスコープを停止させるタグ)または現在のトークン(td
またはth
)が見つかるまでポップする操作です。これにより、現在のセル要素が閉じられ、その親である<tr>
要素が現在の要素となります。- もし
popUntil
がfalse
を返した場合(つまり、適切な停止タグが見つからなかった場合)、そのトークンは無視され、パーサーは引き続きinCellIM
に留まります。これは、</td>
が予期せぬ場所に出現した場合の堅牢なエラーハンドリングです。
- もし
p.clearActiveFormattingElements()
: アクティブなフォーマット要素のリストをクリアします。これは、セルが閉じられた後に、そのセル内で適用されていたフォーマットが後続のコンテンツに誤って影響を与えないようにするために重要です。return inRowIM, true
: 処理が成功した場合、パーサーの挿入モードをinRowIM
(in row insertion mode) に変更します。これは、セルが閉じられた後、パーサーがテーブル行のコンテキストに戻ることを意味します。
この変更により、</td>
タグが読み込まれた際に、HTML5の仕様に従ってテーブルセルが正しく閉じられ、DOMツリーの整合性が保たれるようになります。
フォスターペアレンティングの修正
コミットメッセージで言及されているもう一つの問題は、アクティブなフォーマット要素の再構築によって単一のトークンから複数のノードが生成され、それらがフォスターペアレンティングされる際に親子関係が失われるというものです。
fosterParent
関数は、要素をフォスターペアレンティングする際に呼び出されます。この関数は、要素をDOMツリーの適切な「養子」の場所に挿入する役割を担います。
変更前は、fosterParent
関数内で p.fosterParenting
フラグがどのように扱われていたかは不明ですが、このコミットでは関数の冒頭で p.fosterParenting = false
が追加されています。この変更の意図は、フォスターペアレンティングの処理中に、パーサーが「フォスターペアレンティング中である」という状態をリセットすることにあると考えられます。
コミットメッセージの「If both nodes are foster parented, they will be siblings, but the first node should be the parent of the second.」という記述から推測すると、以前の実装では、フォスターペアレンティングのロジックが、連続して生成されたノードに対して、それらが独立した兄弟ノードであるかのように処理してしまっていた可能性があります。p.fosterParenting = false
を設定することで、フォスターペアレンティングのロジックが、単一のトークンから生成された複数のノードに対して、より慎重に、かつ正しい親子関係を維持するように動作するようになる、と解釈できます。これは、パーサーが一度に一つのノードのみをフォスターペアレンティングの対象として考慮し、その後のノードは前のノードの子として適切に挿入されるようにするための調整である可能性があります。
この修正は、特に複雑なネストされたインライン要素(例: <a>
タグのネスト)がテーブル構造内で不正に配置された場合に、DOMツリーが正しく構築されることを保証します。
コアとなるコードの変更箇所
このコミットでは、主に以下の2つのファイルが変更されています。
-
src/pkg/html/parse.go
: HTMLパーサーの主要なロジックが含まれるファイルです。fosterParent
関数に1行追加。inCellIM
関数内のEndTagToken
のcase "td", "th"
ブロックが修正。
-
src/pkg/html/parse_test.go
: HTMLパーサーのテストケースが含まれるファイルです。TestParser
関数内のループ回数が変更 (i < 77
からi < 78
へ)。tests1.dat
のテストケース30に加えて、テストケース77もレンダリングと再パースのチェックをスキップする条件に追加。
コアとなるコードの解説
src/pkg/html/parse.go
の変更
fosterParent
関数の変更
@@ -126,6 +126,7 @@ func (p *parser) addChild(n *Node) {
// fosterParent adds a child node according to the foster parenting rules.
// Section 11.2.5.3, "foster parenting".
func (p *parser) fosterParent(n *Node) {
+\tp.fosterParenting = false
var table, parent *Node
var i int
for i = len(p.oe) - 1; i >= 0; i-- {
fosterParent
関数の冒頭に p.fosterParenting = false
が追加されました。このフラグは、パーサーが現在フォスターペアレンティングモードにあるかどうかを示すものと推測されます。この行を追加することで、フォスターペアレンティングの処理が開始されるたびに、このフラグがリセットされ、単一のトークンから複数のノードが生成されるような特殊なケースで、フォスターペアレンティングのロジックが誤って連続するノードを兄弟として扱ってしまうのを防ぐ効果があると考えられます。これにより、生成されたノードが正しく親子関係を持つように制御されます。
inCellIM
関数の変更
@@ -986,7 +987,12 @@ func inCellIM(p *parser) (insertionMode, bool) {
case EndTagToken:\n \t\tswitch p.tok.Data {\n \t\tcase "td", "th":\n-\t\t\t// TODO.\n+\t\t\tif !p.popUntil(tableScopeStopTags, p.tok.Data) {\n+\t\t\t\t// Ignore the token.\n+\t\t\t\treturn inCellIM, true\n+\t\t\t}\n+\t\t\tp.clearActiveFormattingElements()\n+\t\t\treturn inRowIM, true
\t\tcase "body", "caption", "col", "colgroup", "html":\n \t\t\t// TODO.\n \t\tcase "table", "tbody", "tfoot", "thead", "tr":
inCellIM
関数は、パーサーがテーブルセル (<td>
または <th>
) の内部にいるときの挿入モードを処理します。以前は </td>
または </th>
の終了タグが来た場合の処理が // TODO.
となっていました。
変更後、</td>
または </th>
が検出されると、以下の処理が実行されます。
if !p.popUntil(tableScopeStopTags, p.tok.Data)
: これは、現在のセル要素を閉じ、テーブル関連のスコープ(<table>
,<tbody>
,<tr>
など)まで要素スタックをポップする試みです。p.tok.Data
は現在のトークンデータ(td
またはth
)を指します。これにより、現在のセルが閉じられ、パーサーはテーブル行のコンテキストに戻ります。もしpopUntil
が失敗した場合(つまり、適切なスコープ停止タグが見つからなかった場合)、トークンは無視され、パーサーは引き続きinCellIM
に留まります。p.clearActiveFormattingElements()
: アクティブなフォーマット要素のリストをクリアします。これは、セル内で適用されていたフォーマットが、セルが閉じられた後に誤って後続のコンテンツに影響を与えないようにするために重要です。return inRowIM, true
: 処理が成功した場合、パーサーの挿入モードをinRowIM
(in row insertion mode) に変更し、処理が完了したことを示します。
この修正により、</td>
タグがHTML5の仕様に従って正しく処理され、テーブル構造の整合性が保たれるようになります。
src/pkg/html/parse_test.go
の変更
@@ -132,7 +132,7 @@ func TestParser(t *testing.T) {
\trc := make(chan io.Reader)\n \t\tgo readDat(filename, rc)\n \t\t// TODO(nigeltao): Process all test cases, not just a subset.\n-\t\tfor i := 0; i < 77; i++ {\n+\t\tfor i := 0; i < 78; i++ {\n \t\t\t// Parse the #data section.\n \t\t\tb, err := ioutil.ReadAll(<-rc)\n \t\t\tif err != nil {\n @@ -161,8 +161,8 @@ func TestParser(t *testing.T) {\n \t\t\t\tcontinue\n \t\t\t}\n \t\t\t// Check that rendering and re-parsing results in an identical tree.\n-\t\t\tif filename == "tests1.dat" && i == 30 {\n-\t\t\t\t// Test 30 in tests1.dat is such messed-up markup that a correct parse\n+\t\t\tif filename == "tests1.dat" && (i == 30 || i == 77) {\n+\t\t\t\t// Some tests in tests1.dat have such messed-up markup that a correct parse\n \t\t\t\t// results in a non-conforming tree (one <a> element nested inside another).\n \t\t\t\t// Therefore when it is rendered and re-parsed, it isn't the same.\n \t\t\t\t// So we skip rendering on that test.\n```
* **テストループの回数変更**: `for i := 0; i < 77; i++` が `for i := 0; i < 78; i++` に変更されました。これは、`tests1.dat` というテストデータファイルに新しいテストケース(テストケース77)が追加されたことを示しています。この新しいテストケースは、このコミットで修正された `</td>` タグの処理やフォスターペアレンティングの問題を検証するために追加されたものです。
* **レンダリングと再パースのスキップ条件の追加**: `tests1.dat` のテストケース30に加えて、テストケース77もレンダリングと再パースのチェックをスキップする条件に追加されました。これは、これらのテストケースが「非常にめちゃくちゃなマークアップ」を含んでおり、正しいパース結果が「非準拠のツリー」(例: <a>要素が別の<a>要素の中にネストされている)になるため、レンダリングして再パースしても元のツリーと同じにならないことを示しています。このようなテストケースは、パーサーが不正な入力に対してどのように堅牢に振る舞うかを検証するために重要ですが、その結果が必ずしも「理想的な」DOMツリーになるとは限らないため、特定の検証ステップをスキップする必要があります。
これらのテストファイルの変更は、新しいバグ修正が正しく機能することを確認し、同時にパーサーの既知の限界(非常に不正なマークアップに対する挙動)を考慮に入れていることを示しています。
## 関連リンク
* **Go Gerrit Code Review**: [https://golang.org/cl/5305074](https://golang.org/cl/5305074)
## 参考にした情報源リンク
* HTML5 Parsing Algorithm: [https://html.spec.whatwg.org/multipage/parsing.html](https://html.spec.whatwg.org/multipage/parsing.html)
* HTML5 Parsing - Insertion Modes: [https://html.spec.whatwg.org/multipage/parsing.html#insertion-modes](https://html.spec.whatwg.org/multipage/parsing.html#insertion-modes)
* HTML5 Parsing - Active Formatting Elements: [https://html.spec.whatwg.org/multipage/parsing.html#list-of-active-formatting-elements](https://html.spec.whatwg.org/multipage/parsing.html#list-of-active-formatting-elements)
* HTML5 Parsing - Foster Parenting: [https://html.spec.whatwg.org/multipage/parsing.html#foster-parenting](https://html.spec.whatwg.org/multipage/parsing.html#foster-parenting)
* Go言語のHTMLパーサーのソースコード (Goの公式リポジトリ): [https://github.com/golang/go/tree/master/src/html](https://github.com/golang/go/tree/master/src/html)
* Go言語のHTMLパーサーのテストデータ (Goの公式リポジトリ): [https://github.com/golang/go/tree/master/src/html/testdata](https://github.com/golang/go/tree/master/src/html/testdata)
* HTML5の仕様に関する一般的な情報源 (MDN Web Docsなど)