[インデックス 10427] ファイルの概要
このコミットは、Go言語の標準ライブラリであるhtml
パッケージにおけるHTMLパーサーの改善に関するものです。具体的には、<select>
要素内で使用される<optgroup>
および<option>
要素の終了タグ(</optgroup>
、</option>
)のパースロジックを修正し、HTML5のパース仕様に準拠させることを目的としています。これにより、不正なHTML構造や省略された終了タグを持つHTMLドキュメントでも、より正確なDOMツリーを構築できるようになります。
コミット
commit 3276afd4d4ae45afee834c7455abbb9be1906540
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Thu Nov 17 10:25:33 2011 +1100
html: parse </optgroup> and </option>
Pass tests2.dat, test 35:
<!DOCTYPE html><select><optgroup><option></optgroup><option><select><option>
| <!DOCTYPE html>
| <html>
| <head>
| <body>
| <select>
| <optgroup>
| <option>
| <option>
| <option>
Also pass tests through test 41:
<!DOCTYPE html><!-- XXX - XXX - XXX -->
R=nigeltao, rsc
CC=golang-dev
https://golang.org/cl/5395045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3276afd4d4ae45afee834c7455abbb9be1906540
元コミット内容
このコミットの元の内容は、Go言語のhtml
パッケージが<optgroup>
と<option>
の終了タグを正しくパースできるようにすることです。これにより、tests2.dat
のテストケース35(およびそれ以降のテストケース41まで)がパスするようになります。テストケース35の例として、<!DOCTYPE html><select><optgroup><option></optgroup><option><select><option>
というHTML入力が挙げられており、そのパース結果の期待されるDOM構造も示されています。
変更の背景
HTMLのパースは、ブラウザがウェブページを表示するために非常に重要なプロセスです。特に、<select>
、<optgroup>
、<option>
のようなフォーム関連要素は、その構造が複雑であり、終了タグが省略されることが多いため、パースロジックが正確でなければなりません。
このコミット以前のhtml
パッケージのパーサーは、<optgroup>
や<option>
の終了タグの処理が不完全でした。具体的には、これらの終了タグが来た際に、現在開いている要素スタック("stack of open elements")から対応する要素を正しくポップするロジックが欠けていました。その結果、不正確なDOMツリーが構築され、ウェブページのレンダリングやJavaScriptによるDOM操作に問題が生じる可能性がありました。
コミットメッセージに示されている// TODO.
コメントは、この部分の処理が未実装であったことを示唆しています。このコミットは、これらのTODOを解決し、HTML5のパースアルゴリズムに沿ってこれらの要素を正しく処理することで、パーサーの堅牢性と正確性を向上させることを目的としています。
前提知識の解説
HTMLパースとDOM
HTMLパースとは、HTMLドキュメントのテキストを読み込み、それをブラウザが理解できる構造化されたデータ(Document Object Model: DOM)に変換するプロセスです。DOMは、HTMLドキュメントの論理構造をツリー形式で表現し、JavaScriptなどのスクリプト言語からドキュメントの内容、構造、スタイルにアクセスし、操作するためのAPIを提供します。
HTML5のパースアルゴリズムは非常に複雑で、エラー耐性があります。これは、不正なHTML(例えば、終了タグの欠落やタグのネストの誤り)であっても、ブラウザが可能な限り一貫した方法でDOMを構築できるようにするためです。
スタック・オブ・オープン・エレメンツ (Stack of Open Elements)
HTML5のパースアルゴリズムにおいて、"stack of open elements"(開いている要素のスタック)は中心的な役割を果たします。これは、現在開いているHTML要素を追跡するためのスタックデータ構造です。パーサーが開始タグを読み込むと、その要素がスタックにプッシュされます。終了タグを読み込むと、対応する要素がスタックからポップされます。このスタックは、要素の正しいネストを保証し、暗黙的な終了タグの処理や、不正なHTML構造の回復(エラーリカバリ)に利用されます。
<select>
, <optgroup>
, <option>
要素の挙動
<select>
: ドロップダウンリストや複数選択リストを作成するための要素です。<optgroup>
:<select>
要素内で、関連する<option>
要素をグループ化するために使用されます。label
属性を持ち、グループのタイトルを表示します。<option>
: ドロップダウンリスト内の個々の選択肢を表します。
これらの要素は、HTMLの仕様上、特定のパースルールを持っています。特に重要なのは、<option>
要素や<optgroup>
要素は、次の<option>
、<optgroup>
、または</select>
タグが出現した場合に、明示的な終了タグがなくても暗黙的に閉じられる("foster parenting"や"implied end tags"のルールが適用される)という点です。パーサーはこれらのルールを正確に実装し、スタック上の要素を適切に処理する必要があります。
Go言語のhtml
パッケージ
Go言語のhtml
パッケージは、HTML5のパースアルゴリズムを実装しており、HTMLドキュメントをパースしてDOMツリーを構築するための機能を提供します。このパッケージは、ウェブスクレイピング、HTMLテンプレート処理、HTMLのサニタイズなど、様々な用途で利用されます。
技術的詳細
このコミットの技術的詳細は、src/pkg/html/parse.go
ファイル内のinSelectIM
関数にあります。この関数は、HTMLパーサーが"in select insertion mode"(<select>
要素の内部をパースしている状態)にあるときに呼び出されるロジックを定義しています。
変更は、EndTagToken
(終了タグ)が検出された際のswitch
文内で行われています。
-
</option>
タグの処理:- 変更前は
// TODO.
とコメントされており、処理が実装されていませんでした。 - 変更後、
p.top().Data == "option"
という条件が追加されました。これは、現在開いている要素のスタック(p.oe
)の最上位要素が<option>
であるかどうかを確認しています。 - もし最上位が
<option>
であれば、p.oe.pop()
が呼び出され、スタックから<option>
要素がポップされます。これにより、明示的な</option>
タグが正しく処理され、DOMツリーから対応する要素が閉じられます。
- 変更前は
-
</optgroup>
タグの処理:- 変更前は
// TODO.
とコメントされており、処理が実装されていませんでした。 - 変更後、より複雑なロジックが追加されました。
i := len(p.oe) - 1
で、スタックの最上位要素のインデックスを取得します。if p.oe[i].Data == "option" { i-- }
:もしスタックの最上位が<option>
要素であれば、その一つ下の要素(i--
)を対象とします。これは、<optgroup>
の終了タグが来た際に、その直前に開いていた<option>
要素が暗黙的に閉じられるというHTMLのパースルールに対応しています。if p.oe[i].Data == "optgroup" { p.oe = p.oe[:i] }
:上記の処理の後、対象の要素が<optgroup>
であれば、その要素をスタックから削除します(スライス操作でスタックを短くする)。これにより、<optgroup>
要素が正しく閉じられます。
- 変更前は
これらの変更により、パーサーは<select>
要素内で<option>
や<optgroup>
の終了タグを検出した際に、HTML5の仕様に従って開いている要素のスタックを適切に操作し、正確なDOMツリーを構築できるようになりました。
コアとなるコードの変更箇所
diff --git a/src/pkg/html/parse.go b/src/pkg/html/parse.go
index ca3907cc02..58b754ef3d 100644
--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -1245,9 +1245,17 @@ func inSelectIM(p *parser) bool {
case EndTagToken:
switch p.tok.Data {
case "option":
- // TODO.
+ if p.top().Data == "option" {
+ p.oe.pop()
+ }
case "optgroup":
- // TODO.
+ i := len(p.oe) - 1
+ if p.oe[i].Data == "option" {
+ i--
+ }
+ if p.oe[i].Data == "optgroup" {
+ p.oe = p.oe[:i]
+ }
case "select":
endSelect = true
default:
diff --git a/src/pkg/html/parse_test.go b/src/pkg/html/parse_test.go
index 01d1facc1a..07e84907cf 100644
--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -134,7 +134,7 @@ func TestParser(t *testing.T) {
}{\n \t\t// TODO(nigeltao): Process all the test cases from all the .dat files.\n \t\t{\"tests1.dat\", -1},\n-\t\t{\"tests2.dat\", 35},\n+\t\t{\"tests2.dat\", 42},\n \t\t{\"tests3.dat\", 0},\n \t}\n \tfor _, tf := range testFiles {\n```
## コアとなるコードの解説
### `src/pkg/html/parse.go` の変更
`inSelectIM` 関数は、パーサーが`<select>`要素の内部にいるときに、次のトークンをどのように処理するかを決定します。
* **`case "option":` の変更**:
```go
if p.top().Data == "option" {
p.oe.pop()
}
```
このコードは、`</option>`終了タグが検出されたときに実行されます。`p.top()`は、開いている要素のスタック(`p.oe`)の最上位にある要素を返します。もしその要素が`<option>`であれば、`p.oe.pop()`を呼び出してスタックからその`<option>`要素を削除します。これにより、`<option>`要素が正しく閉じられ、DOMツリーの構造が更新されます。
* **`case "optgroup":` の変更**:
```go
i := len(p.oe) - 1
if p.oe[i].Data == "option" {
i--
}
if p.oe[i].Data == "optgroup" {
p.oe = p.oe[:i]
}
```
このコードは、`</optgroup>`終了タグが検出されたときに実行されます。
1. `i := len(p.oe) - 1`: 現在の開いている要素のスタックの最上位要素のインデックスを取得します。
2. `if p.oe[i].Data == "option" { i-- }`: もしスタックの最上位が`<option>`要素であれば、それは暗黙的に閉じられるべきなので、インデックス`i`をデクリメントして、その下の要素(通常は`<optgroup>`)を対象とします。これは、`<optgroup>`の終了タグが`<option>`を暗黙的に閉じるというHTMLのパースルールに対応しています。
3. `if p.oe[i].Data == "optgroup" { p.oe = p.oe[:i] }`: 上記の処理の後、対象の要素が`<optgroup>`であれば、スライス操作`p.oe = p.oe[:i]`によって、その`<optgroup>`要素をスタックから削除します。これにより、`<optgroup>`要素が正しく閉じられます。
### `src/pkg/html/parse_test.go` の変更
```diff
--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -134,7 +134,7 @@ func TestParser(t *testing.T) {
}{\n \t\t// TODO(nigeltao): Process all the test cases from all the .dat files.\n \t\t{\"tests1.dat\", -1},\n-\t\t{\"tests2.dat\", 35},\n+\t\t{\"tests2.dat\", 42},\n \t\t{\"tests3.dat\", 0},\n \t}\n for _, tf := range testFiles {\n```
この変更は、テストスイートの更新です。`tests2.dat`ファイルに対するテストの実行範囲が、以前のテストケース35までから、テストケース42までに拡張されました。これは、上記のパースロジックの修正が、より多くのテストケースをパスするようになったことを示しており、修正の有効性を裏付けています。
## 関連リンク
* Go CL (Change List): [https://golang.org/cl/5395045](https://golang.org/cl/5395045)
## 参考にした情報源リンク
* HTML Standard - 13.2.6.4.1 The stack of open elements: [https://html.spec.whatwg.org/multipage/parsing.html#the-stack-of-open-elements](https://html.spec.whatwg.org/multipage/parsing.html#the-stack-of-open-elements)
* HTML Standard - 13.2.6.4.2 The rules for parsing tokens in HTML content: [https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inselect](https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inselect) (特に "in select insertion mode" のセクション)
* Go html package documentation: [https://pkg.go.dev/golang.org/x/net/html](https://pkg.go.dev/golang.org/x/net/html) (このコミットは古いGoのバージョンですが、概念は共通です)
* HTML `<select>` element: [https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select)
* HTML `<optgroup>` element: [https://developer.mozilla.org/en-US/docs/Web/HTML/Element/optgroup](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/optgroup)
* HTML `<option>` element: [https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option)