[インデックス 10397] ファイルの概要
このコミットは、Go言語のhtmlパッケージにおけるHTMLパーサーの挙動を修正するものです。具体的には、<p>要素の後に<form>要素が開始された際に、HTML5の仕様に従って<p>要素を自動的に閉じるようにパーサーのロジックが変更されています。これにより、不正なHTML構造が入力された場合でも、より正確なDOMツリーが生成されるようになります。
コミット
commit b91d82258fdab9568f2ccef9f80669c764d4c7ac
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Tue Nov 15 15:31:22 2011 +1100
html: auto-close <p> elements when starting <form> element.
Pass tests2.dat, test 26:
<!doctypehtml><p><form>
| <!DOCTYPE html>
| <html>
| <head>
| <body>
| <p>
| <form>
Also pass tests through test 32:
<!DOCTYPE html><!-- X
R=nigeltao
CC=golang-dev
https://golang.org/cl/5369114
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b91d82258fdab9568f2ccef9f80669c764d4c7ac
元コミット内容
このコミットの元の内容は、<p>要素の自動閉じに関する修正です。具体的には、<p>要素の直後に<form>要素が出現した場合に、<p>要素を自動的に閉じるようにHTMLパーサーのロジックが変更されました。これにより、<!doctypehtml><p><form>のようなHTMLスニペットが、HTML5の仕様に準拠したDOM構造(<p>が閉じられ、その後に<form>が開始される)として正しくパースされるようになります。
また、この変更はtests2.datのテスト26をパスさせるために行われ、さらにテスト32までの他のテストも引き続きパスすることを確認しています。
変更の背景
HTMLは非常に寛容な言語であり、多くのブラウザは不正なマークアップに対してもエラーを発生させずに表示しようとします。この「エラー回復」のメカニズムは、HTMLの普及に貢献しましたが、同時にパーサーの実装を複雑にしています。特に、要素の開始タグが出現した際に、先行する要素を自動的に閉じるべきかどうかのルールは、HTML5の仕様で厳密に定義されています。
<p>要素は、フローコンテンツを内包できるブロックレベル要素ですが、特定の要素(例えば、別のブロックレベル要素やフォーム関連要素)が開始されると、明示的な終了タグがなくても自動的に閉じられるという特性を持っています。これは、ブラウザがHTMLをパースする際の一般的な挙動であり、HTML5の仕様にも明記されています。
このコミットが行われた背景には、Go言語のhtmlパッケージのパーサーが、このHTML5の自動閉じルール、特に<p>と<form>の間の相互作用を正しく処理できていなかったという問題があります。<!doctypehtml><p><form>のようなマークアップが与えられた場合、パーサーは<p>要素を閉じずに<form>要素をその子要素として扱ってしまう可能性がありました。これは、ブラウザの挙動やHTML5の仕様とは異なるため、互換性の問題や予期せぬDOM構造の生成につながります。
この修正は、htmlパッケージのパーサーがより堅牢で、HTML5の仕様に準拠した挙動を示すようにするための重要なステップでした。
前提知識の解説
HTML5のパースルール
HTML5のパースアルゴリズムは、非常に詳細かつ厳密に定義されており、ブラウザ間の互換性を高めることを目的としています。このアルゴリズムは、トークン化とツリー構築の2つの主要なフェーズに分かれています。
- トークン化: 入力されたHTML文字列を、タグ、属性、テキストなどの個々のトークンに分解します。
- ツリー構築: トークンストリームを処理し、DOMツリーを構築します。このフェーズでは、要素のネストルール、自動閉じルール、エラー回復メカニズムなどが適用されます。
要素の自動閉じ(Implicit Closures)
HTMLでは、一部の要素は特定の条件が満たされたときに、明示的な終了タグがなくても自動的に閉じられます。これは「暗黙的な閉じ(Implicit Closures)」または「タグ省略(Tag Omission)」ルールとして知られています。
例えば、<p>要素は、その後に別のブロックレベル要素(<div>, <h1>, <table>など)や、特定のインライン要素(<img>, <br>など)ではない要素、あるいはフォーム関連要素(<form>, <fieldset>など)が開始された場合に自動的に閉じられます。これは、<p>要素が「パラグラフ」を表すため、その中に別のパラグラフや構造的な要素が直接ネストされることは通常ないというセマンティクスに基づいています。
<p>要素と<form>要素の関係
HTML5の仕様では、<p>要素のコンテンツモデルは「フローコンテンツ」ですが、<p>要素自体は「パラグラフ」を意味するため、その中にブロックレベル要素を直接ネストすることはできません。<form>要素はブロックレベル要素であり、フローコンテンツに分類されますが、<p>要素の子要素として直接配置することはできません。
したがって、<!doctypehtml><p><form>のようなマークアップが出現した場合、HTML5のパースルールに従うと、<form>タグが開始される前に、現在開いている<p>要素は自動的に閉じられる必要があります。これにより、<form>要素は<p>要素の兄弟要素としてDOMツリーに配置され、正しい構造が維持されます。
Go言語のhtmlパッケージ
Go言語の標準ライブラリには、HTMLのパースとレンダリングを扱うhtmlパッケージが含まれています。このパッケージは、HTML5の仕様に準拠したパーサーを提供することを目指しており、ウェブスクレイピング、HTMLテンプレートの処理、HTMLのサニタイズなど、様々な用途で利用されます。
パーサーは、入力されたHTMLをトークン化し、DOMツリー(html.Node構造体で表現される)を構築します。この過程で、HTML5のパースアルゴリズムに定義されている要素のネストルールや自動閉じルールが適用されます。
技術的詳細
このコミットの技術的詳細を理解するためには、Go言語のhtmlパッケージのパーサーがどのように動作するか、特に「挿入モード(Insertion Mode)」の概念を理解することが重要です。
HTML5のパースアルゴリズムでは、パーサーは常に特定の「挿入モード」で動作します。このモードは、次に処理されるトークンがDOMツリーのどこに挿入されるべきか、また、どの要素が自動的に閉じられるべきかを決定します。多くの要素は「in body」挿入モードで処理されます。
src/pkg/html/parse.goのinBodyIM関数は、<body>要素内でのトークン処理ロジックを定義しています。この関数は、現在のトークンのデータ(タグ名)に基づいて、様々なケースを処理します。
変更前は、inBodyIM関数内で<form>タグが検出された際の特別な処理が不足していました。その結果、<p>要素が開いている状態で<form>タグが来ても、<p>が自動的に閉じられず、不正なネスト構造が生成される可能性がありました。
このコミットでは、inBodyIM関数にcase "form":のブロックが追加されました。このブロックのロジックは以下の通りです。
if p.form == nil: これは、まだ<form>要素がDOMツリーに存在しないことを確認しています。HTML5の仕様では、<form>要素は一度に一つしかアクティブにできないため、既に<form>要素が存在する場合は、新しい<form>要素は無視されるか、特別な処理が行われます。このケースでは、新しい<form>要素の開始を処理しています。p.popUntil(buttonScopeStopTags, "p"): この行がこのコミットの核心です。p.popUntil: これは、パーサーのスタック(現在開いている要素のリスト)から要素をポップ(削除)していく関数です。buttonScopeStopTags: これは、<button>要素のスコープを停止させるタグのセットです。このセットには、<p>要素も含まれています。つまり、<p>要素がスタックのトップにある場合、またはbuttonScopeStopTagsに含まれる他の要素がスタックにある場合、それらの要素がポップされるまで処理が続きます。"p": これは、popUntil関数に渡される追加の引数で、<p>要素がスタックからポップされるべき要素であることを明示しています。 この行の目的は、<form>要素が開始される前に、開いている<p>要素を強制的に閉じることです。HTML5の仕様では、<form>要素は<p>要素の子要素にはなれないため、<form>が開始される際には、開いている<p>要素は自動的に閉じられる必要があります。
p.addElement(p.tok.Data, p.tok.Attr): 開いている<p>要素が閉じられた後、新しい<form>要素がDOMツリーに追加されます。p.tok.Dataは現在のトークンのタグ名(この場合は"form")、p.tok.Attrはその属性です。p.form = p.top(): 新しく追加された<form>要素への参照をp.formに保存します。これにより、パーサーは現在アクティブな<form>要素を追跡できます。
この変更により、<!doctypehtml><p><form>のような入力が与えられた場合、パーサーはまず<p>要素をスタックに追加します。次に<form>トークンが来ると、inBodyIM関数内の新しいcase "form":ブロックが実行され、p.popUntil(buttonScopeStopTags, "p")が呼び出されます。これにより、スタックから<p>要素がポップされ(つまり閉じられ)、その後に<form>要素がDOMツリーに追加されます。結果として、HTML5の仕様に準拠した正しいDOM構造が生成されます。
また、src/pkg/html/parse_test.goのテストケースも更新され、tests2.datのテスト26だけでなく、テスト33までがパスするように変更されています。これは、この修正が他の関連するパース挙動にも影響を与えず、全体的な堅牢性を向上させたことを示唆しています。
コアとなるコードの変更箇所
src/pkg/html/parse.go
--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -619,6 +619,12 @@ func inBodyIM(p *parser) bool {
// TODO: detect <select> inside a table.
p.im = inSelectIM
return true
+ case "form":
+ if p.form == nil {
+ p.popUntil(buttonScopeStopTags, "p")
+ p.addElement(p.tok.Data, p.tok.Attr)
+ p.form = p.top()
+ }
case "li":
p.framesetOK = false
for i := len(p.oe) - 1; i >= 0; i-- {
src/pkg/html/parse_test.go
--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -134,7 +134,7 @@ func TestParser(t *testing.T) {
}{
// TODO(nigeltao): Process all the test cases from all the .dat files.
{"tests1.dat", -1},
- {"tests2.dat", 26},
+ {"tests2.dat", 33},
{"tests3.dat", 0},
}
for _, tf := range testFiles {
コアとなるコードの解説
src/pkg/html/parse.go の変更
inBodyIM関数は、HTMLパーサーが<body>要素のコンテキストでトークンを処理する際の主要なロジックを含んでいます。この関数は、現在のトークンのタグ名に基づいて異なる処理を行います。
追加されたcase "form":ブロックは、パーサーが<form>開始タグを検出したときに実行されます。
if p.form == nil: この条件は、現在アクティブな<form>要素が存在しない場合にのみ、新しい<form>要素の処理を進めることを保証します。HTML5の仕様では、<form>要素はネストできないため、このチェックは重要です。p.popUntil(buttonScopeStopTags, "p"): この行が最も重要な変更点です。p.popUntil: パーサーのスタック(開いている要素のリスト)から要素をポップするヘルパー関数です。buttonScopeStopTags: これは、<button>要素のスコープを停止させるタグのセットです。このセットには<p>が含まれています。つまり、この関数はスタックを遡り、<p>要素が見つかるか、buttonScopeStopTagsに含まれる他の要素が見つかるまで、要素をポップし続けます。これにより、<form>要素が開始される前に、開いている<p>要素が自動的に閉じられます。"p":popUntil関数に渡される追加の引数で、<p>要素がスタックからポップされるべき要素であることを明示しています。
p.addElement(p.tok.Data, p.tok.Attr): 開いている<p>要素が閉じられた後、現在のトークン(<form>)がDOMツリーに新しい要素として追加されます。p.form = p.top(): 新しく追加された<form>要素への参照をパーサーの状態(p.form)に保存します。これにより、パーサーは現在アクティブな<form>要素を追跡し、その後のパース処理で<form>関連のルールを適用できるようになります。
この変更により、htmlパーサーは、<p>要素の後に<form>要素が続くようなHTMLスニペットを、HTML5の仕様に準拠した形で正しくパースできるようになりました。
src/pkg/html/parse_test.go の変更
テストファイルでは、TestParser関数内のtestFilesスライスが更新されています。
{"tests2.dat", 26}が{"tests2.dat", 33}に変更されています。- これは、
tests2.datというテストデータファイルにおいて、以前はテスト26までしかパスしていなかったものが、今回の修正によってテスト33までパスするようになったことを示しています。これは、このコミットが単一の特定のケースだけでなく、関連する複数のパースシナリオに対しても正しい挙動をもたらしたことを意味します。
- これは、
このテストの更新は、コードの変更が意図した通りに機能し、既存のテストを壊すことなく、パーサーの堅牢性と正確性を向上させたことを確認するためのものです。
関連リンク
- HTML5仕様: https://html.spec.whatwg.org/multipage/
- HTML5パースアルゴリズム: https://html.spec.whatwg.org/multipage/parsing.html
- Go言語
htmlパッケージドキュメント: https://pkg.go.dev/golang.org/x/net/html (Go 1.10以降はgolang.org/x/net/htmlに移動)
参考にした情報源リンク
- HTML5仕様 (WHATWG): 特に「The HTML syntax」と「Parsing HTML documents」のセクション。
- Go言語の
htmlパッケージのソースコード:src/pkg/html/parse.goとsrc/pkg/html/parse_test.go。 - Web上のHTMLパースに関する記事やチュートリアル。
- ブラウザのHTMLパース挙動に関する情報。