[インデックス 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")