[インデックス 10432] ファイルの概要
このコミットは、Go言語の標準ライブラリである html
パッケージのHTMLパーサーに、非推奨となった <isindex>
HTMLタグのパース機能を追加するものです。これにより、古いHTMLドキュメントに含まれる <isindex>
タグが、現代のブラウザで解釈されるような標準的な <form>
、<hr>
、<label>
、<input>
要素の組み合わせに変換されるようになります。
コミット
- コミットハッシュ:
a1dbfa6f09d2463f421eaa91ee06e15848df7ba0
- 作者: Andrew Balholm andybalholm@gmail.com
- コミット日時: 2011年11月17日 木曜日 13:12:13 +1100
- 変更ファイル数: 2ファイル
src/pkg/html/parse.go
: 38行追加src/pkg/html/parse_test.go
: 1行追加, 1行削除
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a1dbfa6f09d2463f421eaa91ee06e15848df7ba0
元コミット内容
html: parse <isindex>
Pass tests2.dat, test 42:
<isindex test=x name=x>
| <html>
| <head>
| <body>
| <form>
| <hr>
| <label>
| "This is a searchable index. Enter search keywords: "
| <input>
| name="isindex"
| test="x"
| <hr>
R=nigeltao
CC=golang-dev
https://golang.org/cl/5399049
変更の背景
この変更の背景には、HTMLの進化と、ウェブコンテンツの互換性維持という課題があります。
<isindex>
タグは、HTML 2.0で導入された要素で、非常にシンプルな検索フォームを生成するために使用されました。ユーザーがキーワードを入力し、それをサーバーに送信することで、サーバーサイドでインデックス検索を行うという、初期のウェブにおける検索機能の基本的な形態を提供していました。しかし、このタグは機能が限定的であり、より柔軟で強力な検索フォームを構築できる <form>
および <input type="text">
要素の組み合わせが普及したため、HTML 4.01で非推奨となり、HTML5では完全に廃止されました。
Go言語の html
パッケージは、ウェブスクレイピング、HTMLコンテンツの解析、またはHTMLの生成など、様々な目的でHTMLドキュメントを処理するために使用されます。このパッケージのパーサーは、最新のHTML仕様だけでなく、過去の古いHTMLドキュメントも正確に解析できる必要があります。ウェブ上には依然として古いHTML仕様で書かれたページが存在するため、パーサーが非推奨または廃止された要素を適切に処理できないと、それらのドキュメントを正確に解釈したり、DOMツリーを正しく構築したりすることができません。
このコミットは、tests2.dat
のテストケース42(<isindex test=x name=x>
)をパスするために行われました。これは、GoのHTMLパーサーが、この特定の古いHTMLタグを正しく認識し、現代のブラウザが期待するような標準的なHTML構造(<form>
、<hr>
、<label>
、<input>
)に変換できるようにするための修正です。これにより、Goの html
パッケージは、より広範なHTMLドキュメントに対して堅牢なパース能力を提供できるようになります。
前提知識の解説
HTML <isindex>
タグ
<isindex>
は、HTML 2.0で導入され、HTML 4.01で非推奨、HTML5で廃止された要素です。その主な目的は、ユーザーが単一のキーワードを入力してサーバーに送信し、サーバーサイドでインデックス検索を実行するためのシンプルなインターフェースを提供することでした。
- 機能: ブラウザは通常、このタグを
<form>
要素、水平線 (<hr>
)、プロンプトテキスト、そしてテキスト入力フィールド (<input type="text">
) の組み合わせとしてレンダリングしました。 - 属性:
prompt
: 入力フィールドの前に表示されるテキストを指定します。デフォルトはブラウザによって異なりますが、一般的には「This is a searchable index. Enter search keywords:」のようなメッセージでした。action
: 検索クエリを送信するURLを指定します。指定しない場合、現在のドキュメントのURLに送信されます。
- 非推奨の理由:
- 機能が限定的であり、より柔軟なフォーム要素(
<form>
,<input>
,<label>
など)で代替可能であったため。 - フォームのスタイルや動作を細かく制御できないため。
- セマンティックな意味合いが薄く、アクセシビリティの観点からも改善の余地があったため。
- 機能が限定的であり、より柔軟なフォーム要素(
HTMLパーサーの役割
HTMLパーサーは、HTMLドキュメント(バイトストリーム)を読み込み、それをブラウザやアプリケーションが利用できる構造化されたデータ(通常はDOM (Document Object Model) ツリー)に変換するソフトウェアコンポーネントです。その主な役割は以下の通りです。
- 字句解析 (Lexical Analysis): HTMLのテキストをトークン(タグ、属性、テキストコンテンツなど)のストリームに分割します。
- 構文解析 (Syntactic Analysis): トークンのストリームをHTMLの文法規則に従って解析し、DOMツリーを構築します。このプロセスでは、要素の親子関係、属性、テキストノードなどが定義されます。
- エラー回復 (Error Recovery): 不正なHTML(閉じタグの欠落、不正なネストなど)に遭遇した場合でも、可能な限りエラーを回復し、DOMツリーの構築を続行しようとします。これは、ウェブ上の多くのHTMLが厳密な文法に従っていないため、ブラウザの互換性にとって非常に重要です。
- 古いHTMLの扱い: 非推奨または廃止された要素であっても、それらを認識し、現代の標準に沿った形でDOMツリーに組み込むか、適切な代替要素に変換するロジックを持つことがあります。
Go言語の html
パッケージ
Go言語の golang.org/x/net/html
パッケージ(かつては src/pkg/html
に含まれていた)は、HTML5の仕様に準拠したHTMLパーサーを提供します。このパッケージは、HTMLドキュメントをトークン化し、DOMツリーを構築するための低レベルなAPIを提供します。ウェブスクレイピングツール、HTMLテンプレートエンジン、またはHTMLコンテンツのサニタイズなど、HTMLをプログラム的に処理するGoアプリケーションの基盤として利用されます。
Go言語のスライスと構造体
- スライス (Slice): Go言語における動的な配列です。基盤となる配列への参照、長さ、容量を持ちます。
[]Attribute
のように、特定の型の要素のシーケンスを表現するために使用されます。要素の追加や削除によって、自動的にサイズが調整されます。 - 構造体 (Struct): 異なる型のフィールドをまとめた複合データ型です。このコミットでは
Attribute
構造体が使用されており、これはHTML要素の属性(例:Key: "name"
,Val: "isindex"
)を表現するために使われます。
技術的詳細
このコミットの主要な変更は、src/pkg/html/parse.go
ファイル内の inBodyIM
関数に集中しています。この関数は、HTMLパーサーが「in body」挿入モード(HTMLコンテンツの大部分が解析されるモード)でトークンを処理する際のロジックを定義しています。
<isindex>
タグが検出された際の処理フローは以下の通りです。
- タグの検出: パーサーが
<isindex>
トークンを検出します。 - 既存のフォームのチェック:
もし現在アクティブなif p.form != nil { // Ignore the token. return true }
<form>
要素が既に存在する場合、<isindex>
タグは無視されます。これは、HTMLの仕様上、<isindex>
が単一の検索フォームを意図しており、複数のフォームがネストされることを避けるためと考えられます。 - デフォルト値と属性の初期化:
action := ""
:フォームのaction
属性の初期値。prompt := "This is a searchable index. Enter search keywords: "
:<label>
要素のテキストとして使用されるプロンプトのデフォルト値。attr := []Attribute{{Key: "name", Val: "isindex"}}
:<input>
要素に適用される属性の初期リスト。デフォルトでname="isindex"
が含まれます。
- 属性の解析と処理:
for _, a := range p.tok.Attr { switch a.Key { case "action": action = a.Val case "name": // Ignore the attribute. case "prompt": prompt = a.Val default: attr = append(attr, a) } }
<isindex>
タグに指定された属性をループ処理します。action
属性が見つかった場合、その値がaction
変数に格納されます。name
属性は無視されます。これは、<input>
要素にname="isindex"
がデフォルトで設定されるため、<isindex>
タグ自体のname
属性は意味を持たないためです。prompt
属性が見つかった場合、その値がprompt
変数に格納され、デフォルトのプロンプトを上書きします。- 上記以外の属性(例:
test="x"
)は、そのまま<input>
要素の属性リストattr
に追加されます。
- 自己終了タグの処理:
p.acknowledgeSelfClosingTag()
<isindex>
は自己終了タグとして扱われるため、パーサーにその旨を通知します。 - 要素スタックの調整:
これは、パーサーの要素スタック(現在開いている要素を追跡するスタック)を調整する重要なステップです。p.popUntil(buttonScopeStopTags, "p")
buttonScopeStopTags
は特定の要素のセットを指し、"p"
は<p>
要素を指します。この呼び出しは、<isindex>
が挿入される前に、特定の要素(例えば<p>
)が適切に閉じられることを保証します。 - DOMツリーの構築(変換): ここからが、
<isindex>
が標準的なHTML構造に変換される核心部分です。<form>
要素の追加:
新しいp.addElement("form", nil) p.form = p.top() if action != "" { p.form.Attr = []Attribute{{Key: "action", Val: action}} }
<form>
要素がDOMツリーに追加され、現在のフォームとしてp.form
に設定されます。もしaction
属性が指定されていれば、それが<form>
要素に設定されます。- 最初の
<hr>
要素の追加:
水平線 (p.addElement("hr", nil) p.oe.pop() // <hr> は自己終了タグなので、要素スタックからポップ
<hr>
) が追加されます。p.oe.pop()
は、要素が自己終了であるか、またはコンテンツを持たないため、すぐに要素スタックから削除されることを示します。 <label>
要素とプロンプトテキストの追加:p.addElement("label", nil) p.addText(prompt) p.oe.pop() // <label> の閉じタグをシミュレート
<label>
要素が追加され、その中にprompt
テキストが追加されます。<input>
要素の追加:p.addElement("input", attr) p.oe.pop() // <input> は自己終了タグなので、要素スタックからポップ
<input>
要素が追加され、解析された属性attr
が適用されます。- 2番目の
<hr>
要素の追加:
もう一つの水平線 (p.addElement("hr", nil) p.oe.pop() // <hr> は自己終了タグなので、要素スタックからポップ
<hr>
) が追加されます。 <form>
要素の終了:p.oe.pop() // <form> の閉じタグをシミュレート p.form = nil // 現在のフォームをリセット
<form>
要素が閉じられ、p.form
がnil
にリセットされます。
この一連の処理により、古い <isindex>
タグが、現代のブラウザが期待するセマンティックなHTML構造に変換され、後続のパース処理やDOM操作で正しく扱われるようになります。
コアとなるコードの変更箇所
src/pkg/html/parse.go
--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -683,6 +683,44 @@ func inBodyIM(p *parser) bool {
case "image":
p.tok.Data = "img"
return false
+ case "isindex":
+ if p.form != nil {
+ // Ignore the token.
+ return true
+ }
+ action := ""
+ prompt := "This is a searchable index. Enter search keywords: "
+ attr := []Attribute{{Key: "name", Val: "isindex"}}
+ for _, a := range p.tok.Attr {
+ switch a.Key {
+ case "action":
+ action = a.Val
+ case "name":
+ // Ignore the attribute.
+ case "prompt":
+ prompt = a.Val
+ default:
+ attr = append(attr, a)
+ }
+ }
+ p.acknowledgeSelfClosingTag()
+ p.popUntil(buttonScopeStopTags, "p")
+ p.addElement("form", nil)
+ p.form = p.top()
+ if action != "" {
+ p.form.Attr = []Attribute{{Key: "action", Val: action}}
+ }
+ p.addElement("hr", nil)
+ p.oe.pop()
+ p.addElement("label", nil)
+ p.addText(prompt)
+ p.addElement("input", attr)
+ p.oe.pop()
+ p.oe.pop()
+ p.addElement("hr", nil)
+ p.oe.pop()
+ p.oe.pop()
+ p.form = nil
case "caption", "col", "colgroup", "frame", "head", "tbody", "td", "tfoot", "th", "thead", "tr":
// Ignore the token.
default:
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\", 42},\n+\t\t{\"tests2.dat\", 43},\
\t\t{\"tests3.dat\", 0},\
\t}\
\tfor _, tf := range testFiles {
このテストファイルの変更は、tests2.dat
のテストケースのインデックスを 42
から 43
に変更しているだけです。これは、新しいテストケースが追加されたか、既存のテストケースの順序が変更されたことを示唆しています。このコミットの目的は、tests2.dat
のテスト42をパスすることなので、この変更はテストスイートの調整の一部です。
コアとなるコードの解説
src/pkg/html/parse.go
の変更は、inBodyIM
関数内の switch
ステートメントに新しい case "isindex"
ブロックを追加しています。
-
if p.form != nil { return true }
:- 現在パース中のドキュメントに既に
<form>
要素が存在する場合、この<isindex>
タグは無視されます。これは、HTMLの仕様が<isindex>
を単一の検索フォームとして扱っていたため、複数のフォームがネストされることを避けるための挙動です。
- 現在パース中のドキュメントに既に
-
action := ""
/prompt := "..."
/attr := []Attribute{{Key: "name", Val: "isindex"}}
:action
変数は、生成される<form>
要素のaction
属性の値を保持します。prompt
変数は、生成される<label>
要素のテキストコンテンツを保持します。デフォルトの英語メッセージが設定されています。attr
スライスは、生成される<input>
要素に適用される属性のリストを保持します。初期値としてname="isindex"
が設定されています。これは、<isindex>
が生成する入力フィールドの標準的な名前です。
-
for _, a := range p.tok.Attr { ... }
:- 入力された
<isindex>
タグに指定された属性を反復処理します。 action
属性が見つかればaction
変数を更新します。name
属性は無視されます。prompt
属性が見つかればprompt
変数を更新します。- その他の属性(例:
test="x"
)は、そのままattr
スライスに追加され、最終的に<input>
要素に適用されます。
- 入力された
-
p.acknowledgeSelfClosingTag()
:- パーサーに、現在のタグ(
<isindex>
)が自己終了タグであることを通知します。これにより、対応する閉じタグを期待せずに次のトークンに進むことができます。
- パーサーに、現在のタグ(
-
p.popUntil(buttonScopeStopTags, "p")
:- これは、パーサーの要素スタックを操作する重要な関数です。
buttonScopeStopTags
は、ボタンのスコープを停止させる要素のセット(例:<html>
,<body>
,<table>
など)を定義しています。"p"
は<p>
要素を指します。この呼び出しは、要素スタックから指定された要素(この場合は<p>
)またはbuttonScopeStopTags
に含まれる要素が見つかるまで、要素をポップ(スタックから削除)します。これにより、<isindex>
が挿入される前に、不適切なネストが解消され、DOMツリーが正しい状態に保たれます。
- これは、パーサーの要素スタックを操作する重要な関数です。
-
p.addElement("form", nil)
/p.form = p.top()
/if action != "" { ... }
:- 新しい
<form>
要素をDOMツリーに追加します。 p.form
に新しく追加されたフォーム要素を設定し、現在のフォームとして追跡します。- もし
action
変数に値があれば、それを<form>
要素のaction
属性として設定します。
- 新しい
-
p.addElement("hr", nil)
/p.oe.pop()
:- 最初の
<hr>
要素を追加し、すぐに要素スタックからポップします。<hr>
は自己終了タグであり、コンテンツを持たないためです。
- 最初の
-
p.addElement("label", nil)
/p.addText(prompt)
/p.oe.pop()
:<label>
要素を追加し、その中にprompt
テキストを追加します。<label>
要素を要素スタックからポップします。
-
p.addElement("input", attr)
/p.oe.pop()
:<input>
要素を追加し、解析された属性attr
を適用します。<input>
要素を要素スタックからポップします。
-
p.addElement("hr", nil)
/p.oe.pop()
:- 2番目の
<hr>
要素を追加し、すぐに要素スタックからポップします。
- 2番目の
-
p.oe.pop()
/p.form = nil
:- 最後に、
<form>
要素を要素スタックからポップし、p.form
をnil
にリセットして、現在のフォームの追跡を終了します。
- 最後に、
この一連の処理により、入力された単一の <isindex>
タグが、HTMLのレンダリング結果として期待される複数の標準的なHTML要素に変換され、DOMツリーに正しく組み込まれます。
関連リンク
- Go言語のHTMLパッケージのドキュメント: https://pkg.go.dev/golang.org/x/net/html
- Go言語のGerrit Change-ID: https://golang.org/cl/5399049 (Goプロジェクトの内部コードレビューシステムへのリンク)
参考にした情報源リンク
- MDN Web Docs:
<isindex>
: https://developer.mozilla.org/ja/docs/Web/HTML/Element/isindex - HTML 2.0 Specification (RFC 1866) - The ISINDEX Element: https://www.w3.org/MarkUp/html-spec/html-spec_4.html#SEC4.4
- HTML 4.01 Specification - Deprecated elements: https://www.w3.org/TR/html401/sgml/dtd.html#deprecated
- HTML5 Specification - Obsolete features: https://html.spec.whatwg.org/multipage/obsolete.html#obsolete
- Go言語のHTMLパーサーの内部動作に関する一般的な情報 (Goのソースコードや関連するブログ記事など)