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

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

このコミットは、Go言語の実験的なHTMLパーサーライブラリ exp/html における inSelectIM (in "select" insertion mode) の挙動をHTML5仕様に合致させるための調整です。具体的には、select 要素内でのトークン処理ロジックを簡素化し、EOF、ヌルバイト、<html><input><keygen><textarea><script> といった特定のタグの扱いを仕様に沿って修正しています。これにより、5つのテストが新たにパスするようになりました。

コミット

commit 8f66d7dc32b2a2082babfd9829acbfdb5996a6c7
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Tue May 22 15:30:13 2012 +1000

    exp/html: adjust inSelectIM to match spec
    
    Simplify the flow of control.
    
    Handle EOF, null bytes, <html>, <input>, <keygen>, <textarea>, <script>.
    
    Pass 5 more tests.
    
    R=golang-dev, rsc, nigeltao
    CC=golang-dev
    https://golang.org/cl/6220062

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

https://github.com/golang/go/commit/8f66d7dc32b2a2082babfd9829acbfdb5996a6c7

元コミット内容

exp/html: adjust inSelectIM to match spec Simplify the flow of control. Handle EOF, null bytes, <html>, <input>, <keygen>, <textarea>, <script>. Pass 5 more tests.

変更の背景

この変更の背景には、WebブラウザがHTMLドキュメントをどのように解析し、DOMツリーを構築するかを定義するHTML5の複雑なパースアルゴリズムがあります。特に、<select> 要素のような特定のコンテキスト内では、通常のパースルールとは異なる特殊な処理が必要とされます。

exp/html パッケージは、Go言語でHTML5のパース仕様に準拠したパーサーを実装するための実験的なライブラリでした。初期の実装では、まだHTML5仕様の全ての詳細、特にエラー処理や特定の要素のネストに関するエッジケースが完全にカバーされていませんでした。

inSelectIM (in "select" insertion mode) は、パーサーが現在<select>要素の内部にいるときに適用されるパースモードです。このモードでは、<option><optgroup>以外の要素が出現した場合の挙動や、予期せぬトークン(EOF、ヌルバイト、特定の開始タグ/終了タグ)の処理が厳密にHTML5仕様で定義されています。

このコミット以前の inSelectIM の実装は、これらの仕様の細部に完全に準拠しておらず、その結果、特定の不正なHTML構造やエッジケースで誤ったDOMツリーを構築したり、パースエラーを引き起こしたりしていました。テストログ (tests18.dat.log, tests7.dat.log) に示されている FAIL は、これらの不正確な挙動を浮き彫りにしていました。

このコミットの目的は、inSelectIM のロジックをHTML5仕様(特にセクション 12.2.5.4.16 "The "in select" insertion mode")に厳密に合わせることで、パーサーの堅牢性と正確性を向上させ、より多くの標準的なHTMLテストケースをパスさせることにありました。これにより、Go言語のHTMLパーサーがWeb標準にさらに近づき、より広範なHTMLドキュメントを正確に処理できるようになります。

前提知識の解説

このコミットを理解するためには、以下の前提知識が必要です。

  1. HTML5パースアルゴリズム:

    • HTML5の仕様は、WebブラウザがHTMLドキュメントをバイトストリームからDOMツリーに変換する詳細なアルゴリズムを定義しています。これは非常に複雑で、状態機械(ステートマシン)として記述されます。
    • トークナイゼーション: 入力バイトストリームをトークン(開始タグ、終了タグ、テキスト、コメント、DOCTYPEなど)に変換するプロセスです。
    • ツリー構築: トークナイザーから受け取ったトークンに基づいてDOMツリーを構築するプロセスです。このプロセスは、現在の「挿入モード (insertion mode)」によって挙動が大きく異なります。
    • 挿入モード (Insertion Mode): HTMLパーサーの現在の状態を示すもので、次にどのトークンをどのように処理するかを決定します。例えば、"initial"、"before html"、"in head"、"in body"、"in table"、"in select" など、多くのモードが存在します。各モードは、特定のトークンが来た場合の処理ルール(要素のプッシュ/ポップ、DOMへの追加、エラー処理など)を定義しています。
    • 要素スタック (Stack of Open Elements): 現在開いている要素(まだ対応する終了タグが来ていない要素)を追跡するためのスタックです。DOMツリーの階層構造を維持するために重要です。
    • スコープ (Scope): 特定の要素が特定のスコープ内にあるかどうかをチェックする概念です。例えば、"table scope" はテーブル関連の要素(<table>, <tbody>, <tr>, <td>など)が期待されるコンテキストを指します。
  2. exp/html パッケージ:

    • Go言語の標準ライブラリの一部である golang.org/x/net/html パッケージの前身となる実験的なパッケージです。HTML5のパースアルゴリズムをGoで実装することを目的としていました。
    • このパッケージは、HTMLドキュメントを解析し、DOMツリーを構築するための機能を提供します。
  3. inSelectIM (in "select" insertion mode):

    • パーサーが<select>要素の開始タグを処理し、その内部にいるときに遷移する挿入モードです。
    • このモードでは、<option><optgroup>以外の要素(例: <div>, <p>) が出現した場合、それらは通常無視されるか、特定の回復処理が行われます。これは、<select>要素のコンテンツモデルが非常に厳格であるためです。
    • HTML5仕様のセクション 12.2.5.4.16 に詳細なルールが記述されています。
  4. トークンタイプ:

    • ErrorToken: パースエラーが発生したか、入力の終端 (EOF) に達したことを示すトークン。
    • TextToken: テキストコンテンツ。
    • StartTagToken: 開始タグ(例: <div>)。
    • EndTagToken: 終了タグ(例: </div>)。
    • CommentToken: コメント(例: <!-- comment -->)。
    • DoctypeToken: DOCTYPE宣言(例: <!DOCTYPE html>)。
  5. Go言語の基本的な構文:

    • switch ステートメント、casereturnpanic、スライス操作 (p.oe[:i]) など。

これらの知識があることで、コミットがHTMLパースのどの部分を、どのような仕様に基づいて修正しているのかを深く理解できます。

技術的詳細

このコミットは、src/pkg/exp/html/parse.go ファイル内の inSelectIM 関数に焦点を当てた変更です。この関数は、HTMLパーサーが「in select」挿入モードにあるときに、受信したトークンを処理するロジックを実装しています。変更の目的は、HTML5仕様のセクション 12.2.5.4.16 "The "in select" insertion mode" に厳密に準拠することです。

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

  1. selectScope の導入と indexOfElementInScope の修正:

    • const ブロックに selectScope という新しいスコープ定数が追加されました。これは、<select> 要素内での要素のスコープを定義するために使用されます。
    • indexOfElementInScope 関数が selectScope を処理するように修正されました。selectScope の場合、スタック上の要素が <optgroup> または <option> でない限り、その要素はスコープ外と見なされ、-1 が返されます。これは、<select> 内ではこれらの要素のみが有効な子要素であることを反映しています。
  2. inSelectIM 関数のフロー制御の簡素化:

    • 元のコードにあった endSelect := false というフラグと、関数末尾の if endSelect { p.endSelect() } というロジックが削除されました。これにより、endSelect フラグの管理が不要になり、フローが簡素化されました。
    • endSelect フラグの代わりに、必要に応じて p.parseImpliedToken(EndTagToken, "select", nil) を呼び出すか、p.resetInsertionMode() を直接呼び出すことで、select 要素の終了処理が行われるようになりました。
  3. 特定のトークン処理の修正:

    • ErrorToken (EOF):

      • 変更前: // TODO. とコメントされており、具体的な処理が未実装でした。
      • 変更後: return true に変更されました。これは、EOFに達した場合、パースを停止し、現在のモードでの処理を終了することを示唆しています。HTML5仕様では、EOFは現在のモードで処理され、適切なエラー処理とツリー構築の終了が行われます。
    • TextToken (ヌルバイトの処理):

      • 変更前: p.addText(p.tok.Data) と、テキストデータをそのまま追加していました。
      • 変更後: p.addText(strings.Replace(p.tok.Data, "\\x00", "", -1)) と変更されました。これは、テキストデータ内のヌルバイト (\x00) を削除する処理を追加しています。HTML5仕様では、ヌルバイトはテキストデータとして扱われず、無視されるべき文字です。
    • StartTagToken - html:

      • 変更前: // TODO. とコメントされており、未実装でした。
      • 変更後: return inBodyIM(p) に変更されました。これは、<select> 要素内で <html> 開始タグが出現した場合、パーサーは「in body」挿入モードに一時的に切り替えて処理を試みるというHTML5仕様のルールに準拠しています。
    • StartTagToken - select:

      • 変更前: endSelect = true とフラグを設定していました。
      • 変更後: p.tok.Type = EndTagToken とトークンタイプを EndTagToken に変更し、return false としています。これは、<select> 要素内で別の <select> 開始タグが出現した場合、現在の <select> 要素が暗黙的に閉じられるべきであるという仕様を反映しています。トークンタイプを EndTagToken に変更することで、次のパースサイクルで現在の <select> の終了タグとして処理されるようにしています。
    • StartTagToken - input, keygen, textarea:

      • 変更前: // TODO. とコメントされており、未実装でした。
      • 変更後: if p.elementInScope(selectScope, "select") { p.parseImpliedToken(EndTagToken, "select", nil); return false } else { return true } と変更されました。これは、これらの要素が <select> スコープ内で出現した場合、現在の <select> 要素を暗黙的に閉じ、その後でこれらの要素を処理するというHTML5仕様の複雑なルールを実装しています。select 要素がスコープ内にない場合は、トークンを無視します。
    • StartTagToken - script:

      • 変更前: // TODO. とコメントされており、未実装でした。
      • 変更後: return inHeadIM(p) に変更されました。これは、<select> 要素内で <script> 開始タグが出現した場合、パーサーは「in head」挿入モードに一時的に切り替えて処理を試みるというHTML5仕様のルールに準拠しています。
    • EndTagToken - select:

      • 変更前: endSelect = true とフラグを設定していました。
      • 変更後: if p.popUntil(selectScope, "select") { p.resetInsertionMode() } と変更されました。これは、<select> 終了タグが来た場合、スタック上の <select> 要素までをポップし、その後で適切な挿入モードにリセットするという仕様を実装しています。
    • DoctypeToken:

      • 変更前: 処理がありませんでした。
      • 変更後: // Ignore the token. return true と追加されました。これは、<select> 要素内で DOCTYPE 宣言が出現した場合、それを無視するという仕様に準拠しています。
  4. inSelectInTableIM の修正:

    • p.endSelect() の呼び出しが p.parseImpliedToken(EndTagToken, "select", nil) に変更されました。これは、endSelect フラグと endSelect ヘルパー関数の削除に伴う変更で、select 要素の暗黙的な終了処理を直接呼び出すようにしています。
  5. endSelect ヘルパー関数の削除:

    • func (p *parser) endSelect() 関数が完全に削除されました。この関数のロジックは、inSelectIM および inSelectInTableIM 内で直接、または p.parseImpliedTokenp.popUntil などのより汎用的なパーサーヘルパー関数を使って実装されるようになりました。これにより、コードの重複が減り、パーサーの全体的な構造がよりモジュール化されました。

これらの変更により、inSelectIM はHTML5仕様の複雑なルールをより正確に反映し、特にエラー回復や特定の要素のネストに関する挙動が改善されました。結果として、テストケースの合格数が増加し、パーサーの堅牢性が向上しました。

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

src/pkg/exp/html/parse.go

--- a/src/pkg/exp/html/parse.go
+++ b/src/pkg/exp/html/parse.go
@@ -69,6 +69,7 @@ const (
 	tableScope
 	tableRowScope
 	tableBodyScope
+	selectScope
 )
 
 // popUntil pops the stack of open elements at the highest element whose tag
@@ -123,6 +124,10 @@ func (p *parser) indexOfElementInScope(s scope, matchTags ...string) int {
 				if tag == "html" || tag == "table" {
 					return -1
 				}
+			case selectScope:
+				if tag != "optgroup" && tag != "option" {
+					return -1
+				}
 			default:
 				panic("unreachable")
 			}
@@ -1500,16 +1505,16 @@ func inCellIM(p *parser) bool {
 
 // Section 12.2.5.4.16.
 func inSelectIM(p *parser) bool {
-	endSelect := false
 	switch p.tok.Type {
 	case ErrorToken:
-		// TODO.
+		// Stop parsing.
+		return true
 	case TextToken:
-		p.addText(p.tok.Data)
+		p.addText(strings.Replace(p.tok.Data, "\x00", "", -1))
 	case StartTagToken:
 		switch p.tok.Data {
 		case "html":
-			// TODO.
+			return inBodyIM(p)
 		case "option":
 			if p.top().Data == "option" {
 				p.oe.pop()
@@ -1524,13 +1529,17 @@ func inSelectIM(p *parser) bool {
 			}
 			p.addElement(p.tok.Data, p.tok.Attr)
 		case "select":
-			endSelect = true
+			p.tok.Type = EndTagToken
+			return false
 		case "input", "keygen", "textarea":
-			// TODO.
-		case "script":
-			// TODO.
-		default:\
+			if p.elementInScope(selectScope, "select") {
+				p.parseImpliedToken(EndTagToken, "select", nil)
+				return false
+			}
+			// Ignore the token.
+			return true
+		case "script":
+			return inHeadIM(p)
+		}
+	case EndTagToken:
+		switch p.tok.Data {
@@ -1547,19 +1556,20 @@ func inSelectIM(p *parser) bool {
 				p.oe = p.oe[:i]
 			}
 		case "select":
-			endSelect = true
-		default:
-			// Ignore the token.
+			if p.popUntil(selectScope, "select") {
+				p.resetInsertionMode()
+			}
 		}
 	case CommentToken:
 		p.doc.Add(&Node{
 			Type: CommentNode,
 			Data: p.tok.Data,
 		})
+	case DoctypeToken:
+		// Ignore the token.
+		return true
 	}
-	if endSelect {
-		p.endSelect()
-	}
+
 	return true
 }
 
@@ -1570,7 +1580,7 @@ func inSelectInTableIM(p *parser) bool {
 	\tswitch p.tok.Data {\
 	\tcase "caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th":\
 	\t\tif p.tok.Type == StartTagToken || p.elementInScope(tableScope, p.tok.Data) {\
-\t\t\t\tp.endSelect()\
+\t\t\t\tp.parseImpliedToken(EndTagToken, "select", nil)\
 \t\t\t\treturn false\
 \t\t\t} else {\
 \t\t\t\t// Ignore the token.\
@@ -1581,19 +1591,6 @@ func inSelectInTableIM(p *parser) bool {\
 \treturn inSelectIM(p)\
 }\
 \
-func (p *parser) endSelect() {\
-\tfor i := len(p.oe) - 1; i >= 0; i-- {\
-\t\tswitch p.oe[i].Data {\
-\t\tcase "option", "optgroup":\
-\t\t\tcontinue\
-\t\tcase "select":\
-\t\t\tp.oe = p.oe[:i]\
-\t\t\tp.resetInsertionMode()\
-\t\t}\
-\t\treturn\
-\t}\
-}\
-\
 // Section 12.2.5.4.18.\
 func afterBodyIM(p *parser) bool {\
 \tswitch p.tok.Type {\

src/pkg/exp/html/testlogs/tests18.dat.log および src/pkg/exp/html/testlogs/tests7.dat.log も変更され、以前 FAIL だったテストが PASS になっています。

コアとなるコードの解説

このコミットのコアとなる変更は、inSelectIM 関数におけるトークン処理のロジックをHTML5仕様に厳密に合わせることにあります。

  1. selectScope の追加と indexOfElementInScope の更新:

    • selectScope は、<select> 要素のコンテンツモデルが非常に限定的であることをパーサーに伝えるための新しいスコープです。
    • indexOfElementInScope は、特定の要素が指定されたスコープ内に存在するかどうかをチェックするヘルパー関数です。selectScope の場合、スタック上の要素が <optgroup> または <option> でない限り、その要素は <select> の有効な子ではないと判断され、スコープ外と見なされます。これにより、パーサーは <select> 内の不正な要素を適切に処理(通常は無視または暗黙的な終了)できるようになります。
  2. inSelectIM の主要な変更点:

    • endSelect フラグの廃止: 以前は endSelect というブールフラグを使って select 要素の終了処理を制御していましたが、これが削除されました。代わりに、各トークン処理のケース内で直接、またはより汎用的なパーサーヘルパー関数 (p.parseImpliedToken, p.popUntil, p.resetInsertionMode) を呼び出すことで、フローがより直接的かつ仕様に準拠するようになりました。

    • ErrorToken (EOF) の処理:

      • return true に変更されたことで、EOFが検出された際に現在のパースモードでの処理を終了し、パーサーが適切に停止するように指示しています。HTML5仕様では、EOFはパースの終了をトリガーし、未終了の要素を適切に閉じるといった最終処理が行われます。
    • TextToken (ヌルバイトの除去):

      • strings.Replace(p.tok.Data, "\\x00", "", -1) の追加は、HTML5仕様がヌルバイトを無視するように定めているためです。ヌルバイトは、HTMLのテキストコンテンツとしては無効であり、DOMツリーに含めるべきではありません。この修正により、パーサーは仕様に準拠したクリーンなテキストノードを生成します。
    • StartTagToken - html:

      • return inBodyIM(p) は、<select> 内で <html> タグが出現した場合の特殊なエラー回復ルールを実装しています。これは、ブラウザがこのような異常な構造に遭遇した際に、あたかも <body> 内にいるかのように処理を継続しようとする挙動を模倣しています。
    • StartTagToken - select:

      • p.tok.Type = EndTagToken; return false は、ネストされた <select> タグの処理です。HTML5仕様では、<select> 要素はネストできません。したがって、内側の <select> 開始タグが出現した場合、外側の <select> は暗黙的に閉じられるべきです。トークンタイプを EndTagToken に変更することで、現在の <select> の終了タグとして扱われ、inSelectIM から抜ける (return false) ことで、パーサーは適切なモードにリセットされます。
    • StartTagToken - input, keygen, textarea:

      • これらの要素も <select> の直接の子としては無効です。if p.elementInScope(selectScope, "select") { p.parseImpliedToken(EndTagToken, "select", nil); return false } のロジックは、これらのタグが出現した場合に、まず現在の <select> 要素を暗黙的に閉じ(parseImpliedToken は暗黙的な終了タグを生成し、スタックから要素をポップします)、その後でこれらの要素を処理するために inSelectIM から抜ける (return false) ことを意味します。これにより、DOMツリーが仕様に沿って構築されます。
    • StartTagToken - script:

      • return inHeadIM(p) は、<select> 内で <script> タグが出現した場合の特殊なエラー回復です。これは <html> と同様に、ブラウザが <head> 内にいるかのように処理を継続しようとする挙動を模倣しています。
    • EndTagToken - select:

      • if p.popUntil(selectScope, "select") { p.resetInsertionMode() } は、<select> 終了タグが来た場合の標準的な処理です。スタック上の最も近い <select> 要素までをポップし、その後、パーサーの挿入モードを適切な状態にリセットします。
    • DoctypeToken の無視:

      • DoctypeToken はHTMLドキュメントの冒頭に一度だけ出現するべきものであり、<select> 要素の内部で出現することは不正です。したがって、これを無視する (// Ignore the token. return true) のは仕様に準拠した挙動です。
  3. endSelect 関数の削除:

    • このヘルパー関数は、select 要素の終了処理をカプセル化していましたが、上記の変更により、そのロジックが inSelectIM 内で直接、またはより汎用的なパーサープリミティブを使って実装されるようになったため、冗長となり削除されました。これにより、コードの重複が減り、パーサーの構造がよりシンプルになりました。

これらの変更は、HTML5パースアルゴリズムの複雑な状態遷移とエラー回復メカニズムを正確に実装するためのものであり、パーサーの堅牢性と標準への準拠を大幅に向上させています。

関連リンク

  • HTML5仕様 (W3C Recommendation): https://www.w3.org/TR/html5/
  • HTML5パースアルゴリズムのセクション 12.2.5.4.16 "The "in select" insertion mode": (HTML5仕様の該当セクションを参照してください。通常、オンライン版で直接リンクできます。)
  • Go言語の x/net/html パッケージ (このコミットの exp/html の後継): https://pkg.go.dev/golang.org/x/net/html
  • Goのコードレビューシステム (Gerrit) での変更セット: https://golang.org/cl/6220062

参考にした情報源リンク

  • HTML5仕様書 (W3C): HTML5のパースアルゴリズムに関する最も権威ある情報源です。特に「Parsing HTML documents」の章と、各挿入モードの詳細な記述を参照しました。
  • Go言語の x/net/html パッケージのドキュメントとソースコード: exp/html の後継であるこのパッケージの現在の実装は、このコミットで導入された概念がどのように進化し、統合されたかを理解するのに役立ちます。
  • WebブラウザのHTMLパースに関する技術記事やブログ: HTML5パースアルゴリズムの複雑さを解説している記事は、背景知識の理解を深めるのに役立ちました。
  • Go言語のコミット履歴と関連する議論: GoのGerritやGitHubのコミットログは、変更の意図や議論の経緯を理解する上で重要です。