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

[インデックス 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) ツリー)に変換するソフトウェアコンポーネントです。その主な役割は以下の通りです。

  1. 字句解析 (Lexical Analysis): HTMLのテキストをトークン(タグ、属性、テキストコンテンツなど)のストリームに分割します。
  2. 構文解析 (Syntactic Analysis): トークンのストリームをHTMLの文法規則に従って解析し、DOMツリーを構築します。このプロセスでは、要素の親子関係、属性、テキストノードなどが定義されます。
  3. エラー回復 (Error Recovery): 不正なHTML(閉じタグの欠落、不正なネストなど)に遭遇した場合でも、可能な限りエラーを回復し、DOMツリーの構築を続行しようとします。これは、ウェブ上の多くのHTMLが厳密な文法に従っていないため、ブラウザの互換性にとって非常に重要です。
  4. 古い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> タグが検出された際の処理フローは以下の通りです。

  1. タグの検出: パーサーが <isindex> トークンを検出します。
  2. 既存のフォームのチェック:
    if p.form != nil {
        // Ignore the token.
        return true
    }
    
    もし現在アクティブな <form> 要素が既に存在する場合、<isindex> タグは無視されます。これは、HTMLの仕様上、<isindex> が単一の検索フォームを意図しており、複数のフォームがネストされることを避けるためと考えられます。
  3. デフォルト値と属性の初期化:
    • action := "":フォームの action 属性の初期値。
    • prompt := "This is a searchable index. Enter search keywords: "<label> 要素のテキストとして使用されるプロンプトのデフォルト値。
    • attr := []Attribute{{Key: "name", Val: "isindex"}}<input> 要素に適用される属性の初期リスト。デフォルトで name="isindex" が含まれます。
  4. 属性の解析と処理:
    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 に追加されます。
  5. 自己終了タグの処理:
    p.acknowledgeSelfClosingTag()
    
    <isindex> は自己終了タグとして扱われるため、パーサーにその旨を通知します。
  6. 要素スタックの調整:
    p.popUntil(buttonScopeStopTags, "p")
    
    これは、パーサーの要素スタック(現在開いている要素を追跡するスタック)を調整する重要なステップです。buttonScopeStopTags は特定の要素のセットを指し、"p"<p> 要素を指します。この呼び出しは、<isindex> が挿入される前に、特定の要素(例えば <p>)が適切に閉じられることを保証します。
  7. 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.formnil にリセットされます。

この一連の処理により、古い <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" ブロックを追加しています。

  1. if p.form != nil { return true }:

    • 現在パース中のドキュメントに既に <form> 要素が存在する場合、この <isindex> タグは無視されます。これは、HTMLの仕様が <isindex> を単一の検索フォームとして扱っていたため、複数のフォームがネストされることを避けるための挙動です。
  2. action := "" / prompt := "..." / attr := []Attribute{{Key: "name", Val: "isindex"}}:

    • action 変数は、生成される <form> 要素の action 属性の値を保持します。
    • prompt 変数は、生成される <label> 要素のテキストコンテンツを保持します。デフォルトの英語メッセージが設定されています。
    • attr スライスは、生成される <input> 要素に適用される属性のリストを保持します。初期値として name="isindex" が設定されています。これは、<isindex> が生成する入力フィールドの標準的な名前です。
  3. for _, a := range p.tok.Attr { ... }:

    • 入力された <isindex> タグに指定された属性を反復処理します。
    • action 属性が見つかれば action 変数を更新します。
    • name 属性は無視されます。
    • prompt 属性が見つかれば prompt 変数を更新します。
    • その他の属性(例: test="x")は、そのまま attr スライスに追加され、最終的に <input> 要素に適用されます。
  4. p.acknowledgeSelfClosingTag():

    • パーサーに、現在のタグ(<isindex>)が自己終了タグであることを通知します。これにより、対応する閉じタグを期待せずに次のトークンに進むことができます。
  5. p.popUntil(buttonScopeStopTags, "p"):

    • これは、パーサーの要素スタックを操作する重要な関数です。buttonScopeStopTags は、ボタンのスコープを停止させる要素のセット(例: <html>, <body>, <table> など)を定義しています。"p"<p> 要素を指します。この呼び出しは、要素スタックから指定された要素(この場合は <p>)または buttonScopeStopTags に含まれる要素が見つかるまで、要素をポップ(スタックから削除)します。これにより、<isindex> が挿入される前に、不適切なネストが解消され、DOMツリーが正しい状態に保たれます。
  6. p.addElement("form", nil) / p.form = p.top() / if action != "" { ... }:

    • 新しい <form> 要素をDOMツリーに追加します。
    • p.form に新しく追加されたフォーム要素を設定し、現在のフォームとして追跡します。
    • もし action 変数に値があれば、それを <form> 要素の action 属性として設定します。
  7. p.addElement("hr", nil) / p.oe.pop():

    • 最初の <hr> 要素を追加し、すぐに要素スタックからポップします。<hr> は自己終了タグであり、コンテンツを持たないためです。
  8. p.addElement("label", nil) / p.addText(prompt) / p.oe.pop():

    • <label> 要素を追加し、その中に prompt テキストを追加します。
    • <label> 要素を要素スタックからポップします。
  9. p.addElement("input", attr) / p.oe.pop():

    • <input> 要素を追加し、解析された属性 attr を適用します。
    • <input> 要素を要素スタックからポップします。
  10. p.addElement("hr", nil) / p.oe.pop():

    • 2番目の <hr> 要素を追加し、すぐに要素スタックからポップします。
  11. p.oe.pop() / p.form = nil:

    • 最後に、<form> 要素を要素スタックからポップし、p.formnil にリセットして、現在のフォームの追跡を終了します。

この一連の処理により、入力された単一の <isindex> タグが、HTMLのレンダリング結果として期待される複数の標準的なHTML要素に変換され、DOMツリーに正しく組み込まれます。

関連リンク

参考にした情報源リンク