[インデックス 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ドキュメントを正確に処理できるようになります。
前提知識の解説
このコミットを理解するためには、以下の前提知識が必要です。
-
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>
など)が期待されるコンテキストを指します。
-
exp/html
パッケージ:- Go言語の標準ライブラリの一部である
golang.org/x/net/html
パッケージの前身となる実験的なパッケージです。HTML5のパースアルゴリズムをGoで実装することを目的としていました。 - このパッケージは、HTMLドキュメントを解析し、DOMツリーを構築するための機能を提供します。
- Go言語の標準ライブラリの一部である
-
inSelectIM
(in "select" insertion mode):- パーサーが
<select>
要素の開始タグを処理し、その内部にいるときに遷移する挿入モードです。 - このモードでは、
<option>
や<optgroup>
以外の要素(例:<div>
,<p>
) が出現した場合、それらは通常無視されるか、特定の回復処理が行われます。これは、<select>
要素のコンテンツモデルが非常に厳格であるためです。 - HTML5仕様のセクション 12.2.5.4.16 に詳細なルールが記述されています。
- パーサーが
-
トークンタイプ:
ErrorToken
: パースエラーが発生したか、入力の終端 (EOF) に達したことを示すトークン。TextToken
: テキストコンテンツ。StartTagToken
: 開始タグ(例:<div>
)。EndTagToken
: 終了タグ(例:</div>
)。CommentToken
: コメント(例:<!-- comment -->
)。DoctypeToken
: DOCTYPE宣言(例:<!DOCTYPE html>
)。
-
Go言語の基本的な構文:
switch
ステートメント、case
、return
、panic
、スライス操作 (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" に厳密に準拠することです。
主要な変更点は以下の通りです。
-
selectScope
の導入とindexOfElementInScope
の修正:const
ブロックにselectScope
という新しいスコープ定数が追加されました。これは、<select>
要素内での要素のスコープを定義するために使用されます。indexOfElementInScope
関数がselectScope
を処理するように修正されました。selectScope
の場合、スタック上の要素が<optgroup>
または<option>
でない限り、その要素はスコープ外と見なされ、-1
が返されます。これは、<select>
内ではこれらの要素のみが有効な子要素であることを反映しています。
-
inSelectIM
関数のフロー制御の簡素化:- 元のコードにあった
endSelect := false
というフラグと、関数末尾のif endSelect { p.endSelect() }
というロジックが削除されました。これにより、endSelect
フラグの管理が不要になり、フローが簡素化されました。 endSelect
フラグの代わりに、必要に応じてp.parseImpliedToken(EndTagToken, "select", nil)
を呼び出すか、p.resetInsertionMode()
を直接呼び出すことで、select
要素の終了処理が行われるようになりました。
- 元のコードにあった
-
特定のトークン処理の修正:
-
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
宣言が出現した場合、それを無視するという仕様に準拠しています。
-
-
inSelectInTableIM
の修正:p.endSelect()
の呼び出しがp.parseImpliedToken(EndTagToken, "select", nil)
に変更されました。これは、endSelect
フラグとendSelect
ヘルパー関数の削除に伴う変更で、select
要素の暗黙的な終了処理を直接呼び出すようにしています。
-
endSelect
ヘルパー関数の削除:func (p *parser) endSelect()
関数が完全に削除されました。この関数のロジックは、inSelectIM
およびinSelectInTableIM
内で直接、またはp.parseImpliedToken
やp.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仕様に厳密に合わせることにあります。
-
selectScope
の追加とindexOfElementInScope
の更新:selectScope
は、<select>
要素のコンテンツモデルが非常に限定的であることをパーサーに伝えるための新しいスコープです。indexOfElementInScope
は、特定の要素が指定されたスコープ内に存在するかどうかをチェックするヘルパー関数です。selectScope
の場合、スタック上の要素が<optgroup>
または<option>
でない限り、その要素は<select>
の有効な子ではないと判断され、スコープ外と見なされます。これにより、パーサーは<select>
内の不正な要素を適切に処理(通常は無視または暗黙的な終了)できるようになります。
-
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
) のは仕様に準拠した挙動です。
-
-
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のコミットログは、変更の意図や議論の経緯を理解する上で重要です。