[インデックス 13585] ファイルの概要
このコミットは、Go言語の実験的なHTMLパーサーパッケージ exp/html
における、HTML要素の「フォスターペアレンティング (foster-parenting)」に関するバグ修正です。特に、<nobr>
や <p>
のような要素が別の開始タグによって暗黙的に閉じられた場合に、フォスターペアレンティングが正しく機能しない問題を解決します。この修正により、HTML5の複雑なパースルール、特にテーブル内での不正な要素配置に対する堅牢性が向上しました。
コミット
commit 2276ab92c116b8ae376fd28850bb0cf845f6de49
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Tue Aug 7 09:35:09 2012 +1000
exp/html: fix foster-parenting when elements are implicitly closed
When an element (like <nobr> or <p>) was implicitly closed by another
start tag, it would keep foster parenting from working because
the check for what was on top of the stack of open elements was
in the wrong place.
Move the check to addChild.
Pass 2 additional tests.
R=nigeltao
CC=golang-dev
https://golang.org/cl/6460045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2276ab92c116b8ae376fd28850bb0cf845f6de49
元コミット内容
このコミットは、exp/html
パッケージにおけるフォスターペアレンティングの不具合を修正するものです。具体的には、<nobr>
や <p>
のような特定のHTML要素が、その終了タグがないにもかかわらず、別の開始タグ(例えば <table>
や <tr>
など)によって暗黙的に閉じられる際に、フォスターペアレンティングのロジックが正しく動作しないという問題がありました。
問題の根源は、オープン要素のスタックの最上位にある要素がフォスターペアレンティングの対象となるべきかどうかをチェックする場所が不適切であったことにあります。このチェックが誤った場所で行われていたため、暗黙的に閉じられた要素がフォスターペアレンティングのプロセスを妨げていました。
修正は、このチェックロジックを addChild
関数内に移動させることで行われました。これにより、要素がDOMツリーに追加される際に、より適切なタイミングでフォスターペアレンティングの条件が評価されるようになります。この変更により、2つの追加テストケースがパスするようになりました。
変更の背景
HTMLのパースは、一見単純に見えて非常に複雑なプロセスです。特に、ブラウザがどのように不正なHTML(well-formedでないHTML)を処理し、一貫したDOMツリーを構築するかは、HTML5仕様で厳密に定義されています。この仕様には、エラー回復メカニズムの一環として「フォスターペアレンティング」という概念が含まれています。
フォスターペアレンティングは、主にテーブル要素(<table>
, <tbody>
, <thead>
, <tfoot>
, <tr>
など)の内部に、本来配置されるべきではないコンテンツ(例えば、テキストノードやブロックレベル要素など)が誤って挿入された場合に適用されるルールです。HTMLの仕様では、これらの要素の内部に直接配置できるコンテンツの種類が厳しく制限されています。もし不正なコンテンツが検出された場合、ブラウザはエラーとしてパースを中断するのではなく、その不正なコンテンツをテーブルの外部(通常はテーブルの直前またはテーブルの親要素の直後)に「里子に出す」ように再配置します。これが「フォスターペアレンティング」と呼ばれる所以です。
このコミットの背景にある問題は、このフォスターペアレンティングのロジックが、HTMLの「暗黙的な要素の閉じ方」と組み合わさったときに発生していました。例えば、<p>
タグは、その後に別のブロックレベル要素(例: <div>
や別の <p>
)の開始タグが来た場合、明示的な </p>
終了タグがなくても自動的に閉じられます。同様に、<nobr>
のような要素も特定の状況で暗黙的に閉じられることがあります。
問題は、このような暗黙的な閉じが発生した際に、パーサーがオープン要素のスタックの最上位にある要素を正しく認識できず、結果としてフォスターペアレンティングの条件判断が狂ってしまい、本来フォスターペアレンティングされるべきコンテンツが正しく処理されない、あるいは予期せぬ場所に挿入されてしまうというものでした。このバグは、HTMLのレンダリング結果に予期せぬ影響を与える可能性がありました。
前提知識の解説
HTMLパーシングの基本
HTMLパーシングは、HTMLドキュメントを読み込み、ブラウザがレンダリングできるDOM (Document Object Model) ツリーに変換するプロセスです。このプロセスは通常、以下のステップで構成されます。
- トークン化 (Tokenization): HTMLの文字列を、開始タグ、終了タグ、テキスト、コメントなどの「トークン」に分解します。
- ツリー構築 (Tree Construction): トークンストリームを処理し、DOMツリーを構築します。この際、パーサーは「オープン要素のスタック (stack of open elements)」を維持し、現在開いている要素を追跡します。新しい要素が開始されるとスタックにプッシュされ、終了するとポップされます。
HTML5パーシングアルゴリズムとエラー回復
HTML5仕様は、ブラウザ間の一貫性を保証するために、非常に詳細なパーシングアルゴリズムを定義しています。このアルゴリズムは、不正なHTML(構文エラーのあるHTML)に対しても堅牢であり、エラーを検出してもパースを停止せず、可能な限りDOMツリーを構築しようとします。このエラー回復メカニズムの一部として、以下の重要な概念があります。
- 挿入モード (Insertion Modes): パーサーは、ドキュメントの現在の位置(例: "in body", "in table", "in head")に応じて異なる「挿入モード」で動作します。各モードには、特定のトークンが検出された場合の処理ルールが定義されています。
- 暗黙的な要素の閉じ方 (Implicit Closing of Elements): HTMLには、特定の要素が明示的な終了タグなしで閉じられるルールがあります。例えば、
<p>
要素は、別のブロックレベル要素(<div>
,<h1>
など)の開始タグが検出された場合、自動的に閉じられます。これは、HTMLの柔軟性を提供しますが、パーサーにとっては複雑な処理を要求します。 - フォスターペアレンティング (Foster Parenting): 前述の通り、テーブル関連要素(
<table>
,<tbody>
,<thead>
,<tfoot>
,<tr>
)の内部に、本来許可されていないコンテンツ(例: テキストノード、<div>
、<p>
など)が挿入された場合に適用される特殊なルールです。このような不正なコンテンツは、テーブルの外部(通常はテーブルの直前、またはテーブルの親要素の直後)に「里子に出され」、DOMツリーの別の場所に再配置されます。これにより、テーブルの構造が壊れるのを防ぎつつ、コンテンツが失われるのを避けます。
Go言語の exp/html
パッケージ
exp/html
は、Go言語の標準ライブラリの一部として提供されている html
パッケージの実験的な前身、またはその開発過程で使われたパッケージであると考えられます。HTML5仕様に準拠したパーサーの実装を目指しており、ブラウザの挙動を模倣して不正なHTMLも適切に処理することを目指しています。このようなパッケージは、Webスクレイピング、HTMLのサニタイズ、HTMLテンプレートエンジンのバックエンドなど、様々な用途で利用されます。
技術的詳細
このコミットの技術的詳細は、exp/html
パッケージのパーサー内部におけるフォスターペアレンティングのロジックと、オープン要素のスタックの管理方法に深く関わっています。
問題は、addChild
関数(新しいノードをDOMツリーに追加する役割を担う)と inTableIM
関数(パーサーがテーブル内部の挿入モードにあるかどうかを判断し、フォスターペアレンティングを有効にする役割を担う)の間の相互作用にありました。
元の実装では、inTableIM
関数内で、パーサーがテーブル関連要素(<table>
, <tbody>
, <tfoot>
, <thead>
, <tr>
)のいずれかの内部にいる場合に p.fosterParenting = true
を設定し、関数の終了時に defer func() { p.fosterParenting = false }()
を使って p.fosterParenting
フラグをリセットしていました。しかし、この p.fosterParenting
フラグが addChild
関数内で評価される際に、スタックの最上位要素のチェックが不適切でした。
具体的には、addChild
関数内で p.fosterParenting
が true
の場合、すぐに p.fosterParent(n)
を呼び出していました。しかし、フォスターペアレンティングが実際に適用されるべきかどうかは、スタックの最上位要素がテーブル関連要素であるかどうかに依存します。暗黙的に閉じられた要素(例: <p>
や <nobr>
) がスタックの最上位にある場合、p.fosterParenting
フラグは inTableIM
によって true
に設定されているにもかかわらず、実際にはフォスターペアレンティングの条件を満たしていませんでした。このミスマッチがバグの原因でした。
修正は以下の通りです。
-
addChild
関数内のロジック変更:fp
というローカル変数を導入し、フォスターペアレンティングが実際に適用されるべきかどうかを判断するためのフラグとして使用します。p.fosterParenting
がtrue
の場合にのみ、スタックの最上位要素 (p.top().DataAtom
) がa.Table
,a.Tbody
,a.Tfoot
,a.Thead
,a.Tr
のいずれかであるかをチェックします。- このチェックが
true
であればfp = true
と設定します。 - 最終的に、
if fp
の条件が満たされた場合にのみp.fosterParent(n)
を呼び出すように変更されました。これにより、フォスターペアレンティングの適用条件がより厳密になり、暗黙的に閉じられた要素がスタックの最上位にある場合に誤ってフォスターペアレンティングがトリガーされるのを防ぎます。
-
fosterParent
関数内の変更:- 関数冒頭にあった
p.fosterParenting = false
の行が削除されました。これは、fosterParenting
フラグの管理がinTableIM
とaddChild
に集約されたため、この関数内でリセットする必要がなくなったことを示唆しています。
- 関数冒頭にあった
-
inTableIM
関数内の変更:- 以前は
switch p.top().DataAtom
を使ってスタックの最上位要素をチェックし、テーブル関連要素の場合にのみp.fosterParenting = true
を設定していましたが、このチェックが削除されました。 - 代わりに、
inTableIM
関数に入ると無条件にp.fosterParenting = true
を設定し、関数終了時にdefer func() { p.fosterParenting = false }()
でリセットするように変更されました。これは、inTableIM
モードに入った時点でフォスターペアレンティングの可能性が常に存在し、実際の適用判断はaddChild
に委ねられるという設計思想の変更を反映しています。
- 以前は
この修正により、フォスターペアレンティングの条件判断が addChild
関数に一元化され、より正確なタイミングで、かつ適切な要素に対してのみ適用されるようになりました。
コアとなるコードの変更箇所
src/pkg/exp/html/parse.go
--- a/src/pkg/exp/html/parse.go
+++ b/src/pkg/exp/html/parse.go
@@ -208,7 +208,15 @@ loop:
// addChild adds a child node n to the top element, and pushes n onto the stack
// of open elements if it is an element node.
func (p *parser) addChild(n *Node) {
+ fp := false
if p.fosterParenting {
+ switch p.top().DataAtom {
+ case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr:
+ fp = true
+ }
+ }
+
+ if fp {
p.fosterParent(n)
} else {
p.top().Add(n)
@@ -222,7 +230,6 @@ func (p *parser) addChild(n *Node) {\
// fosterParent adds a child node according to the foster parenting rules.\
// Section 12.2.5.3, "foster parenting".\
func (p *parser) fosterParent(n *Node) {
- p.fosterParenting = false
var table, parent *Node
var i int
for i = len(p.oe) - 1; i >= 0; i-- {
@@ -1308,11 +1315,8 @@ func inTableIM(p *parser) bool {
return true
}
- switch p.top().DataAtom {
- case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr:
- p.fosterParenting = true
- defer func() { p.fosterParenting = false }()
- }
+ p.fosterParenting = true
+ defer func() { p.fosterParenting = false }()
return inBodyIM(p)
}
src/pkg/exp/html/parse_test.go
--- a/src/pkg/exp/html/parse_test.go
+++ b/src/pkg/exp/html/parse_test.go
@@ -389,6 +389,8 @@ var renderTestBlacklist = map[string]bool{
`<a href="blah">aba<table><a href="foo">br<tr><td></td></tr>x</table>aoe`: true,
`<a><table><a></table><p><a><div><a>`: true,
`<a><table><td><a><table></table><a></tr><a></table><a>`: true,
+ // A similar reparenting situation involving <nobr>:
+ `<!DOCTYPE html><body><b><nobr>1<table><nobr></b><i><nobr>2<nobr></i>3`: true,
// A <plaintext> element is reparented, putting it before a table.
// A <plaintext> element can't have anything after it in HTML.
`<table><plaintext><td>`: true,
src/pkg/exp/html/testlogs/tests26.dat.log
--- a/src/pkg/exp/html/testlogs/tests26.dat.log
+++ b/src/pkg/exp/html/testlogs/tests26.dat.log
@@ -1,6 +1,6 @@
PASS "<!DOCTYPE html><body><a href='#1'><nobr>1<nobr></a><br><a href='#2'><nobr>2<nobr></a><br><a href='#3'><nobr>3<nobr></a>"
PASS "<!DOCTYPE html><body><b><nobr>1<nobr></b><i><nobr>2<nobr></i>3"
-FAIL "<!DOCTYPE html><body><b><nobr>1<table><nobr></b><i><nobr>2<nobr></i>3"
+PASS "<!DOCTYPE html><body><b><nobr>1<table><nobr></b><i><nobr>2<nobr></i>3"
PASS "<!DOCTYPE html><body><b><nobr>1<table><tr><td><nobr></b><i><nobr>2<nobr></i>3"
PASS "<!DOCTYPE html><body><b><nobr>1<div><nobr></b><i><nobr>2<nobr></i>3"
PASS "<!DOCTYPE html><body><b><nobr>1<nobr></b><div><i><nobr>2<nobr></i>3"
src/pkg/exp/html/testlogs/tricky01.dat.log
--- a/src/pkg/exp/html/testlogs/tricky01.dat.log
+++ b/src/pkg/exp/html/testlogs/tricky01.dat.log
@@ -4,6 +4,6 @@ PASS "<html><body>\n<p><font size=\"7\">First paragraph.</p>\n<p>Second paragrap
PASS "<html>\n<dl>\n<dt><b>Boo\n<dd>Goo?\n</dl>\n</html>"
PASS "<html><body>\n<label><a><div>Hello<div>World</div></a></label> \n</body></html>"
PASS "<table><center> <font>a</center> <img> <tr><td> </td> </tr> </table>"
-FAIL "<table><tr><p><a><p>You should see this text."
+PASS "<table><tr><p><a><p>You should see this text."
PASS "<TABLE>\n<TR>\n<CENTER><CENTER><TD></TD></TR><TR>\n<FONT>\n<TABLE><tr></tr></TABLE>\n</P>\n<a></font><font></a>\nThis page contains an insanely badly-nested tag sequence."
PASS "<html>\n<body>\n<b><nobr><div>This text is in a div inside a nobr</nobr>More text that should not be in the nobr, i.e., the\nnobr should have closed the div inside it implicitly. </b><pre>A pre tag outside everything else.</pre>\n</body>\n</html>"
コアとなるコードの解説
src/pkg/exp/html/parse.go
の変更点
-
addChild
関数:fp := false
という新しいブール型変数fp
(foster parentingの略) が導入されました。これは、現在のノードがフォスターペアレンティングされるべきかどうかを判断するための一時的なフラグです。if p.fosterParenting
ブロックの内部に、switch p.top().DataAtom
ステートメントが移動されました。これにより、p.fosterParenting
フラグがtrue
の場合にのみ、オープン要素のスタックの最上位にある要素が<table>
,<tbody>
,<tfoot>
,<thead>
,<tr>
のいずれかであるかどうかがチェックされます。- もしこれらの条件が満たされれば、
fp
はtrue
に設定されます。 - 最終的に、
if fp
の条件がtrue
の場合にのみp.fosterParent(n)
が呼び出されます。この変更により、フォスターペアレンティングの適用が、p.fosterParenting
がtrue
であることに加えて、スタックの最上位要素がテーブル関連要素であるという二重の条件を満たす場合に限定されるようになりました。これにより、暗黙的に閉じられた要素がスタックの最上位にある場合に誤ってフォスターペアレンティングがトリガーされるのを防ぎます。
-
fosterParent
関数:- 関数冒頭にあった
p.fosterParenting = false
の行が削除されました。これは、p.fosterParenting
フラグのライフサイクル管理がinTableIM
とaddChild
に移譲されたため、この関数内で明示的にリセットする必要がなくなったことを意味します。
- 関数冒頭にあった
-
inTableIM
関数:- 以前は、
switch p.top().DataAtom
を使用してスタックの最上位要素がテーブル関連要素であるかをチェックし、その場合にのみp.fosterParenting = true
を設定していました。このswitch
ブロックが完全に削除されました。 - 代わりに、
inTableIM
関数に入ると、無条件にp.fosterParenting = true
が設定されるようになりました。そして、defer func() { p.fosterParenting = false }()
によって、この関数が終了する際に必ずp.fosterParenting
がfalse
にリセットされるようになりました。この変更は、パーサーがテーブル内部の挿入モードにある間は常にフォスターペアレンティングの可能性を考慮し、実際の適用判断はaddChild
関数に委ねるという設計思想の変更を反映しています。
- 以前は、
テストファイルの変更点
-
src/pkg/exp/html/parse_test.go
:renderTestBlacklist
マップに新しいテストケースが追加されました。
このテストケースは、<!DOCTYPE html><body><b><nobr>1<table><nobr></b><i><nobr>2<nobr></i>3
<nobr>
要素と<table>
要素が絡む複雑なリペアレンティング(再親子付け)の状況をシミュレートしており、フォスターペアレンティングのバグを顕在化させるためのものです。コメントにも「A similar reparenting situation involving:」と明記されています。
-
src/pkg/exp/html/testlogs/tests26.dat.log
およびsrc/pkg/exp/html/testlogs/tricky01.dat.log
:- これらのログファイルは、テストの実行結果を記録するものです。
tests26.dat.log
では、上記で追加されたテストケース<!DOCTYPE html><body><b><nobr>1<table><nobr></b><i><nobr>2<nobr></i>3
の結果がFAIL
からPASS
に変更されています。これは、修正がこの特定のシナリオでフォスターペアレンティングを正しく機能させたことを示しています。tricky01.dat.log
では、別のテストケース<table><tr><p><a><p>You should see this text.
の結果がFAIL
からPASS
に変更されています。このテストケースも、テーブル内での不正な<p>
要素の配置とフォスターペアレンティングに関連するものであり、修正が広範なフォスターペアレンティングの問題を解決したことを裏付けています。
これらの変更は、HTML5パーシングアルゴリズムの複雑な側面、特にエラー回復と要素の再配置に関するGo言語の実装の成熟度を示すものです。
関連リンク
- Go言語の
html
パッケージ (Go 1.0以降の標準パッケージ): https://pkg.go.dev/golang.org/x/net/html (このコミットのexp/html
は、このパッケージの前身または関連する実験的なバージョンである可能性が高いです。) - HTML5仕様 - 13.2.6.4.5 The "in table" insertion mode: https://html.spec.whatwg.org/multipage/parsing.html#the-in-table-insertion-mode
- HTML5仕様 - 13.2.6.4.6 The "in table body" insertion mode: https://html.spec.whatwg.org/multipage/parsing.html#the-in-table-body-insertion-mode
- HTML5仕様 - 13.2.6.4.7 The "in row" insertion mode: https://html.spec.whatwg.org/multipage/parsing.html#the-in-row-insertion-mode
- HTML5仕様 - 13.2.6.4.8 The "in cell" insertion mode: https://html.spec.whatwg.org/multipage/parsing.html#the-in-cell-insertion-mode
- 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.2 The stack of open elements: https://html.spec.whatwg.org/multipage/parsing.html#the-stack-of-open-elements
- HTML5仕様 - 13.2.6.4.10 Foster parenting: https://html.spec.whatwg.org/multipage/parsing.html#foster-parenting
参考にした情報源リンク
- Go言語の公式ドキュメント
- HTML5仕様 (WHATWG)
- Web上のHTMLパーシングに関する技術記事
- Go言語のGerritコードレビューシステム (golang.org/cl)
- GitHubのgolang/goリポジトリ
[インデックス 13585] ファイルの概要
このコミットは、Go言語の実験的なHTMLパーサーパッケージ exp/html
における、HTML要素の「フォスターペアレンティング (foster-parenting)」に関する重要なバグ修正です。特に、<nobr>
や <p>
のような特定のHTML要素が、その終了タグがないにもかかわらず、別の開始タグによって暗黙的に閉じられた場合に、フォスターペアレンティングのロジックが正しく機能しないという問題を解決します。この修正により、HTML5の複雑なパースルール、特にテーブル内での不正な要素配置に対する堅牢性が向上し、ブラウザの挙動により近いDOMツリー構築が実現されました。
コミット
commit 2276ab92c116b8ae376fd28850bb0cf845f6de49
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Tue Aug 7 09:35:09 2012 +1000
exp/html: fix foster-parenting when elements are implicitly closed
When an element (like <nobr> or <p>) was implicitly closed by another
start tag, it would keep foster parenting from working because
the check for what was on top of the stack of open elements was
in the wrong place.
Move the check to addChild.
Pass 2 additional tests.
R=nigeltao
CC=golang-dev
https://golang.org/cl/6460045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2276ab92c116b8ae376fd28850bb0cf845f6de49
元コミット内容
このコミットは、Go言語の exp/html
パッケージにおけるフォスターペアレンティングの不具合を修正するものです。具体的には、<nobr>
や <p>
のような特定のHTML要素が、その終了タグがないにもかかわらず、別の開始タグ(例えば <table>
や <tr>
など)によって暗黙的に閉じられる際に、フォスターペアレンティングのロジックが正しく動作しないという問題がありました。
問題の根源は、オープン要素のスタックの最上位にある要素がフォスターペアレンティングの対象となるべきかどうかをチェックする場所が不適切であったことにあります。このチェックが誤った場所で行われていたため、暗黙的に閉じられた要素がフォスターペアレンティングのプロセスを妨げていました。
修正は、このチェックロジックを addChild
関数内に移動させることで行われました。これにより、要素がDOMツリーに追加される際に、より適切なタイミングでフォスターペアレンティングの条件が評価されるようになります。この変更により、2つの追加テストケースがパスするようになりました。
変更の背景
HTMLのパースは、一見単純に見えて非常に複雑なプロセスです。特に、ブラウザがどのように不正なHTML(well-formedでないHTML)を処理し、一貫したDOMツリーを構築するかは、HTML5仕様で厳密に定義されています。この仕様には、エラー回復メカニズムの一環として「フォスターペアレンティング」という概念が含まれています。
フォスターペアレンティングは、主にテーブル要素(<table>
, <tbody>
, <thead>
, <tfoot>
, <tr>
など)の内部に、本来配置されるべきではないコンテンツ(例えば、テキストノードやブロックレベル要素など)が誤って挿入された場合に適用されるルールです。HTMLの仕様では、これらの要素の内部に直接配置できるコンテンツの種類が厳しく制限されています。もし不正なコンテンツが検出された場合、ブラウザはエラーとしてパースを中断するのではなく、その不正なコンテンツをテーブルの外部(通常はテーブルの直前またはテーブルの親要素の直後)に「里子に出す」ように再配置します。これが「フォスターペアレンティング」と呼ばれる所以です。
このコミットの背景にある問題は、このフォスターペアレンティングのロジックが、HTMLの「暗黙的な要素の閉じ方」と組み合わさったときに発生していました。例えば、<p>
タグは、その後に別のブロックレベル要素(例: <div>
や別の <p>
)の開始タグが来た場合、明示的な </p>
終了タグがなくても自動的に閉じられます。同様に、<nobr>
のような要素も特定の状況で暗黙的に閉じられることがあります。
問題は、このような暗黙的な閉じが発生した際に、パーサーがオープン要素のスタックの最上位にある要素を正しく認識できず、結果としてフォスターペアレンティングの条件判断が狂ってしまい、本来フォスターペアレンティングされるべきコンテンツが正しく処理されない、あるいは予期せぬ場所に挿入されてしまうというものでした。このバグは、HTMLのレンダリング結果に予期せぬ影響を与える可能性がありました。
前提知識の解説
HTMLパーシングの基本
HTMLパーシングは、HTMLドキュメントを読み込み、ブラウザがレンダリングできるDOM (Document Object Model) ツリーに変換するプロセスです。このプロセスは通常、以下のステップで構成されます。
- トークン化 (Tokenization): HTMLの文字列を、開始タグ、終了タグ、テキスト、コメントなどの「トークン」に分解します。
- ツリー構築 (Tree Construction): トークンストリームを処理し、DOMツリーを構築します。この際、パーサーは「オープン要素のスタック (stack of open elements)」を維持し、現在開いている要素を追跡します。新しい要素が開始されるとスタックにプッシュされ、終了するとポップされます。
HTML5パーシングアルゴリズムとエラー回復
HTML5仕様は、ブラウザ間の一貫性を保証するために、非常に詳細なパーシングアルゴリズムを定義しています。このアルゴリズムは、不正なHTML(構文エラーのあるHTML)に対しても堅牢であり、エラーを検出してもパースを停止せず、可能な限りDOMツリーを構築しようとします。このエラー回復メカニズムの一部として、以下の重要な概念があります。
- 挿入モード (Insertion Modes): パーサーは、ドキュメントの現在の位置(例: "in body", "in table", "in head")に応じて異なる「挿入モード」で動作します。各モードには、特定のトークンが検出された場合の処理ルールが定義されています。
- 暗黙的な要素の閉じ方 (Implicit Closing of Elements): HTMLには、特定の要素が明示的な終了タグなしで閉じられるルールがあります。例えば、
<p>
要素は、別のブロックレベル要素(<div>
,<h1>
など)の開始タグが検出された場合、自動的に閉じられます。これは、HTMLの柔軟性を提供しますが、パーサーにとっては複雑な処理を要求します。 - フォスターペアレンティング (Foster Parenting): 前述の通り、テーブル関連要素(
<table>
,<tbody>
,<thead>
,<tfoot>
,<tr>
)の内部に、本来許可されていないコンテンツ(例: テキストノード、<div>
、<p>
など)が挿入された場合に適用される特殊なルールです。このような不正なコンテンツは、テーブルの外部(通常はテーブルの直前、またはテーブルの親要素の直後)に「里子に出され」、DOMツリーの別の場所に再配置されます。これにより、テーブルの構造が壊れるのを防ぎつつ、コンテンツが失われるのを避けます。
Go言語の exp/html
パッケージ
exp/html
は、Go言語の標準ライブラリの一部として提供されている html
パッケージの実験的な前身、またはその開発過程で使われたパッケージであると考えられます。HTML5仕様に準拠したパーサーの実装を目指しており、ブラウザの挙動を模倣して不正なHTMLも適切に処理することを目指しています。このようなパッケージは、Webスクレイピング、HTMLのサニタイズ、HTMLテンプレートエンジンのバックエンドなど、様々な用途で利用されます。
技術的詳細
このコミットの技術的詳細は、exp/html
パッケージのパーサー内部におけるフォスターペアレンティングのロジックと、オープン要素のスタックの管理方法に深く関わっています。
問題は、addChild
関数(新しいノードをDOMツリーに追加する役割を担う)と inTableIM
関数(パーサーがテーブル内部の挿入モードにあるかどうかを判断し、フォスターペアレンティングを有効にする役割を担う)の間の相互作用にありました。
元の実装では、inTableIM
関数内で、パーサーがテーブル関連要素(<table>
, <tbody>
, <tfoot>
, <thead>
, <tr>
)のいずれかの内部にいる場合に p.fosterParenting = true
を設定し、関数の終了時に defer func() { p.fosterParenting = false }()
を使って p.fosterParenting
フラグをリセットしていました。しかし、この p.fosterParenting
フラグが addChild
関数内で評価される際に、スタックの最上位要素のチェックが不適切でした。
具体的には、addChild
関数内で p.fosterParenting
が true
の場合、すぐに p.fosterParent(n)
を呼び出していました。しかし、フォスターペアレンティングが実際に適用されるべきかどうかは、スタックの最上位要素がテーブル関連要素であるかどうかに依存します。暗黙的に閉じられた要素(例: <p>
や <nobr>
) がスタックの最上位にある場合、p.fosterParenting
フラグは inTableIM
によって true
に設定されているにもかかわらず、実際にはフォスターペアレンティングの条件を満たしていませんでした。このミスマッチがバグの原因でした。
修正は以下の通りです。
-
addChild
関数内のロジック変更:fp
というローカル変数を導入し、フォスターペアレンティングが実際に適用されるべきかどうかを判断するためのフラグとして使用します。p.fosterParenting
がtrue
の場合にのみ、スタックの最上位要素 (p.top().DataAtom
) がa.Table
,a.Tbody
,a.Tfoot
,a.Thead
,a.Tr
のいずれかであるかをチェックします。- このチェックが
true
であればfp = true
と設定します。 - 最終的に、
if fp
の条件が満たされた場合にのみp.fosterParent(n)
を呼び出すように変更されました。これにより、フォスターペアレンティングの適用条件がより厳密になり、暗黙的に閉じられた要素がスタックの最上位にある場合に誤ってフォスターペアレンティングがトリガーされるのを防ぎます。
-
fosterParent
関数内の変更:- 関数冒頭にあった
p.fosterParenting = false
の行が削除されました。これは、fosterParenting
フラグの管理がinTableIM
とaddChild
に集約されたため、この関数内でリセットする必要がなくなったことを示唆しています。
- 関数冒頭にあった
-
inTableIM
関数内の変更:- 以前は
switch p.top().DataAtom
を使ってスタックの最上位要素をチェックし、テーブル関連要素の場合にのみp.fosterParenting = true
を設定していましたが、このチェックが削除されました。 - 代わりに、
inTableIM
関数に入ると無条件にp.fosterParenting = true
を設定し、関数終了時にdefer func() { p.fosterParenting = false }()
でリセットするように変更されました。これは、inTableIM
モードに入った時点でフォスターペアレンティングの可能性が常に存在し、実際の適用判断はaddChild
に委ねられるという設計思想の変更を反映しています。
- 以前は
この修正により、フォスターペアレンティングの条件判断が addChild
関数に一元化され、より正確なタイミングで、かつ適切な要素に対してのみ適用されるようになりました。
コアとなるコードの変更箇所
src/pkg/exp/html/parse.go
--- a/src/pkg/exp/html/parse.go
+++ b/src/pkg/exp/html/parse.go
@@ -208,7 +208,15 @@ loop:
// addChild adds a child node n to the top element, and pushes n onto the stack
// of open elements if it is an element node.
func (p *parser) addChild(n *Node) {
+ fp := false
if p.fosterParenting {
+ switch p.top().DataAtom {
+ case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr:
+ fp = true
+ }
+ }
+
+ if fp {
p.fosterParent(n)
} else {
p.top().Add(n)
@@ -222,7 +230,6 @@ func (p *parser) addChild(n *Node) {\
// fosterParent adds a child node according to the foster parenting rules.\
// Section 12.2.5.3, "foster parenting".\
func (p *parser) fosterParent(n *Node) {
- p.fosterParenting = false
var table, parent *Node
var i int
for i = len(p.oe) - 1; i >= 0; i-- {
@@ -1308,11 +1315,8 @@ func inTableIM(p *parser) bool {
return true
}
- switch p.top().DataAtom {
- case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr:
- p.fosterParenting = true
- defer func() { p.fosterParenting = false }()
- }
+ p.fosterParenting = true
+ defer func() { p.fosterParenting = false }()
return inBodyIM(p)
}
src/pkg/exp/html/parse_test.go
--- a/src/pkg/exp/html/parse_test.go
+++ b/src/pkg/exp/html/parse_test.go
@@ -389,6 +389,8 @@ var renderTestBlacklist = map[string]bool{
`<a href="blah">aba<table><a href="foo">br<tr><td></td></tr>x</table>aoe`: true,
`<a><table><a></table><p><a><div><a>`: true,
`<a><table><td><a><table></table><a></tr><a></table><a>`: true,
+ // A similar reparenting situation involving <nobr>:
+ `<!DOCTYPE html><body><b><nobr>1<table><nobr></b><i><nobr>2<nobr></i>3`: true,
// A <plaintext> element is reparented, putting it before a table.
// A <plaintext> element can't have anything after it in HTML.
`<table><plaintext><td>`: true,
src/pkg/exp/html/testlogs/tests26.dat.log
--- a/src/pkg/exp/html/testlogs/tests26.dat.log
+++ b/src/pkg/exp/html/testlogs/tests26.dat.log
@@ -1,6 +1,6 @@
PASS "<!DOCTYPE html><body><a href='#1'><nobr>1<nobr></a><br><a href='#2'><nobr>2<nobr></a><br><a href='#3'><nobr>3<nobr></a>"
PASS "<!DOCTYPE html><body><b><nobr>1<nobr></b><i><nobr>2<nobr></i>3"
-FAIL "<!DOCTYPE html><body><b><nobr>1<table><nobr></b><i><nobr>2<nobr></i>3"
+PASS "<!DOCTYPE html><body><b><nobr>1<table><nobr></b><i><nobr>2<nobr></i>3"
PASS "<!DOCTYPE html><body><b><nobr>1<table><tr><td><nobr></b><i><nobr>2<nobr></i>3"
PASS "<!DOCTYPE html><body><b><nobr>1<div><nobr></b><i><nobr>2<nobr></i>3"
PASS "<!DOCTYPE html><body><b><nobr>1<nobr></b><div><i><nobr>2<nobr></i>3"
src/pkg/exp/html/testlogs/tricky01.dat.log
--- a/src/pkg/exp/html/testlogs/tricky01.dat.log
+++ b/src/pkg/exp/html/testlogs/tricky01.dat.log
@@ -4,6 +4,6 @@ PASS "<html><body>\n<p><font size=\"7\">First paragraph.</p>\n<p>Second paragrap
PASS "<html>\n<dl>\n<dt><b>Boo\n<dd>Goo?\n</dl>\n</html>"
PASS "<html><body>\n<label><a><div>Hello<div>World</div></a></label> \n</body></html>"
PASS "<table><center> <font>a</center> <img> <tr><td> </td> </tr> </table>"
-FAIL "<table><tr><p><a><p>You should see this text."
+PASS "<table><tr><p><a><p>You should see this text."
PASS "<TABLE>\n<TR>\n<CENTER><CENTER><TD></TD></TR><TR>\n<FONT>\n<TABLE><tr></tr></TABLE>\n</P>\n<a></font><font></a>\nThis page contains an insanely badly-nested tag sequence."
PASS "<html>\n<body>\n<b><nobr><div>This text is in a div inside a nobr</nobr>More text that should not be in the nobr, i.e., the\nnobr should have closed the div inside it implicitly. </b><pre>A pre tag outside everything else.</pre>\n</body>\n</html>"
コアとなるコードの解説
src/pkg/exp/html/parse.go
の変更点
-
addChild
関数:fp := false
の追加: 新しいブール型変数fp
(foster parentingの略) が導入されました。これは、現在のノードがフォスターペアレンティングされるべきかどうかを判断するための一時的なフラグです。switch p.top().DataAtom
の移動と条件の追加: 以前はinTableIM
関数内でスタックの最上位要素のチェックが行われていましたが、このロジックがaddChild
関数内のif p.fosterParenting
ブロックの内部に移動されました。これにより、p.fosterParenting
フラグがtrue
の場合にのみ、オープン要素のスタックの最上位にある要素が<table>
,<tbody>
,<tfoot>
,<thead>
,<tr>
のいずれかであるかどうかがチェックされます。もしこれらの条件が満たされれば、fp
はtrue
に設定されます。if fp
条件の導入: 最終的に、if fp
の条件がtrue
の場合にのみp.fosterParent(n)
が呼び出されます。この変更により、フォスターペアレンティングの適用が、p.fosterParenting
がtrue
であることに加えて、スタックの最上位要素がテーブル関連要素であるという二重の条件を満たす場合に限定されるようになりました。これにより、暗黙的に閉じられた要素がスタックの最上位にある場合に誤ってフォスターペアレンティングがトリガーされるのを防ぎ、より正確なDOMツリー構築を可能にします。
-
fosterParent
関数:p.fosterParenting = false
の削除: 関数冒頭にあったp.fosterParenting = false
の行が削除されました。これは、p.fosterParenting
フラグのライフサイクル管理がinTableIM
とaddChild
に移譲されたため、この関数内で明示的にリセットする必要がなくなったことを意味します。これにより、フラグの管理が一元化され、コードの意図が明確になります。
-
inTableIM
関数:switch p.top().DataAtom
ブロックの削除: 以前は、switch p.top().DataAtom
を使用してスタックの最上位要素がテーブル関連要素であるかをチェックし、その場合にのみp.fosterParenting = true
を設定していました。このswitch
ブロックが完全に削除されました。- 無条件の
p.fosterParenting = true
設定とdefer
の維持: 代わりに、inTableIM
関数に入ると、無条件にp.fosterParenting = true
が設定されるようになりました。そして、defer func() { p.fosterParenting = false }()
によって、この関数が終了する際に必ずp.fosterParenting
がfalse
にリセットされるようになりました。この変更は、パーサーがテーブル内部の挿入モードにある間は常にフォスターペアレンティングの可能性を考慮し、実際の適用判断はaddChild
関数に委ねるという設計思想の変更を反映しています。これにより、inTableIM
の役割が「テーブル挿入モードであること」を示すことに特化され、フォスターペアレンティングの具体的な適用ロジックはaddChild
に集約されました。
テストファイルの変更点
-
src/pkg/exp/html/parse_test.go
:renderTestBlacklist
マップに新しいテストケースが追加されました。
このテストケースは、<!DOCTYPE html><body><b><nobr>1<table><nobr></b><i><nobr>2<nobr></i>3
<nobr>
要素と<table>
要素が絡む複雑なリペアレンティング(再親子付け)の状況をシミュレートしており、フォスターペアレンティングのバグを顕在化させるためのものです。コメントにも「A similar reparenting situation involving:」と明記されており、この特定のシナリオが修正のターゲットであったことが示唆されます。
-
src/pkg/exp/html/testlogs/tests26.dat.log
およびsrc/pkg/exp/html/testlogs/tricky01.dat.log
:- これらのログファイルは、テストの実行結果を記録するものです。
tests26.dat.log
では、上記で追加されたテストケース<!DOCTYPE html><body><b><nobr>1<table><nobr></b><i><nobr>2<nobr></i>3
の結果がFAIL
からPASS
に変更されています。これは、修正がこの特定のシナリオでフォスターペアレンティングを正しく機能させたことを明確に示しています。tricky01.dat.log
では、別のテストケース<table><tr><p><a><p>You should see this text.
の結果がFAIL
からPASS
に変更されています。このテストケースも、テーブル内での不正な<p>
要素の配置とフォスターペアレンティングに関連するものであり、修正が広範なフォスターペアレンティングの問題を解決し、パーサーの堅牢性を向上させたことを裏付けています。
これらの変更は、HTML5パーシングアルゴリズムの複雑な側面、特にエラー回復と要素の再配置に関するGo言語の実装の成熟度を示すものです。
関連リンク
- Go言語の
html
パッケージ (Go 1.0以降の標準パッケージ): https://pkg.go.dev/golang.org/x/net/html (このコミットのexp/html
は、このパッケージの前身または関連する実験的なバージョンである可能性が高いです。) - HTML5仕様 - 13.2.6.4.5 The "in table" insertion mode: https://html.spec.whatwg.org/multipage/parsing.html#the-in-table-insertion-mode
- HTML5仕様 - 13.2.6.4.6 The "in table body" insertion mode: https://html.spec.whatwg.org/multipage/parsing.html#the-in-table-body-insertion-mode
- HTML5仕様 - 13.2.6.4.7 The "in row" insertion mode: https://html.spec.whatwg.org/multipage/parsing.html#the-in-row-insertion-mode
- HTML5仕様 - 13.2.6.4.8 The "in cell" insertion mode: https://html.spec.whatwg.org/multipage/parsing.html#the-in-cell-insertion-mode
- 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.2 The stack of open elements: https://html.spec.whatwg.org/multipage/parsing.html#the-stack-of-open-elements
- HTML5仕様 - 13.2.6.4.10 Foster parenting: https://html.spec.whatwg.org/multipage/parsing.html#foster-parenting
参考にした情報源リンク
- Go言語の公式ドキュメント
- HTML5仕様 (WHATWG)
- Web上のHTMLパーシングに関する技術記事
- Go言語のGerritコードレビューシステム (golang.org/cl)
- GitHubのgolang/goリポジトリ
- Google Web Search (query: "HTML5 parsing algorithm foster parenting implicitly closed elements")