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

[インデックス 12909] ファイルの概要

このコミットは、Go言語の実験的なHTMLパーサーパッケージ exp/html における beforeHeadIM (before head insertion mode) のロジック改善に関するものです。具体的には、HTML5の仕様に沿って、DoctypeToken の扱いを無視するように変更し、コードの制御フローをより明確にすることで、パーサーの堅牢性と正確性を向上させています。

コミット

commit b65c9a633ef594b171cb11b823f3d96f47d9f4e3
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Wed Apr 18 22:45:36 2012 +1000

    exp/html: improve beforeHeadIM
    
    Add a case to ignore doctype tokens.
    
    Clean up the flow of control to more clearly match the spec.
    
    Pass one more test.
    
    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/6062047

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/b65c9a633ef594b171cb11b823f3d96f47d9f4e3

元コミット内容

exp/html: improve beforeHeadIM Add a case to ignore doctype tokens. Clean up the flow of control to more clearly match the spec. Pass one more test.

変更の背景

このコミットは、Go言語のHTMLパーサー exp/html がHTML5の仕様に厳密に準拠するための改善の一環です。特に、HTMLドキュメントの <head> 要素が始まる前の状態 (before head insertion mode) におけるトークンの処理ロジックに焦点を当てています。

HTML5の仕様では、パーサーが特定の状態にあるときに、特定のトークン(例えば、コメント、DOCTYPE宣言、開始タグ、終了タグ、テキストなど)をどのように処理すべきかが詳細に定義されています。以前の実装では、DoctypeTokenbeforeHeadIM で適切に無視されていなかった可能性があり、また、他のトークン処理における制御フローが仕様の記述と完全に一致していなかった可能性があります。

この不一致は、特定の不正なHTML構造を持つドキュメントをパースする際に、予期せぬ結果やエラーを引き起こす可能性がありました。そのため、仕様への準拠を強化し、より多くのテストケースをパスできるようにするために、この改善が行われました。

前提知識の解説

HTML5パーシングアルゴリズム

HTML5のパースは、非常に複雑なステートマシンとして定義されています。これは、ブラウザがHTMLドキュメントをどのように解析し、DOMツリーを構築するかを厳密に規定したものです。パーサーは、入力ストリームからトークンを読み込み、現在の「挿入モード (insertion mode)」に基づいて、そのトークンを処理します。挿入モードは、パーサーがドキュメントのどの部分を処理しているかに応じて変化します。

挿入モード (Insertion Mode)

挿入モードは、HTMLパーサーの現在の状態を示します。各挿入モードには、特定のトークンタイプ(開始タグ、終了タグ、テキスト、コメント、DOCTYPEなど)が到着したときに実行すべき一連のルールが定義されています。これにより、不正なHTMLであっても、ブラウザが一貫した方法でDOMツリーを構築できるようになっています。

before head 挿入モード (Section 12.2.5.4.3)

このコミットが対象としているのは、HTML5仕様の「12.2.5.4.3 The rules for parsing tokens in the "before head" insertion mode」セクションです。このモードは、HTMLドキュメントのルート要素 (<html>) がパースされ、次に <head> 要素が期待される状態です。このモードでは、以下のようなトークンが到着した場合の処理が定義されています。

  • DOCTYPEトークン: このモードでは、DOCTYPEトークンは無視されるべきです。
  • コメントトークン: コメントノードとして現在のノード(通常は <html> 要素)の子として追加されます。
  • テキストトークン: 先頭の空白文字は無視され、残りのテキストが存在する場合、暗黙的に <head> 要素が生成され、その中にテキストが挿入されるか、またはテキストが無視される場合があります。
  • 開始タグトークン:
    • <html> タグ: in body 挿入モードに切り替わります。
    • <body> タグ: 暗黙的に <head> タグが生成され、in body 挿入モードに切り替わります。
    • head タグ: <head> 要素が生成され、in head 挿入モードに切り替わります。
    • その他のタグ: 暗黙的に <head> タグが生成され、in head 挿入モードに切り替わります。
  • 終了タグトークン:
    • head, body, html, br タグ: 暗黙的に <head> タグが生成され、in head 挿入モードに切り替わります。
    • その他のタグ: 無視されます。
  • EOFトークン: 暗黙的に <head> タグが生成され、in head 挿入モードに切り替わり、EOFトークンが再処理されます。

exp/html パッケージ

exp/html は、Go言語の標準ライブラリの一部として提供されている html パッケージの初期段階の実験的なバージョンでした。このパッケージは、HTML5の仕様に準拠したHTMLパーサーとレンダラーを提供することを目的としていました。最終的に、この実験的な成果は golang.org/x/net/html パッケージとして提供され、Goの標準的なHTML処理ライブラリとなっています。このコミットが行われた2012年時点では、まだ exp/html として開発が進められていました。

技術的詳細

このコミットの主要な目的は、src/pkg/exp/html/parse.go ファイル内の beforeHeadIM 関数をHTML5仕様の「12.2.5.4.3 The rules for parsing tokens in the "before head" insertion mode」に厳密に合わせることです。

変更点は以下の通りです。

  1. DoctypeToken の無視: 以前のバージョンでは DoctypeToken の明示的な処理がありませんでしたが、HTML5仕様ではこのモードで DoctypeToken は無視されるべきとされています。このコミットでは、case DoctypeToken: を追加し、return true (トークンを消費して次のトークンに進む) ことで、これを無視するように修正しています。
  2. 制御フローの簡素化と明確化:
    • 以前のコードでは addimplied というブール変数を使用して、<head> タグを明示的に追加するか、暗黙的に追加するかを制御していました。
    • 新しいコードでは、StartTagTokenhead ケースで直接 p.addElement("head", p.tok.Attr) を呼び出し、p.im = inHeadIM に遷移し、return true することで、明示的な <head> タグの処理を簡潔にしています。
    • TextTokenStartTagTokenhtml 以外のケース、EndTagTokenhead, body, html, br 以外のケースでは、以前は implied = true を設定していましたが、新しいコードではこれらのケースで直接 return true (トークンを無視) するか、または後続の暗黙的な <head> タグの追加ロジックにフォールスルーするように変更されています。
    • 最終的な <head> タグの暗黙的な追加ロジック (p.addElement("head", nil)) は、switch ステートメントの後に移動され、return false (トークンを再処理しない) とすることで、より仕様に沿ったフローになっています。これにより、特定のトークンが処理された後に、暗黙的な <head> タグの生成と挿入モードの変更が適切に行われるようになります。
  3. テストケースの修正: src/pkg/exp/html/testlogs/tests19.dat.log ファイルのテスト結果が修正されています。具体的には、FAIL "<!doctype html><html></p><!--foo-->"PASS "<!doctype html><html></p><!--foo-->" に変更されており、これは beforeHeadIM の改善によって、この特定のHTMLスニペットが正しくパースされるようになったことを示しています。このテストケースは、<!doctype html> の後に <html> が続き、その後に不正な </p> 終了タグとコメントが続くというもので、before head モードでの DOCTYPE と不正な終了タグの処理が正しく行われるようになったことを確認しています。

これらの変更により、パーサーはHTML5の仕様にさらに厳密に準拠し、より多くのエッジケースや不正なHTML構造を正しく処理できるようになります。

コアとなるコードの変更箇所

src/pkg/exp/html/parse.go ファイルの beforeHeadIM 関数が主な変更箇所です。

--- a/src/pkg/exp/html/parse.go
+++ b/src/pkg/exp/html/parse.go
@@ -446,37 +446,30 @@ func beforeHTMLIM(p *parser) bool {
 
 // Section 12.2.5.4.3.
 func beforeHeadIM(p *parser) bool {
-	var (
-		add     bool
-		attr    []Attribute
-		implied bool
-	)
 	switch p.tok.Type {
-	case ErrorToken:
-		implied = true
 	case TextToken:
 		p.tok.Data = strings.TrimLeft(p.tok.Data, whitespace)
 		if len(p.tok.Data) == 0 {
 			// It was all whitespace, so ignore it.
 			return true
 		}
-		implied = true
 	case StartTagToken:
 		switch p.tok.Data {
 		case "head":
-			add = true
-			attr = p.tok.Attr
+			p.addElement(p.tok.Data, p.tok.Attr)
+			p.head = p.top()
+			p.im = inHeadIM
+			return true
 		case "html":
 			return inBodyIM(p)
-		default:
-			implied = true
 		}
 	case EndTagToken:
 		switch p.tok.Data {
 		case "head", "body", "html", "br":
-			implied = true
+			// Drop down to adding an implied <head> tag.
 		default:
 			// Ignore the token.
+			return true
 		}
 	case CommentToken:
 		p.addChild(&Node{
@@ -484,13 +477,15 @@ func beforeHeadIM(p *parser) bool {
 			Data: p.tok.Data,\n \t\t})\n \t\treturn true
+	case DoctypeToken:
+		// Ignore the token.
+		return true
 	}\n-	if add || implied {
-		p.addElement("head", attr)
-		p.head = p.top()
-	}\n+
+	p.addElement("head", nil)
+	p.head = p.top()
 	p.im = inHeadIM
-	return !implied
+	return false
 }\n 
 // Section 12.2.5.4.4.

また、src/pkg/exp/html/testlogs/tests19.dat.log ファイルのテスト結果も変更されています。

--- a/src/pkg/exp/html/testlogs/tests19.dat.log
+++ b/src/pkg/exp/html/testlogs/tests19.dat.log
@@ -1,5 +1,5 @@
 FAIL "<!doctype html><math><mn DefinitionUrl=\"foo\">"
-FAIL "<!doctype html><html></p><!--foo-->"
+PASS "<!doctype html><html></p><!--foo-->"
 PASS "<!doctype html><head></head></p><!--foo-->"
 FAIL "<!doctype html><body><p><pre>"
 FAIL "<!doctype html><body><p><listing>"

コアとなるコードの解説

beforeHeadIM 関数は、HTMLパーサーが「before head」挿入モードにあるときに、次のトークンをどのように処理するかを決定します。

変更前は、addimplied というフラグ変数を使って、<head> 要素を明示的に追加するか(add)、または暗黙的に追加するか(implied)を制御していました。このロジックは、switch ステートメントの後に続く if add || implied ブロックで処理されていました。

変更後では、このフラグ変数を廃止し、各 case ブロック内で直接、またはフォールスルーによって、HTML5仕様に沿った処理を行うように改善されています。

  1. TextToken の処理:

    • 先頭の空白文字を削除し、残りのテキストが空であれば return true でトークンを無視します。
    • テキストが残っている場合は、以前は implied = true となっていましたが、変更後は switch の後に続く暗黙的な <head> 追加ロジックにフォールスルーします。これは、仕様でテキストトークンがこのモードで現れた場合、暗黙的に <head> が生成されるというルールに合致します。
  2. StartTagToken の処理:

    • head タグの場合: 以前は add = trueattr = p.tok.Attr を設定していましたが、変更後は p.addElement("head", p.tok.Attr) を直接呼び出し、p.head を設定し、挿入モードを inHeadIM に変更して return true します。これにより、明示的な <head> タグの処理がその場で完結します。
    • html タグの場合: inBodyIM(p) を呼び出して in body モードに遷移し、return します。これは変更ありません。
    • その他の開始タグの場合: 以前は implied = true となっていましたが、変更後は switch の後に続く暗黙的な <head> 追加ロジックにフォールスルーします。これは、仕様でこれらのタグが暗黙的に <head> を生成するというルールに合致します。
  3. EndTagToken の処理:

    • head, body, html, br タグの場合: 以前は implied = true となっていましたが、変更後はコメント // Drop down to adding an implied <head> tag. が示唆するように、暗黙的な <head> 追加ロジックにフォールスルーします。これは、仕様でこれらの終了タグが暗黙的に <head> を生成するというルールに合致します。
    • その他の終了タグの場合: return true でトークンを無視します。これは、仕様でこれらのタグが無視されるというルールに合致します。
  4. CommentToken の処理:

    • コメントノードを追加し、return true でトークンを無視します。これは変更ありません。
  5. DoctypeToken の追加:

    • 新しく case DoctypeToken: が追加され、return true でトークンを無視するように明示的に指定されています。これは、HTML5仕様でこのモードでは DoctypeToken が無視されるべきであるというルールに直接対応しています。
  6. 最終的な <head> 追加ロジック:

    • switch ステートメントの後に、p.addElement("head", nil)p.head = p.top() が移動され、p.im = inHeadIM に設定されます。
    • 最後に return false が追加されています。これは、この関数が false を返した場合、現在のトークンが再処理されることを意味します。しかし、この文脈では、暗黙的な <head> が追加された後に、次のトークンが in head モードで処理されることを意図していると考えられます。

これらの変更により、beforeHeadIM 関数はHTML5の仕様にさらに忠実になり、パーサーの動作がより予測可能で正確になりました。特に、DoctypeToken の無視と、制御フローの簡素化は、コードの可読性と保守性を向上させています。

関連リンク

参考にした情報源リンク

  • Go言語のHTMLパーサーに関する議論やドキュメント (当時の exp/html や現在の golang.org/x/net/html の背景を理解するため)
  • HTML5仕様書 (特にパーシングアルゴリズムのセクション)
  • Go言語のコミット履歴と関連するコードレビュー (CL: Change List)
  • Go言語の html パッケージのソースコード (現在の実装との比較のため)