[インデックス 10238] ファイルの概要
このコミットは、Go言語の標準ライブラリである html
パッケージにおけるHTMLパーサーの挙動を修正するものです。具体的には、<link>
要素がHTMLの <head>
要素の後に誤って配置された場合に、正しく <head>
要素内に移動させるようにパーサーのロジックが変更されました。これにより、HTML5の仕様に準拠した正しいDOMツリーが生成されるようになります。
コミット
- コミットハッシュ:
46308d7d1191b75dc86f848dbc362616f5b0b0cb
- 作者: Andrew Balholm andybalholm@gmail.com
- コミット日時: 2011年11月4日(金) 09:29:06 +1100
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/46308d7d1191b75dc86f848dbc362616f5b0b0cb
元コミット内容
html: move <link> element from after <head> into <head>
Pass tests1.dat, test 85:
<head><meta></head><link>
| <html>
| <head>
| <meta>
| <link>
| <body>
R=nigeltao
CC=golang-dev
https://golang.org/cl/5297079
変更の背景
この変更は、HTML5のパースアルゴリズムにおける特定のケース、特に <head>
要素が閉じられた後に <link>
要素が出現した場合の処理に関するバグを修正するために行われました。
元のパーサーは、以下のようなHTMLスニペットを処理する際に問題がありました。
<head><meta></head><link>
この入力に対して、パーサーは <link>
要素を <head>
の外、つまり <body>
の直前(または <body>
がまだ存在しない場合はその位置)に配置してしまう可能性がありました。しかし、HTML5の仕様では、<link>
要素は通常、ドキュメントのメタデータとして <head>
要素内に配置されるべきです。たとえ <head>
タグが明示的に閉じられた後に出現したとしても、パーサーはそれを <head>
内に挿入しようと試みるべきです。
この問題は tests1.dat
のテストケース85で顕在化しました。このテストケースは、上記の入力に対して、以下のような正しいDOM構造が生成されることを期待していました。
<html>
<head>
<meta>
<link>
<body>
このコミットは、この特定のテストケースをパスし、HTML5の仕様に準拠した正しいパース結果を得るために必要でした。
前提知識の解説
HTML5のパースアルゴリズム
HTML5のパースアルゴリズムは、非常に堅牢でエラー耐性があるように設計されています。これは、不完全なHTMLや不正なHTMLに対しても、ブラウザが予測可能な方法でDOMツリーを構築できるようにするためです。このアルゴリズムは、ステートマシンとして機能し、「挿入モード (insertion mode)」と呼ばれる状態に基づいて、入力トークン(タグ、テキストなど)を処理します。
主要な挿入モードには以下のようなものがあります。
initial
: ドキュメントの開始状態。before html
:<html>
タグの前にいる状態。before head
:<html>
タグの後、<head>
タグの前にいる状態。in head
:<head>
タグの中にいる状態。after head
:<head>
タグの後、<body>
タグの前にいる状態。in body
:<body>
タグの中にいる状態。
各挿入モードでは、特定の入力トークンが来た場合に、DOMツリーにどのように要素を追加するか、またはどの挿入モードに遷移するかというルールが定義されています。
<head>
要素と <link>
要素
<head>
要素: HTMLドキュメントのメタデータ(ドキュメントに関する情報)を格納するコンテナです。タイトル、文字エンコーディング、スタイルシートへのリンク、スクリプト、SEO情報などが含まれます。<link>
要素: 外部リソース(主にスタイルシート)をHTMLドキュメントにリンクするために使用されます。通常、<head>
要素内に配置されます。
HTML5のパースルールでは、<head>
要素が既に閉じられている場合でも、特定のメタデータ要素(<link>
, <meta>
, <style>
, <script>
, <title>
など)が後続して出現した場合、それらを自動的に既存の <head>
要素の子として挿入しようと試みる挙動が定義されています。これは、開発者がHTMLを記述する際に起こりうる一般的なミス(例えば、<head>
タグを早めに閉じてしまうなど)を吸収し、より堅牢なパース結果を提供するためです。
Go言語の html
パッケージ
Go言語の golang.org/x/net/html
パッケージ(当時は src/pkg/html
)は、HTML5のパースアルゴリズムを実装したものです。このパッケージは、HTMLドキュメントをトークン化し、DOMツリーを構築するための機能を提供します。内部的には、上述の挿入モードや要素スタック(オープン要素スタック p.oe
)などの概念を用いて、HTMLの構造を解析します。
parser
構造体: パーサーの状態を保持します。p.oe
(open elements): 現在開いている要素のスタック。DOMツリーの階層構造を追跡するために使用されます。p.head
: 現在のドキュメントの<head>
要素へのポインタ。
insertionMode
関数: 各挿入モードに対応する関数で、入力トークンに基づいてパースロジックを実行します。beforeHeadIM
:before head
挿入モードの処理。afterHeadIM
:after head
挿入モードの処理。
addElement(name, attr)
: 指定された名前と属性を持つ要素をDOMツリーに追加します。top()
: オープン要素スタックの最上位(現在開いている最も内側の要素)を返します。useTheRulesFor(p, currentIM, newIM)
: 特定の挿入モードのルールを適用し、必要に応じて挿入モードを切り替えるヘルパー関数。
技術的詳細
このコミットの技術的詳細な変更点は、主に src/pkg/html/parse.go
内の beforeHeadIM
関数と afterHeadIM
関数の挙動にあります。
beforeHeadIM
関数の変更
// beforeHeadIM handles the "before head" insertion mode.
func beforeHeadIM(p *parser) (insertionMode, bool) {
// ... (既存のロジック) ...
if add || implied {
p.addElement("head", attr)
+ p.head = p.top() // ここが追加された行
}
return inHeadIM, !implied
}
beforeHeadIM
は、パーサーが <head>
タグの前にいる状態を処理します。このモードで <head>
要素が追加される際(明示的な <head>
タグが見つかった場合や、暗黙的に <head>
が生成される場合)、p.head = p.top()
という行が追加されました。
この変更の目的は、パーサーが <head>
要素を生成した直後に、その要素への参照を p.head
フィールドに確実に保存することです。これにより、後続のパース処理で <head>
要素がどこにあるかを正確に追跡し、必要に応じてその中に要素を挿入できるようになります。特に、afterHeadIM
で <link>
要素を <head>
に移動させる際に、この p.head
の参照が不可欠になります。
afterHeadIM
関数の変更
// afterHeadIM handles the "after head" insertion mode.
func afterHeadIM(p *parser) (insertionMode, bool) {
switch p.tn.Type {
// ... (既存のロジック) ...
case "base", "basefont", "bgsound", "link", "meta", "noframes", "script", "style", "title":
- // TODO. // ここが変更された行
+ p.oe = append(p.oe, p.head) // ここが追加された行
+ defer p.oe.pop() // ここが追加された行
+ return useTheRulesFor(p, afterHeadIM, inHeadIM) // ここが変更された行
case "head":
// TODO.
default:
// ... (既存のロジック) ...
}
}
afterHeadIM
は、パーサーが <head>
タグの後にいる状態を処理します。このモードで、<link>
、<meta>
、<style>
などのメタデータ要素が検出された場合、以前は // TODO.
となっており、適切に処理されていませんでした。
変更後のロジックは以下の通りです。
p.oe = append(p.oe, p.head)
: オープン要素スタックp.oe
に、現在パース中のドキュメントの<head>
要素(p.head
に保存されている参照)を追加します。これにより、一時的に<head>
要素が「開いている」状態として扱われ、次に挿入される要素がその子として追加される準備が整います。defer p.oe.pop()
:defer
キーワードを使用することで、現在の関数の実行が終了する直前にp.oe.pop()
が実行されるようにスケジュールします。これは、一時的にスタックに追加した<head>
要素を、この処理が完了した後にスタックから取り除くためのものです。これにより、スタックの状態が元のafter head
モードのセマンティクスに戻ります。return useTheRulesFor(p, afterHeadIM, inHeadIM)
: これは非常に重要な変更です。この行は、現在の挿入モード (afterHeadIM
) のルールを適用しつつ、実質的に挿入モードをinHeadIM
(in head insertion mode) に切り替えることを意味します。これにより、検出された<link>
要素はinHeadIM
のルールに従って処理され、結果としてp.head
の子として正しく挿入されることになります。
これらの変更により、パーサーは <head>
要素が閉じられた後に出現する <link>
要素を、HTML5の仕様に従って正しく <head>
要素内に再配置できるようになりました。
テストファイルの変更
--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -133,7 +133,7 @@ func TestParser(t *testing.T) {
rc := make(chan io.Reader)
go readDat(filename, rc)
// TODO(nigeltao): Process all test cases, not just a subset.
- for i := 0; i < 85; i++ {
+ for i := 0; i < 86; i++ {
// Parse the #data section.
b, err := ioutil.ReadAll(<-rc)
if err != nil {
テストファイル src/pkg/html/parse_test.go
では、TestParser
関数内のループが i < 85
から i < 86
に変更されました。これは、tests1.dat
ファイル内のテストケース85(0-indexedで84番目)に加えて、新たにテストケース85(0-indexedで85番目)も実行対象に含めることを意味します。この新しいテストケースが、まさにこのコミットで修正された <head><meta></head><link>
のシナリオを検証するためのものです。
コアとなるコードの変更箇所
diff --git a/src/pkg/html/parse.go b/src/pkg/html/parse.go
index 38f8ba481a..0204b7c281 100644
--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -427,6 +427,7 @@ func beforeHeadIM(p *parser) (insertionMode, bool) {
}\n \tif add || implied {\n \t\tp.addElement(\"head\", attr)\n+\t\tp.head = p.top()\n \t}\n \treturn inHeadIM, !implied\n }\n@@ -511,7 +512,9 @@ func afterHeadIM(p *parser) (insertionMode, bool) {\n \t\tcase \"frameset\":\n \t\t\t// TODO.\n \t\tcase \"base\", \"basefont\", \"bgsound\", \"link\", \"meta\", \"noframes\", \"script\", \"style\", \"title\":\n-\t\t\t// TODO.\n+\t\t\tp.oe = append(p.oe, p.head)\n+\t\t\tdefer p.oe.pop()\n+\t\t\treturn useTheRulesFor(p, afterHeadIM, inHeadIM)\n \t\tcase \"head\":\n \t\t\t// TODO.\n \t\tdefault:\
diff --git a/src/pkg/html/parse_test.go b/src/pkg/html/parse_test.go
index 3194a3fa47..8dc00ba484 100644
--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -133,7 +133,7 @@ func TestParser(t *testing.T) {\n \t\trc := make(chan io.Reader)\n \t\tgo readDat(filename, rc)\n \t\t// TODO(nigeltao): Process all test cases, not just a subset.\n-\t\tfor i := 0; i < 85; i++ {\n+\t\tfor i := 0; i < 86; i++ {\n \t\t\t// Parse the #data section.\n \t\t\tb, err := ioutil.ReadAll(<-rc)\n \t\t\tif err != nil {\
コアとなるコードの解説
src/pkg/html/parse.go
-
beforeHeadIM
関数内:p.head = p.top()
の追加:<head>
要素がDOMツリーに追加された直後に、パーサーのp.head
フィールドにその要素への参照を格納します。これにより、パーサーは常に現在のドキュメントの<head>
要素を正確に指し示すことができ、後続の処理でこの参照を利用して要素を<head>
内に挿入できるようになります。
-
afterHeadIM
関数内:case "base", ..., "title":
のブロックが変更されました。p.oe = append(p.oe, p.head)
: 現在のオープン要素スタックp.oe
に、ドキュメントの<head>
要素を一時的に追加します。これは、次にパースされる要素(この場合は<link>
など)が、この<head>
要素の子として挿入されるようにするための準備です。defer p.oe.pop()
:defer
ステートメントにより、このafterHeadIM
関数が終了する際に、先ほどスタックに追加した<head>
要素をスタックから取り除きます。これにより、スタックの状態がクリーンアップされ、他の要素のパースに影響を与えません。return useTheRulesFor(p, afterHeadIM, inHeadIM)
: この行が最も重要です。これは、現在の挿入モードがafterHeadIM
であるにもかかわらず、検出されたメタデータ要素(<link>
など)の処理にはinHeadIM
(in head insertion mode) のルールを適用するようにパーサーに指示します。inHeadIM
のルールでは、これらの要素は<head>
の子として挿入されることが期待されているため、この切り替えによって正しいDOM構造が構築されます。
src/pkg/html/parse_test.go
TestParser
関数内:for i := 0; i < 85; i++
がfor i := 0; i < 86; i++
に変更されました。- この変更は、
tests1.dat
ファイルに含まれるテストケースの実行範囲を拡張し、新たにテストケース85(0-indexedで85番目)もテスト対象に含めることを意味します。このテストケースは、<head><meta></head><link>
のような特定のHTMLスニペットが正しくパースされることを検証するために追加されました。
これらの変更により、Go言語の html
パッケージは、HTML5のパースアルゴリズムの堅牢性をさらに高め、より広範な不正なHTML入力に対しても正しいDOMツリーを生成できるようになりました。
関連リンク
- Go CL 5297079: https://golang.org/cl/5297079
参考にした情報源リンク
- HTML Standard - 8.2.5.4.7 The "in head" insertion mode: https://html.spec.whatwg.org/multipage/parsing.html#the-in-head-insertion-mode
- HTML Standard - 8.2.5.4.8 The "after head" insertion mode: https://html.spec.whatwg.org/multipage/parsing.html#the-after-head-insertion-mode
- GoDoc for golang.org/x/net/html: https://pkg.go.dev/golang.org/x/net/html
- HTML5 Parsing Algorithm (W3C Editor's Draft): (当時のW3Cドラフトへのリンクは現在ではWHATWG HTML Standardに統合されていますが、当時の仕様を理解する上で参考になります)