Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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パーサーの主要な挿入モードである beforeHTMLIMinBodyIM、および afterBodyIM のロジックを修正しています。これらの修正は、HTML5のパーシング仕様、特に「奇妙な場所にある終了タグ」の処理に関するルールに厳密に準拠することを目的としています。

beforeHTMLIM (Before HTML Insertion Mode) の変更

beforeHTMLIM は、パーサーがまだ <html> 要素を構築していない状態を扱います。このモードでの主な変更点は以下の通りです。

  • 不要な変数の削除: add, attr, implied といったローカル変数が削除されました。これらの変数は、新しいロジックでは不要になったか、より直接的な方法で処理されるようになりました。
  • エラーとテキストトークンの処理の簡素化: 以前は ErrorTokenTextToken が検出された場合に implied = true と設定されていましたが、これらのケースは暗黙的な <html> タグの生成に直接つながるように変更されました。
  • StartTagTokenhtml 以外の処理: 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> 要素が閉じられた後にパースしている状態を扱います。

  • エラーとテキストトークンの処理: 以前は ErrorTokenTextToken の処理が TODO となっていましたが、ErrorToken の場合はパースを停止する (return nil, true) ように変更されました。TextToken の処理は削除され、inBodyIM に移行して処理されるようになりました。
  • StartTagTokenhtml 処理: html の開始タグが検出された場合、以前は TODO となっていましたが、useTheRulesFor(p, afterBodyIM, inBodyIM) を呼び出すことで、inBodyIM のルールを適用しつつ、現在のモードを afterBodyIM に設定するように変更されました。これは、<body> の後に <html> が再度開かれた場合のHTML5の挙動に合致します。
  • EndTagTokenhtml 処理: 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の仕様に近づけています。

  • 以前は addimplied といったフラグを使って <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` に移動しています)