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

[インデックス 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> 要素: 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. となっており、適切に処理されていませんでした。

変更後のロジックは以下の通りです。

  1. p.oe = append(p.oe, p.head): オープン要素スタック p.oe に、現在パース中のドキュメントの <head> 要素(p.head に保存されている参照)を追加します。これにより、一時的に <head> 要素が「開いている」状態として扱われ、次に挿入される要素がその子として追加される準備が整います。
  2. defer p.oe.pop(): defer キーワードを使用することで、現在の関数の実行が終了する直前に p.oe.pop() が実行されるようにスケジュールします。これは、一時的にスタックに追加した <head> 要素を、この処理が完了した後にスタックから取り除くためのものです。これにより、スタックの状態が元の after head モードのセマンティクスに戻ります。
  3. 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

  1. beforeHeadIM 関数内:

    • p.head = p.top() の追加: <head> 要素がDOMツリーに追加された直後に、パーサーの p.head フィールドにその要素への参照を格納します。これにより、パーサーは常に現在のドキュメントの <head> 要素を正確に指し示すことができ、後続の処理でこの参照を利用して要素を <head> 内に挿入できるようになります。
  2. 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

  1. 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ツリーを生成できるようになりました。

関連リンク

参考にした情報源リンク