[インデックス 10415] ファイルの概要
このコミットは、Go言語の標準ライブラリ html パッケージにおけるHTMLパーサーの改善に関するものです。具体的には、HTMLの <optgroup> タグのパース処理が修正され、<select> 要素内で <option> タグの後に続く <optgroup> タグが正しく処理されるようになりました。これにより、特定のHTML構造が期待通りにDOMツリーに変換されるようになり、関連するテストケースがパスするようになりました。
コミット
commit 3307597069f533a1f34beadb735af804d47ef6de
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Wed Nov 16 19:25:55 2011 +1100
html: parse <optgroup> tags
Pass tests2.dat, test 34:
<!DOCTYPE html><select><option><optgroup>
| <!DOCTYPE html>
| <html>
| <head>
| <body>
| <select>
| <option>
| <optgroup>
R=nigeltao
CC=golang-dev
https://golang.org/cl/5393045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3307597069f533a1f34beadb735af804d47ef6de
元コミット内容
このコミットの元の内容は、Go言語の html パッケージが <optgroup> タグを正しくパースできるようにするための変更です。特に、<select> 要素内で <option> タグの後に <optgroup> タグが続くようなケースにおいて、パーサーが既存の <option> や <optgroup> を適切に閉じてから新しい <optgroup> を追加するロジックが導入されました。これにより、tests2.dat のテストケース34がパスするようになりました。
変更の背景
HTMLのパースは、ウェブブラウザやサーバーサイドのレンダリング、スクレイピングなど、多岐にわたるアプリケーションで必要とされる基本的な機能です。HTMLの仕様は非常に複雑であり、特にエラーハンドリングや、タグのネストに関するルールは厳密に定義されています。
このコミットが行われた当時、Go言語の html パッケージはまだ初期段階にあり、HTML5のパースアルゴリズムに準拠するための継続的な改善が行われていました。<select> 要素内の <option> や <optgroup> のようなフォーム関連要素は、特定のネストルールを持っており、パーサーがこれらのルールを正確に適用しないと、DOMツリーが期待通りに構築されず、結果としてウェブページの表示やデータ処理に問題が生じる可能性がありました。
このコミットは、tests2.dat のテストケース34が示す特定のHTMLスニペット(<!DOCTYPE html><select><option><optgroup>)が正しくパースされないという問題に対応するために作成されました。これは、パーサーが <option> の後に <optgroup> が来た際に、既存の <option> を自動的に閉じるべきであるというHTMLのパースルールを遵守していなかったことを示唆しています。
前提知識の解説
HTML <optgroup> タグ
<optgroup> タグは、HTMLの <select> 要素内で、関連する <option> 要素をグループ化するために使用されます。これにより、ドロップダウンリストの選択肢を論理的に整理し、ユーザーインターフェースの使いやすさを向上させることができます。
例:
<select>
<optgroup label="Fruits">
<option value="apple">Apple</option>
<option value="banana">Banana</option>
</optgroup>
<optgroup label="Vegetables">
<option value="carrot">Carrot</option>
<option value="broccoli">Broccoli</option>
</optgroup>
</select>
HTMLパースアルゴリズム
ウェブブラウザやHTMLパーサーは、HTMLドキュメントを読み込み、それをDOM(Document Object Model)ツリーと呼ばれる構造に変換します。このプロセスは、HTML5の仕様で厳密に定義されたパースアルゴリズムに従います。このアルゴリズムは、タグの開始、終了、属性、テキストノードなどを処理し、不正なHTMLに対しても堅牢に動作するように設計されています。
特に重要なのは、「インサーションモード (Insertion Mode)」という概念です。HTMLパーサーは、現在のパース状態に応じて異なるインサーションモードを持ち、各モードで特定のタグが検出された際の処理ルールが定義されています。例えば、<select> 要素内では、パーサーは「in select」インサーションモードに入り、このモードでは <option> や <optgroup> などの特定のタグが特別な方法で処理されます。
HTML5のパースアルゴリズムでは、特定の要素(例えば <option> や <optgroup>)が、その親要素のコンテキスト内で出現した場合、既存の要素が自動的に閉じられるべきかどうかが定義されています。例えば、<option> 要素の内部に別の <option> や <optgroup> が出現した場合、既存の <option> は暗黙的に閉じられる必要があります。
Go言語 html パッケージ
Go言語の html パッケージは、HTML5の仕様に準拠したHTMLパーサーを提供します。このパッケージは、ウェブアプリケーションでのHTMLの生成、解析、変換などに利用されます。内部的には、HTML5のパースアルゴリズムを実装しており、トークナイザーとツリーコンストラクターの2つの主要なコンポーネントで構成されています。
- トークナイザー: 入力されたHTML文字列を、タグ、属性、テキストなどの個々の「トークン」に分解します。
- ツリーコンストラクター: トークナイザーから受け取ったトークンを基に、DOMツリーを構築します。この際、HTML5のパースアルゴリズムで定義されたインサーションモードとルールに従って、要素の追加、削除、ネストの調整などを行います。
技術的詳細
このコミットは、src/pkg/html/parse.go ファイル内の inSelectIM 関数に焦点を当てています。この関数は、パーサーが「in select」インサーションモードにあるときに呼び出され、<select> 要素の内部で検出されたタグを処理します。
変更前のコードでは、<optgroup> タグが検出された際に // TODO. というコメントがあり、適切な処理が実装されていませんでした。これは、パーサーが <option> や既存の <optgroup> の後に新しい <optgroup> が出現した場合に、それらを適切に閉じるロジックが欠けていたことを意味します。
HTML5のパースアルゴリズムでは、<select> 要素内で <option> や <optgroup> が出現した場合、現在の要素スタックのトップが <option> または <optgroup> であれば、それらをポップ(閉じる)する必要があります。これは、これらの要素が特定のコンテンツモデルを持ち、他の <option> や <optgroup> を子として直接持つことができないためです。
このコミットでは、このルールを適用するために以下のロジックが追加されました。
p.top().Data == "option"のチェック: 現在の要素スタックのトップが<option>であれば、p.oe.pop()を呼び出してその<option>を閉じます。p.top().Data == "optgroup"のチェック: その後、現在の要素スタックのトップが<optgroup>であれば、p.oe.pop()を呼び出してその<optgroup>を閉じます。
これらのチェックとポップ操作の後、新しい <optgroup> タグが p.addElement(p.tok.Data, p.tok.Attr) によってDOMツリーに追加されます。これにより、HTML5のパースルールに従って、<select><option><optgroup> のような構造が正しく解釈され、DOMツリーが構築されるようになります。
また、src/pkg/html/parse_test.go の変更は、この修正が正しく機能することを確認するためのテストケースの更新です。tests2.dat のテストケース34が、この修正によってパスするようになったため、テストの期待値が34から35に更新されました。これは、テストスイート全体でパスするテストケースの総数が増加したことを意味します。
コアとなるコードの変更箇所
src/pkg/html/parse.go
--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -1226,7 +1226,13 @@ func inSelectIM(p *parser) bool {
}\n \t\t\tp.addElement(p.tok.Data, p.tok.Attr)\n \t\tcase \"optgroup\":\n-\t\t\t// TODO.\n+\t\t\tif p.top().Data == \"option\" {\n+\t\t\t\tp.oe.pop()\n+\t\t\t}\n+\t\t\tif p.top().Data == \"optgroup\" {\n+\t\t\t\tp.oe.pop()\n+\t\t\t}\n+\t\t\tp.addElement(p.tok.Data, p.tok.Attr)\n \t\tcase \"select\":\n \t\t\tendSelect = true\n \t\tcase \"input\", \"keygen\", \"textarea\":
src/pkg/html/parse_test.go
--- 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\", 34},\n+\t\t{\"tests2.dat\", 35},\n \t\t{\"tests3.dat\", 0},\n \t}\n \tfor _, tf := range testFiles {
コアとなるコードの解説
src/pkg/html/parse.go の変更
inSelectIM 関数は、HTMLパーサーが <select> 要素の内部をパースしているときに呼び出される主要な関数です。この関数は、現在のトークン(p.tok)のデータ(タグ名)に基づいて、適切なパースアクションを実行します。
変更された case "optgroup": ブロックは、パーサーが <optgroup> 開始タグを検出したときの動作を定義しています。
-
if p.top().Data == "option" { p.oe.pop() }: この行は、現在の要素スタックの最上位(p.top())が<option>要素であるかどうかをチェックします。もしそうであれば、p.oe.pop()を呼び出してその<option>要素をスタックから取り除きます。これは、HTML5のパースルールにおいて、<option>要素の内部に<optgroup>が出現した場合、既存の<option>は自動的に閉じられるべきであるという要件を満たすためです。 -
if p.top().Data == "optgroup" { p.oe.pop() }: この行は、上記の<option>のチェックの後、現在の要素スタックの最上位が<optgroup>要素であるかどうかをチェックします。もしそうであれば、p.oe.pop()を呼び出してその既存の<optgroup>を閉じます。これは、<optgroup>要素の内部に別の<optgroup>が出現した場合も同様に、既存の<optgroup>が自動的に閉じられるべきであるというHTML5のパースルールに対応しています。 -
p.addElement(p.tok.Data, p.tok.Attr): 上記のポップ操作が完了した後、この行は現在検出された新しい<optgroup>タグをDOMツリーに追加します。p.tok.Dataはタグ名("optgroup")、p.tok.Attrはそのタグの属性(例:label属性)です。
これらの変更により、パーサーは <select> 要素内で <option> や既存の <optgroup> の後に新しい <optgroup> が出現した場合でも、HTML5の仕様に厳密に従ってDOMツリーを構築できるようになりました。
src/pkg/html/parse_test.go の変更
TestParser 関数は、HTMLパーサーのテストスイートを実行します。このテストは、testFiles という構造体のスライスをイテレートし、各テストファイル(例: tests1.dat, tests2.dat)と、そのファイル内で期待されるパスするテストケースの数を指定します。
{"tests2.dat", 34},から{"tests2.dat", 35},への変更: この変更は、tests2.datファイル内のテストケースのうち、以前は失敗していたテストケース34が、今回の<optgroup>パースの修正によってパスするようになったことを反映しています。したがって、tests2.datでパスするテストケースの総数が1つ増え、35になったことを示しています。これは、コードの修正が意図した通りに機能し、特定のHTML構造のパースに関するバグが修正されたことの検証となります。
関連リンク
- Go言語
htmlパッケージのドキュメント: https://pkg.go.dev/golang.org/x/net/html (コミット当時のパスとは異なる可能性がありますが、現在のドキュメントです) - HTML5 パースアルゴリズムの仕様: https://html.spec.whatwg.org/multipage/parsing.html
- HTML
<optgroup>要素のMDN Web Docs: https://developer.mozilla.org/ja/docs/Web/HTML/Element/optgroup
参考にした情報源リンク
- Go言語の公式リポジトリ (GitHub): https://github.com/golang/go
- Go言語のコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されている
https://golang.org/cl/5393045は、このGerritシステムへのリンクです) - HTML5仕様 (WHATWG): https://html.spec.whatwg.org/
- MDN Web Docs: https://developer.mozilla.org/