[インデックス 10355] ファイルの概要
このコミットは、Go言語の html
パッケージにおけるHTMLパーサーの挙動を改善し、特に「奇妙な場所にある終了タグ」のハンドリングを強化するものです。これにより、HTML5のパース仕様に準拠し、より堅牢な「タグスープ」処理能力を提供します。具体的には、tests1.dat
のテストケース111およびその他のテストケースをパスするように修正が加えられています。
コミット
commit 3df0512469e98361b94e6107d6d12842f7c545b4
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Sat Nov 12 12:23:30 2011 +1100
html: handle end tags in strange places
Pass tests1.dat, test 111:
</strong></b></em></i></u></strike></s></blink></tt></pre></big></small></font></select></h1></h2></h3></h4></h5></h6></body></br></a></img></title></span></style></script></table></th></td></tr></frame></area></link></param></hr></input></col></base></meta></basefont></bgsound></embed></spacer></p></dd></dt></caption></colgroup></tbody></tfoot></thead></address></blockquote></center></dir></div></dl></fieldset></listing></menu></ol></ul></li></nobr></wbr></form></button></marquee></object></html></frameset></head></iframe></image></isindex></noembed></noframes></noscript></optgroup></option></plaintext></textarea>
| <html>
| <head>
| <body>
| <br>
| <p>
Also pass all the remaining tests in tests1.dat.
R=nigeltao
CC=golang-dev
https://golang.org/cl/5372066
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3df0512469e98361b94e6107d6d12842f7c545b4
元コミット内容
html: handle end tags in strange places
Pass tests1.dat, test 111:
</strong></b></em></i></u></strike></s></blink></tt></pre></big></small></font></select></h1></h2></h3></h4></h5><h6></body></br></a></img></title></span></style></script></table></th></td></tr></frame></area></link></param></hr></input></col></base></meta></basefont></bgsound></embed></spacer></p></dd></dt></caption></colgroup></tbody></tfoot></thead></address></blockquote></center></dir></div></dl></fieldset></listing></menu></ol></ul></li></nobr></wbr></form></button></marquee></object></html></frameset></head></iframe></image></isindex></noembed></noframes></noscript></optgroup></option></plaintext></textarea>
| <html>
| <head>
| <body>
| <br>
| <p>
Also pass all the remaining tests in tests1.dat.
R=nigeltao
CC=golang-dev
https://golang.org/cl/5372066
変更の背景
HTMLは非常に寛容な言語であり、ブラウザはしばしば不完全または不正なマークアップ(「タグスープ」と呼ばれる)を解釈し、表示しようとします。このコミットの背景には、Go言語の html
パッケージが、このような非標準的なHTML構造、特に予期せぬ場所に現れる終了タグを、HTML5のパース仕様に則って正しく処理できるようにするという目的があります。
元のパーサーは、特定の終了タグが予期しないコンテキストで出現した場合に、正しく処理できない、あるいは無視できない問題がありました。コミットメッセージに示されている tests1.dat
のテストケース111は、非常に多くの終了タグが連続して出現する極端な例であり、これはブラウザがどのようにこれらのタグを「無視」または「適切に処理」するかを模倣するためのものです。
この修正により、GoのHTMLパーサーは、より多くの現実世界のHTMLドキュメント(特にウェブスクレイピングや既存のウェブコンテンツの処理において)を、より堅牢かつ正確にパースできるようになります。これは、ウェブ標準への準拠と、実用的な堅牢性の両方を向上させるための重要なステップです。
前提知識の解説
HTMLパーシングとタグスープ
HTMLパーシングとは、HTMLドキュメントのテキストを読み込み、それをブラウザが理解できる構造(DOMツリー)に変換するプロセスです。HTMLは非常に柔軟な言語であり、開発者が閉じタグを忘れたり、要素を誤ってネストしたりすることがよくあります。このような「不正な」HTMLは「タグスープ」と呼ばれます。
ウェブブラウザは、このようなタグスープを処理するために、非常に複雑で寛容なエラー回復メカニズムを持っています。これは、HTML5の仕様で詳細に定義されており、ブラウザ間の互換性を保ちつつ、不正なマークアップを「最善の努力」で解釈するためのアルゴリズムが記述されています。
HTML5パーシングアルゴリズムと挿入モード (Insertion Modes)
HTML5のパーシングアルゴリズムは、ステートマシンとして設計されており、「挿入モード (Insertion Modes)」という概念が中心にあります。パーサーは、現在のコンテキスト(例えば、<head>
タグの中、<body>
タグの中、テーブルの中など)に応じて異なる挿入モードに切り替わります。各挿入モードには、特定のトークン(開始タグ、終了タグ、テキストなど)が検出されたときに実行すべき一連のルールが定義されています。
例えば、before html
挿入モードでは、<html>
タグがまだ見つかっていない状態を扱います。このモードで予期せぬ終了タグ(例: </body>
)が検出された場合、HTML5の仕様では、そのタグを無視するか、あるいは暗黙的に <html>
や <body>
タグを生成して、適切な挿入モードに移行するなどのルールが定められています。
暗黙的なタグ生成 (Implied Tags)
HTML5のパーシングでは、特定の状況下で、明示的に記述されていないタグが自動的に生成されることがあります。例えば、HTMLドキュメントの先頭でいきなり <body>
タグが検出された場合、パーサーは自動的に <html>
タグと <head>
タグを生成し、それらをDOMツリーに追加します。これを「暗黙的なタグ生成」と呼びます。
tests1.dat
と HTML5 Conformance Test Suite
tests1.dat
は、HTML5の仕様に準拠しているかをテストするための、HTML5 Conformance Test Suiteの一部である可能性があります。これらのテストスイートは、様々な有効なHTMLと無効なHTMLの組み合わせを網羅しており、パーサーが仕様通りに動作するかを確認するために使用されます。テストケース111のような極端な例は、パーサーのエラー回復能力を試すために設計されています。
技術的詳細
このコミットは、Go言語の html
パッケージにおけるHTMLパーサーの主要な挿入モードである beforeHTMLIM
、inBodyIM
、および afterBodyIM
のロジックを修正しています。これらの修正は、HTML5のパーシング仕様、特に「奇妙な場所にある終了タグ」の処理に関するルールに厳密に準拠することを目的としています。
beforeHTMLIM
(Before HTML Insertion Mode) の変更
beforeHTMLIM
は、パーサーがまだ <html>
要素を構築していない状態を扱います。このモードでの主な変更点は以下の通りです。
- 不要な変数の削除:
add
,attr
,implied
といったローカル変数が削除されました。これらの変数は、新しいロジックでは不要になったか、より直接的な方法で処理されるようになりました。 - エラーとテキストトークンの処理の簡素化: 以前は
ErrorToken
やTextToken
が検出された場合にimplied = true
と設定されていましたが、これらのケースは暗黙的な<html>
タグの生成に直接つながるように変更されました。 StartTagToken
のhtml
以外の処理:html
以外の開始タグが検出された場合、以前はimplied = true
となっていましたが、新しいコードではp.addElement(p.tok.Data, p.tok.Attr)
を呼び出して要素を追加し、すぐにbeforeHeadIM
に移行するように変更されました。これは、<html>
タグが暗黙的に生成される前に、他の要素が先に現れた場合のHTML5の挙動に近いです。EndTagToken
の処理の厳格化:head
,body
,html
,br
の終了タグが検出された場合、以前はimplied = true
となっていましたが、新しいコードではこれらのタグが検出された場合でも、暗黙的な<html>
タグの生成に直接進むようになりました。これは、これらの終了タグが<html>
タグの前に現れても、<html>
タグが暗黙的に生成されるべきであるという仕様に合致します。- その他の終了タグが検出された場合、以前は単に無視されていましたが、新しいコードでは
return beforeHTMLIM, true
となり、トークンを無視しつつ、現在の挿入モードを維持するように明示されました。
- 暗黙的な
<html>
タグ生成のロジックの変更: 以前はadd || implied
の条件に基づいて<html>
タグが追加されていましたが、新しいコードでは、特定の開始タグ(html
以外)が検出された場合を除き、常に暗黙的に<html>
タグがnil
の属性で追加されるようになりました。そして、beforeHeadIM
に移行する際のreprocess
フラグ(戻り値のbool
)の計算も変更され、より正確な挙動を反映しています。
inBodyIM
(In Body Insertion Mode) の変更
inBodyIM
は、<body>
要素の内部をパースしている状態を扱います。
br
終了タグの特殊処理:inBodyIM
においてbr
の終了タグが検出された場合、HTML5の仕様ではこれを開始タグとして扱うべきとされています。このコミットでは、p.tok.Type = StartTagToken
とすることでトークンのタイプをStartTagToken
に変更し、inBodyIM
を再処理(return inBodyIM, false
)することで、br
開始タグとして適切に処理されるように修正されました。
afterBodyIM
(After Body Insertion Mode) の変更
afterBodyIM
は、<body>
要素が閉じられた後にパースしている状態を扱います。
- エラーとテキストトークンの処理: 以前は
ErrorToken
やTextToken
の処理がTODO
となっていましたが、ErrorToken
の場合はパースを停止する (return nil, true
) ように変更されました。TextToken
の処理は削除され、inBodyIM
に移行して処理されるようになりました。 StartTagToken
のhtml
処理:html
の開始タグが検出された場合、以前はTODO
となっていましたが、useTheRulesFor(p, afterBodyIM, inBodyIM)
を呼び出すことで、inBodyIM
のルールを適用しつつ、現在のモードをafterBodyIM
に設定するように変更されました。これは、<body>
の後に<html>
が再度開かれた場合のHTML5の挙動に合致します。EndTagToken
のhtml
処理:html
の終了タグが検出された場合、以前はTODO
となっていましたが、afterAfterBodyIM
に移行するように変更されました。その他の終了タグの処理は削除され、inBodyIM
に移行して処理されるようになりました。- デフォルトの戻り値の変更: 以前は
return afterBodyIM, true
となっていましたが、return inBodyIM, false
に変更されました。これは、afterBodyIM
で処理されなかったトークンは、inBodyIM
のルールで再処理されるべきであるというHTML5の仕様に合致します。
parse_test.go
の変更
TestParser
関数内のtests1.dat
のテストケース番号が111
から-1
に変更されました。これは、tests1.dat
の全てのテストケースを処理するように変更されたことを意味します。これにより、このコミットが単一のテストケースだけでなく、tests1.dat
全体の堅牢性を向上させることを意図していることが示唆されます。
これらの変更は、GoのHTMLパーサーが、より多くの現実世界のHTMLドキュメントを、HTML5の仕様に厳密に準拠して、より堅牢かつ正確にパースできるようにするためのものです。特に、ブラウザがどのように不正なマークアップを「修正」するかを模倣する能力が向上しています。
コアとなるコードの変更箇所
このコミットで変更されたファイルは以下の2つです。
src/pkg/html/parse.go
: HTMLパーサーの主要なロジックが含まれるファイル。- 追加: 18行
- 削除: 31行
src/pkg/html/parse_test.go
: HTMLパーサーのテストコードが含まれるファイル。- 追加: 1行
- 削除: 1行
コアとなるコードの解説
src/pkg/html/parse.go
beforeHTMLIM
関数の変更点
--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -352,30 +352,19 @@ func initialIM(p *parser) (insertionMode, bool) {
// Section 11.2.5.4.2.
func beforeHTMLIM(p *parser) (insertionMode, bool) {
- var (
- add bool
- attr []Attribute
- implied bool
- )
switch p.tok.Type {
- case ErrorToken:
- implied = true
- case TextToken:
- // TODO: distinguish whitespace text from others.
- implied = true
case StartTagToken:
if p.tok.Data == "html" {
- add = true
- attr = p.tok.Attr
+ p.addElement(p.tok.Data, p.tok.Attr)
+ return beforeHeadIM, true
} else {
- implied = true
+ // Create an implied <html> tag.
+ p.addElement("html", nil)
+ return beforeHeadIM, false
}
case EndTagToken:
switch p.tok.Data {
case "head", "body", "html", "br":
- implied = true
+ // Drop down to creating an implied <html> tag.
default:
// Ignore the token.
+ return beforeHTMLIM, true
}
case CommentToken:
p.doc.Add(&Node{
@@ -384,10 +373,9 @@ func beforeHTMLIM(p *parser) (insertionMode, bool) {
})
return beforeHTMLIM, true
}\n- if add || implied {\n-\t\tp.addElement(\"html\", attr)\n-\t}\n-\treturn beforeHeadIM, !implied\n+\t// Create an implied <html> tag.\n+\tp.addElement(\"html\", nil)\n+\treturn beforeHeadIM, false
}\n
// Section 11.2.5.4.3.
この変更は、beforeHTMLIM
のロジックを大幅に簡素化し、HTML5の仕様に近づけています。
- 以前は
add
やimplied
といったフラグを使って<html>
タグの追加を制御していましたが、新しいコードでは、html
開始タグが直接検出された場合はそのタグを追加し、それ以外の場合は暗黙的に<html>
タグを生成してbeforeHeadIM
に移行するという、より直接的なアプローチを取っています。 - 特に、
EndTagToken
の処理が明確化され、head
,body
,html
,br
の終了タグが検出された場合でも、暗黙的な<html>
タグの生成に繋がるように変更されています。その他の終了タグは明示的に無視されます。
inBodyIM
関数の変更点
--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -691,6 +679,9 @@ func inBodyIM(p *parser) (insertionMode, bool) {\n \t\tif p.popUntil(defaultScopeStopTags, p.tok.Data) {\n \t\t\tp.clearActiveFormattingElements()\n \t\t}\n+\t\tcase "br":\n+\t\t\tp.tok.Type = StartTagToken\n+\t\t\treturn inBodyIM, false
\t\tdefault:\n \t\t\tp.inBodyEndTagOther(p.tok.Data)\n \t\t}\n```
`inBodyIM` では、`br` の終了タグが検出された場合に、そのトークンタイプを `StartTagToken` に変更し、現在の挿入モード (`inBodyIM`) で再処理するように修正されています。これは、HTML5の仕様で `br` 終了タグが開始タグとして扱われるべきというルールに準拠するためです。
#### `afterBodyIM` 関数の変更点
```diff
--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -1192,18 +1183,15 @@ func inSelectIM(p *parser) (insertionMode, bool) {\n func afterBodyIM(p *parser) (insertionMode, bool) {\n switch p.tok.Type {\n case ErrorToken:\n-\t\t// TODO.\n-\tcase TextToken:\n-\t\t// TODO.\n+\t\t// Stop parsing.\n+\t\treturn nil, true
\tcase StartTagToken:\n-\t\t// TODO.\n+\t\tif p.tok.Data == "html" {\n+\t\t\treturn useTheRulesFor(p, afterBodyIM, inBodyIM)\n+\t\t}\n \tcase EndTagToken:\n-\t\tswitch p.tok.Data {\n-\t\tcase "html":\n-\t\t\t// TODO: autoclose the stack of open elements.\n+\t\tif p.tok.Data == "html" {\n \t\t\treturn afterAfterBodyIM, true
-\t\tdefault:\n-\t\t\t// TODO.\n \t\t}\n \tcase CommentToken:\n \t\t// The comment is attached to the <html> element.\n@@ -1216,8 +1204,7 @@ func afterBodyIM(p *parser) (insertionMode, bool) {\n \t\t})\n \t\treturn afterBodyIM, true\n \t}\n-\t// TODO: should this be "return inBodyIM, true"?\n-\treturn afterBodyIM, true\n+\treturn inBodyIM, false
}\n
// Section 11.2.5.4.19.
afterBodyIM
では、ErrorToken
が検出された場合にパースを停止するように変更されました。また、html
開始タグが検出された場合は inBodyIM
のルールを適用するように、html
終了タグが検出された場合は afterAfterBodyIM
に移行するように明確化されました。その他のトークンは inBodyIM
で再処理されるように、デフォルトの戻り値も変更されています。
src/pkg/html/parse_test.go
--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -133,7 +133,7 @@ func TestParser(t *testing.T) {\n \tn int\n }{\n \t// TODO(nigeltao): Process all the test cases from all the .dat files.\n-\t\t{\"tests1.dat\", 111},\n+\t\t{\"tests1.dat\", -1},\n \t\t{\"tests2.dat\", 0},\n \t\t{\"tests3.dat\", 0},\n \t}\n```
テストファイルでは、`tests1.dat` のテストケース番号が `111` から `-1` に変更されました。これは、`tests1.dat` 内の全てのテストケースを対象としてテストを実行することを示しており、このコミットが単一の特定のケースだけでなく、より広範な堅牢性向上を目指していることを裏付けています。
## 関連リンク
* Go CL: [https://golang.org/cl/5372066](https://golang.org/cl/5372066)
## 参考にした情報源リンク
* HTML Standard - 13.2.6 The parsing model: [https://html.spec.whatwg.org/multipage/parsing.html#the-parsing-model](https://html.spec.whatwg.org/multipage/parsing.html#the-parsing-model)
* HTML Standard - 13.2.6.4.2 The "before html" insertion mode: [https://html.spec.whatwg.org/multipage/parsing.html#the-before-html-insertion-mode](https://html.spec.whatwg.org/multipage/parsing.html#the-before-html-insertion-mode)
* HTML Standard - 13.2.6.4.5 The "in body" insertion mode: [https://html.spec.whatwg.org/multipage/parsing.html#the-in-body-insertion-mode](https://html.spec.whatwg.org/multipage/parsing.html#the-in-body-insertion-mode)
* HTML Standard - 13.2.6.4.18 The "after body" insertion mode: [https://html.spec.whatwg.org/multipage/parsing.html#the-after-body-insertion-mode](https://html.spec.whatwg.org/multipage/parsing.html#the-after-body-insertion-mode)
* Go html package documentation: [https://pkg.go.dev/golang.org/x/net/html](https://pkg.go.dev/golang.org/x/net/html) (コミット当時のパッケージパスは `src/pkg/html` でしたが、現在は `golang.org/x/net/html` に移動しています)