[インデックス 13608] ファイルの概要
このコミットは、Go言語の実験的なHTMLパーサーパッケージである exp/html
に関連する変更です。具体的には、HTMLのパースロジックを定義する src/pkg/exp/html/parse.go
と、そのパーサーのテストログファイルである src/pkg/exp/html/testlogs/tests_innerHTML_1.dat.log
が変更されています。
parse.go
は、HTMLドキュメントやフラグメントを解析するための主要なロジックを含んでいます。これには、トークン化された入力に基づいてDOMツリーを構築するための状態機械(挿入モード)の実装が含まれます。
tests_innerHTML_1.dat.log
は、特定のHTMLスニペットがどのようにパースされるべきかを示すテストケースとその期待される結果を記録したログファイルです。このファイルは、パーサーの挙動が仕様に準拠していることを確認するために使用されます。
コミット
exp/html: ignore </html> in afterBodyIM when parsing a fragment
Pass 1 additional test.
R=nigeltao
CC=golang-dev
https://golang.org/cl/6454124
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/22e918f5d6023da619ed8c0790c8a0d6830e95ab
元コミット内容
commit 22e918f5d6023da619ed8c0790c8a0d6830e95ab
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Thu Aug 9 10:19:25 2012 +1000
exp/html: ignore </html> in afterBodyIM when parsing a fragment
Pass 1 additional test.
R=nigeltao
CC=golang-dev
https://golang.org/cl/6454124
---
src/pkg/exp/html/parse.go | 7 ++++++-\
src/pkg/exp/html/testlogs/tests_innerHTML_1.dat.log | 2 +-\
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/src/pkg/exp/html/parse.go b/src/pkg/exp/html/parse.go
index 03c007e1cd..0ae660c83d 100644
--- a/src/pkg/exp/html/parse.go
+++ b/src/pkg/exp/html/parse.go
@@ -42,6 +42,8 @@ type parser struct {
fosterParenting bool
// quirks is whether the parser is operating in "quirks mode."
quirks bool
+\t// fragment is whether the parser is parsing an HTML fragment.
+\tfragment bool
// context is the context element when parsing an HTML fragment
// (section 12.4).
context *Node
@@ -1692,7 +1694,9 @@ func afterBodyIM(p *parser) bool {
\t\t}\n \tcase EndTagToken:\n \t\tif p.tok.DataAtom == a.Html {\n-\t\t\tp.im = afterAfterBodyIM\n+\t\t\tif !p.fragment {\n+\t\t\t\tp.im = afterAfterBodyIM\n+\t\t\t}\n \t\t\treturn true\n \t\t}\n \tcase CommentToken:\n@@ -2054,6 +2058,7 @@ func ParseFragment(r io.Reader, context *Node) ([]*Node, error) {\n \t\t\tType: DocumentNode,\n \t\t},\n \t\tscripting: true,\n+\t\tfragment: true,\n \t\tcontext: context,\n \t}\n \ndiff --git a/src/pkg/exp/html/testlogs/tests_innerHTML_1.dat.log b/src/pkg/exp/html/testlogs/tests_innerHTML_1.dat.log\nindex d3df267de9..392483ce79 100644\n--- a/src/pkg/exp/html/testlogs/tests_innerHTML_1.dat.log\n+++ b/src/pkg/exp/html/testlogs/tests_innerHTML_1.dat.log\n@@ -80,6 +80,6 @@ PASS \"</select><option>\"\n PASS \"<input><option>\"\n PASS \"<keygen><option>\"\n PASS \"<textarea><option>\"\n-FAIL \"</html><!--abc-->\"\n+PASS \"</html><!--abc-->\"\n PASS \"</frameset><frame>\"\n PASS \"\"\n```
## 変更の背景
このコミットの背景には、HTMLパーシングにおける「フラグメントのパース」と「完全なドキュメントのパース」の違い、およびHTML仕様への準拠があります。
HTMLパーシングの仕様(HTML Living Standard)では、パーサーは入力ストリームを処理する際に様々な「挿入モード」と呼ばれる状態を遷移します。これらのモードは、現在のパーサーの状態と入力トークンに基づいて、DOMツリーにどのようにノードを追加するかを決定します。
`afterBodyIM` (after body insertion mode) は、`<body>` 要素が閉じられた後、または暗黙的に終了した後にパーサーが遷移するモードです。このモードでは、通常、`</html>` 終了タグを受け取ると、パーサーは `afterAfterBodyIM` (after after body insertion mode) に遷移し、パースがほぼ完了した状態になります。
しかし、HTMLフラグメント(例えば、`innerHTML` や `insertAdjacentHTML` のようなAPIに渡されるHTMLスニペット)をパースする場合、完全なHTMLドキュメントとは異なるルールが適用されます。フラグメントのパースでは、`<html>`, `<head>`, `<body>` などのルート要素は暗黙的に存在するものとして扱われ、明示的な `</html>` 終了タグは通常無視されるべきです。完全なドキュメントのパース中に `</html>` が現れると、それはドキュメントの終わりを示す重要なシグナルですが、フラグメント内では単なる余分なマークアップとして扱われるべきです。
このコミット以前は、`exp/html` パッケージのパーサーがフラグメントをパースしている最中に `</html>` 終了タグに遭遇すると、完全なドキュメントのパース時と同様に `afterAfterBodyIM` に遷移しようとしていました。これはHTML仕様のフラグメントパースの挙動と異なり、特定のテストケース `</html><!--abc-->` でパーシングエラー(FAIL)を引き起こしていました。
この変更は、フラグメントパース時に `</html>` 終了タグを正しく無視することで、パーサーの挙動をHTML仕様に準拠させ、関連するテストケースをPASSさせることを目的としています。
## 前提知識の解説
### Go言語の `exp/html` パッケージ
`exp/html` は、Go言語の標準ライブラリの一部として提供されている `golang.org/x/net/html` パッケージの前身、または実験的なバージョンです。このパッケージは、WHATWG (Web Hypertext Application Technology Working Group) のHTML Living Standardで定義されているHTML5のパースアルゴリズムを実装しています。このアルゴリズムは、WebブラウザがHTMLをどのように解析し、DOM (Document Object Model) ツリーを構築するかを厳密に定めています。
### HTMLパーシングアルゴリズムと挿入モード
HTMLパーシングアルゴリズムは、非常に堅牢でエラー耐性があるように設計されています。これは、不正なHTMLマークアップに対しても、常にDOMツリーを構築しようとすることを意味します。このアルゴリズムの核心は、入力ストリームからトークンを読み取り、そのトークンと現在のパーサーの状態(「挿入モード」)に基づいて、DOMツリーを構築・変更していく状態機械です。
主要な挿入モードには以下のようなものがあります(一部抜粋):
* **initial**: 初期状態。
* **before html**: `<html>` タグの前にいる状態。
* **before head**: `<head>` タグの前にいる状態。
* **in head**: `<head>` タグの中にいる状態。
* **after head**: `<head>` タグの後にいる状態。
* **in body**: `<body>` タグの中にいる状態。
* **after body**: `<body>` タグの後にいる状態。
* **after after body**: `<body>` タグと `<html>` タグの後にいる状態。
各挿入モードでは、特定のトークンが来た場合に、DOMツリーへのノードの追加、要素のスタックへのプッシュ/ポップ、または別の挿入モードへの遷移といった異なるアクションが定義されています。
### HTMLフラグメントのパース
通常のHTMLドキュメントのパースは、`<!DOCTYPE html>` から始まり、`<html>`、`<head>`、`<body>` といった要素が順に処理され、最終的に `</html>` で終了します。
一方、HTMLフラグメントのパースは、完全なドキュメントの一部として扱われるHTMLスニペットを解析するプロセスです。例えば、JavaScriptの `element.innerHTML = "..."` や `document.createRange().createContextualFragment("...")` のようなAPIは、HTMLフラグメントをパースします。
フラグメントのパースでは、以下の点が異なります:
* **コンテキスト要素**: フラグメントは、特定の既存の要素(例えば、`<div>` の `innerHTML` であれば `div` 要素)の内部に挿入されることを前提としてパースされます。この「コンテキスト要素」が、フラグメント内の要素がどのネームスペース(HTML、SVG、MathMLなど)に属するか、および特定のタグが許可されるかどうかを決定します。
* **暗黙的な要素**: `<html>`, `<head>`, `<body>` といったドキュメント構造を定義するタグは、フラグメントパースでは通常無視されるか、特別な扱いを受けます。例えば、フラグメント内に `<html>` タグがあっても、それは完全なドキュメントのルート要素としては扱われません。
* **終了タグの扱い**: 完全なドキュメントのパースでは `</html>` はドキュメントの終わりを示す重要なシグナルですが、フラグメントパースでは、このような「ドキュメントの終わり」を示すタグは通常無視され、単なるテキストとして扱われるか、エラーとして処理されます。
### `afterBodyIM` と `afterAfterBodyIM`
* **`afterBodyIM` (after body insertion mode)**: このモードは、`<body>` 要素が閉じられた後、または暗黙的に終了した後にパーサーが位置する状態です。このモードで `</html>` 終了タグが来ると、通常は `afterAfterBodyIM` に遷移します。
* **`afterAfterBodyIM` (after after body insertion mode)**: このモードは、`<html>` 要素が閉じられた後、または暗黙的に終了した後にパーサーが位置する状態です。このモードに到達すると、HTMLドキュメントのパースは実質的に完了したと見なされます。
## 技術的詳細
このコミットの技術的詳細は、HTMLパーシングアルゴリズムにおけるフラグメントパースの特殊な挙動を正確に実装することにあります。
1. **`parser` 構造体への `fragment` フィールドの追加**:
`parser` 構造体は、HTMLパーサーの現在の状態を保持します。このコミットでは、`fragment bool` という新しいフィールドが追加されました。このフィールドは、現在のパース操作が完全なHTMLドキュメントのパースであるか、それともHTMLフラグメントのパースであるかを区別するために使用されます。`true` であればフラグメントパース中であることを示します。
2. **`ParseFragment` 関数での `fragment` フィールドの設定**:
`ParseFragment` 関数は、HTMLフラグメントをパースするためのエントリポイントです。この関数内で `parser` 構造体を初期化する際に、新しく追加された `fragment` フィールドが `true` に設定されます。これにより、パーサーは自身がフラグメントを処理していることを認識できるようになります。
3. **`afterBodyIM` 挿入モードでの `</html>` 終了タグの条件付き処理**:
`afterBodyIM` 関数は、パーサーが `after body` 挿入モードにあるときのロジックを実装しています。このモードで `EndTagToken` (終了タグ) が検出され、そのタグが `</html>` であった場合、従来のロジックでは無条件にパーサーの挿入モードを `afterAfterBodyIM` に変更していました。
このコミットでは、このモード遷移に条件が追加されました。具体的には、`if !p.fragment` という条件が導入され、`p.im = afterAfterBodyIM` への代入がこの条件の内側に移動しました。
これは、**パーサーがフラグメントをパースしていない場合(つまり、完全なドキュメントをパースしている場合)にのみ**、`</html>` 終了タグが `afterAfterBodyIM` への遷移を引き起こすことを意味します。
もしパーサーがフラグメントをパースしている場合 (`p.fragment` が `true` の場合)、`</html>` 終了タグは `afterAfterBodyIM` への遷移を引き起こさず、`afterBodyIM` モードに留まるか、そのモードでの他のルールに従って処理されます。これにより、フラグメントパースにおける `</html>` の無視が実現されます。
この変更により、`</html><!--abc-->` のようなテストケースが、フラグメントとしてパースされた際に正しく処理されるようになり、テストがFAILからPASSに変わりました。これは、HTMLパーサーがHTML仕様のフラグメントパースルールに厳密に準拠するようになったことを示しています。
## コアとなるコードの変更箇所
```diff
--- a/src/pkg/exp/html/parse.go
+++ b/src/pkg/exp/html/parse.go
@@ -42,6 +42,8 @@ type parser struct {
fosterParenting bool
// quirks is whether the parser is operating in "quirks mode."
quirks bool
+\t// fragment is whether the parser is parsing an HTML fragment.
+\tfragment bool
// context is the context element when parsing an HTML fragment
// (section 12.4).
context *Node
@@ -1692,7 +1694,9 @@ func afterBodyIM(p *parser) bool {
\t\t}\n \tcase EndTagToken:\n \t\tif p.tok.DataAtom == a.Html {\n-\t\t\tp.im = afterAfterBodyIM\n+\t\t\tif !p.fragment {\n+\t\t\t\tp.im = afterAfterBodyIM\n+\t\t\t}\n \t\t\treturn true\n \t\t}\n \tcase CommentToken:\
@@ -2054,6 +2058,7 @@ func ParseFragment(r io.Reader, context *Node) ([]*Node, error) {\n \t\t\tType: DocumentNode,\n \t\t},\n \t\tscripting: true,\n+\t\tfragment: true,\n \t\tcontext: context,\n \t}\n \ndiff --git a/src/pkg/exp/html/testlogs/tests_innerHTML_1.dat.log b/src/pkg/exp/html/testlogs/tests_innerHTML_1.dat.log
index d3df267de9..392483ce79 100644
--- a/src/pkg/exp/html/testlogs/tests_innerHTML_1.dat.log
+++ b/src/pkg/exp/html/testlogs/tests_innerHTML_1.dat.log
@@ -80,6 +80,6 @@ PASS \"</select><option>\"\n PASS \"<input><option>\"\n PASS \"<keygen><option>\"\n PASS \"<textarea><option>\"\n-FAIL \"</html><!--abc-->\"\n+PASS \"</html><!--abc-->\"\n PASS \"</frameset><frame>\"\n PASS \"\"\
コアとなるコードの解説
src/pkg/exp/html/parse.go
-
parser
構造体へのfragment
フィールド追加:type parser struct { // ... 既存のフィールド ... // fragment is whether the parser is parsing an HTML fragment. fragment bool // ... 既存のフィールド ... }
parser
構造体にfragment
というブール型のフィールドが追加されました。このフィールドは、現在のパース操作がHTMLフラグメントのパースであるかどうかを示すフラグとして機能します。 -
afterBodyIM
関数内のロジック変更:func afterBodyIM(p *parser) bool { // ... 既存のロジック ... case EndTagToken: if p.tok.DataAtom == a.Html { // </html> 終了タグの場合 if !p.fragment { // フラグメントパース中でない場合のみ p.im = afterAfterBodyIM // モードを afterAfterBodyIM に遷移 } return true } // ... 既存のロジック ... }
afterBodyIM
は、パーサーがafter body
挿入モードにあるときに呼び出される関数です。ここで、入力トークンが</html>
終了タグである場合、以前は無条件にパーサーの挿入モード (p.im
) をafterAfterBodyIM
に変更していました。 この変更により、if !p.fragment
という条件が追加されました。これは、「もしパーサーがHTMLフラグメントをパースしていない(つまり、完全なHTMLドキュメントをパースしている)ならば」という条件を意味します。この条件が真の場合にのみ、モードがafterAfterBodyIM
に遷移します。 これにより、HTMLフラグメントのパース中に</html>
が現れても、パーサーはafterAfterBodyIM
に遷移せず、フラグメントパースの仕様に沿った挙動をするようになります。 -
ParseFragment
関数でのfragment
フィールドの設定:func ParseFragment(r io.Reader, context *Node) ([]*Node, error) { p := &parser{ // ... 既存の初期化 ... scripting: true, fragment: true, // ここで fragment フィールドを true に設定 context: context, } // ... 既存のロジック ... }
ParseFragment
関数は、HTMLフラグメントをパースするために使用されるエントリポイントです。この関数内でparser
構造体を初期化する際に、新しく追加されたfragment
フィールドが明示的にtrue
に設定されます。これにより、afterBodyIM
関数内の条件分岐が正しく機能し、フラグメントパース時の</html>
の挙動が制御されます。
src/pkg/exp/html/testlogs/tests_innerHTML_1.dat.log
--- a/src/pkg/exp/html/testlogs/tests_innerHTML_1.dat.log
+++ b/src/pkg/exp/html/testlogs/tests_innerHTML_1.dat.log
@@ -80,6 +80,6 @@ PASS "</select><option>"
PASS "<input><option>"
PASS "<keygen><option>"
PASS "<textarea><option>"
-FAIL "</html><!--abc-->"
+PASS "</html><!--abc-->"
PASS "</frameset><frame>"
PASS ""
このテストログファイルでは、"</html><!--abc-->"
という特定のテストケースの結果が FAIL
から PASS
に変更されています。これは、上記のコード変更によって、このHTMLスニペットがフラグメントとして正しくパースされるようになったことを示しています。具体的には、</html>
がフラグメントパースの文脈で適切に処理(この場合は無視)され、その後のコメントノードが正しく認識されるようになった結果です。
関連リンク
- Go CL 6454124 (このコミットに対応するGerrit Code Reviewの変更リスト)
参考にした情報源リンク
- HTML Living Standard - 13.2.6.4.1. The "after body" insertion mode
- HTML Living Standard - 13.2.6.4.2. The "after after body" insertion mode
- HTML Living Standard - 13.2.5. The HTML fragment parsing algorithm
- golang.org/x/net/html documentation (exp/html の後継パッケージのドキュメント)
- Go言語のHTMLパーサーの内部構造と使い方 (日本語の解説記事、
golang.org/x/net/html
について) - HTML5 Parsing: The Details (ブラウザのHTMLパースアルゴリズムに関する一般的な解説)
- HTML5 Parsing Algorithm - A Quick Overview (W3CのHTML5仕様におけるパースアルゴリズムの概要)
- HTML5 Parsing: Insertion Modes (HTML5の挿入モードに関する解説)
- HTML5 Parsing: The Tokenization Algorithm (HTML5のトークン化アルゴリズムに関する解説)
- HTML5 Parsing: The Tree Construction Algorithm (HTML5のツリー構築アルゴリズムに関する解説)