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

[インデックス 13624] ファイルの概要

このコミットは、Go言語の実験的なHTMLパーサーパッケージ exp/html におけるバグ修正に関するものです。具体的には、非推奨のHTMLタグである <isindex> のパース時に、その代替要素(フォーム、入力フィールドなど)を生成する際のネスト構造が誤っていた問題を修正しています。既存の addSyntheticElement メソッドが不適切なネストを引き起こしていたため、より堅牢な parseImpliedToken メソッドに置き換えることで、正しいHTMLツリー構造が生成されるように改善されました。これにより、関連するテストケースが FAIL から PASS に変更されています。

コミット

commit 3ba25e76a7dbb1574006081f597dcc6d9b569869
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Tue Aug 14 09:53:10 2012 +1000

    exp/html: generate replacement for <isindex> correctly
    
    When generating replacement elements for an <isindex> tag, the old
    addSyntheticElement method was producing the wrong nesting. Replace
    it with parseImpliedToken.
    
    Pass the one remaining test in the test suite.
    
    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/6453114

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/3ba25e76a7dbb1574006081f597dcc6d9b569869

元コミット内容

exp/html: generate replacement for <isindex> correctly

When generating replacement elements for an <isindex> tag, the old
addSyntheticElement method was producing the wrong nesting. Replace
it with parseImpliedToken.

Pass the one remaining test in the test suite.

R=nigeltao
CC=golang-dev
https://golang.org/cl/6453114

変更の背景

この変更の背景には、HTMLの <isindex> タグの特殊な性質と、それをHTMLパーサーがどのように扱うべきかという問題があります。

<isindex> タグは、HTML 2.0で導入され、ユーザーが単一のキーワードクエリを入力するための入力フィールドを生成するために使用されました。しかし、HTML 4.01で非推奨となり、HTML5では完全に廃止されました。現代のウェブ開発では <form><input type="text"> を組み合わせて同様の機能を実現するのが一般的です。

ブラウザやHTMLパーサーは、後方互換性のために、非推奨または廃止された要素に対しても特定の挙動を定義しています。<isindex> タグの場合、多くのブラウザはこれをパースする際に、内部的に <form><hr><label><input> といった要素を「合成」して、視覚的に検索フォームのように見えるようにしていました。

Go言語の exp/html パッケージは、HTML5のパースアルゴリズムに準拠しようとしていましたが、この <isindex> タグの合成要素生成ロジックに問題がありました。具体的には、addSyntheticElement メソッドを使用してこれらの要素を追加する際に、要素間のネスト関係が正しく構築されず、結果として不適切なHTMLツリーが生成されていました。この不正確なネストは、生成されたHTMLの構造的な整合性を損ない、後続の処理やレンダリングに問題を引き起こす可能性がありました。

このコミットは、このネストの問題を解決し、<isindex> タグがパースされた際に、HTML5の仕様に沿った(またはそれに近い)正しい代替要素の構造が生成されるようにすることを目的としています。

前提知識の解説

<isindex> タグ

<isindex> は、HTML 2.0で導入された要素で、ユーザーが単一のキーワードクエリを入力するためのテキスト入力フィールドを生成するために使用されました。このタグは通常、ドキュメントの <head> セクションに配置され、ブラウザはこれを検出すると、ページ内に検索フォームのようなUIを自動的に生成しました。

例:

<head>
  <isindex prompt="Enter search keywords:">
</head>
<body>
  <h1>My Page</h1>
</body>

このHTMLは、ブラウザによって以下のような構造に変換されてレンダリングされることが期待されました(ブラウザの実装によって異なる場合がありますが、一般的にはフォーム要素が合成されます):

<form action="[現在のURL]">
  <hr>
  <label>Enter search keywords: <input type="text" name="isindex"></label>
  <hr>
</form>

しかし、このタグはHTML 4.01で非推奨となり、HTML5では完全に廃止されました。その理由は、より柔軟でセマンティックな <form> および <input> 要素の組み合わせで同様の機能が実現できるためです。現代のウェブ開発では使用されることはありませんが、古いHTMLドキュメントを正確にパースするためには、パーサーがこのタグの挙動を理解し、適切に処理する必要があります。

HTMLパーシングの基本

HTMLパーシングは、HTMLドキュメントの文字列を読み込み、それをブラウザが理解できるDOM(Document Object Model)ツリー構造に変換するプロセスです。このプロセスは通常、以下の主要な段階を含みます。

  1. トークン化 (Tokenization): HTMLの文字列を、開始タグ、終了タグ、テキスト、コメントなどの「トークン」のストリームに分解します。
  2. ツリー構築 (Tree Construction): トークンのストリームを読み込み、それらをDOMツリーのノードとして追加していきます。この段階では、要素のネスト関係、属性、テキストコンテンツなどが決定されます。HTMLの仕様には、タグの閉じ忘れや不正なネストに対するエラー処理ルール(「quirks mode」や「standards mode」など)が詳細に定義されており、パーサーはこれに従ってDOMツリーを構築します。

HTMLは非常に寛容な言語であり、不正なマークアップであってもブラウザは可能な限りレンダリングしようとします。このため、HTMLパーサーは、仕様に厳密に従うだけでなく、一般的なブラウザの挙動(「エラー回復」)を模倣する必要があります。

Go言語の exp/html パッケージ

exp/html は、Go言語の標準ライブラリの一部として提供されていた実験的なHTMLパーサーパッケージです。このパッケージは、HTML5のパースアルゴリズムに準拠することを目指して設計されました。最終的には、このパッケージの機能は golang.org/x/net/html パッケージに移行され、現在ではGo言語でHTMLをパースする際の標準的な選択肢となっています。

このパッケージは、HTMLドキュメントをトークン化し、DOMツリーを構築するための機能を提供します。ウェブスクレイピング、HTMLテンプレート処理、HTMLのサニタイズなど、様々な用途で利用されます。

合成要素 (Synthetic Elements) と暗黙のトークン (Implied Tokens)

HTMLパーシングにおいて、「合成要素」とは、HTMLソースコードには明示的に記述されていないが、パーサーが特定のルールに基づいてDOMツリーに自動的に追加する要素のことです。例えば、多くのブラウザは、<table> タグの直後に <tr> が来た場合、自動的に <tbody> を合成して <tr> をその中にネストさせます。これは、HTMLの構造的な整合性を保つためや、古いHTMLの挙動を模倣するために行われます。

「暗黙のトークン (Implied Tokens)」は、この合成要素の概念と密接に関連しています。パーサーが特定の状況下で、あたかも特定の開始タグや終了タグが入力ストリームに存在するかのように振る舞うことを指します。これは、HTMLの「エラー回復」メカニズムの一部であり、不完全なHTMLマークアップを「修正」して有効なDOMツリーを構築するために使用されます。

addSyntheticElement のようなメソッドは、単に子ノードとして要素を追加するだけかもしれません。しかし、parseImpliedToken のようなメソッドは、パーサーの内部状態(例えば、要素スタックや挿入モード)を適切に更新しながら、あたかもそのトークンが実際に読み込まれたかのように要素を処理します。これにより、より複雑なHTMLパーシングのルール(特にネストのルール)が正確に適用され、結果としてより堅牢で正しいDOMツリーが構築されます。

このコミットでは、addSyntheticElement が単にノードを追加するだけで、パーサーの内部状態を適切に更新していなかったために、<isindex> の代替要素のネストが誤っていたと考えられます。parseImpliedToken に置き換えることで、パーサーがHTML5の仕様に沿った「暗黙のトークン」の処理ロジックを適用し、正しいネストを実現しています。

技術的詳細

このコミットの技術的詳細の核心は、Go言語の exp/html パッケージにおけるHTMLパーサーの内部ロジック、特に <isindex> タグの処理方法の改善にあります。

HTML5のパースアルゴリズムでは、特定の状況下で要素が「暗黙的に」生成されることがあります。<isindex> タグは、その廃止された性質にもかかわらず、後方互換性のために、パース時に特定の要素群(<form>, <hr>, <label>, <input> など)を合成してDOMツリーに追加するよう定義されています。

以前の実装では、addSyntheticElement というメソッドがこの合成処理に使用されていました。このメソッドは、指定されたタグと属性を持つ新しい要素ノードを作成し、現在のノードの子として追加するシンプルなものでした。しかし、HTMLのパースにおいては、単にノードを追加するだけでなく、パーサーの内部状態(例えば、現在開いている要素のスタック、挿入モードなど)を適切に管理することが極めて重要です。特に、要素のネスト関係は、このスタックの状態に大きく依存します。

コミットメッセージが示唆するように、addSyntheticElement は「producing the wrong nesting」(誤ったネストを生成していた)とのことです。これは、このメソッドが要素を追加する際に、パーサーの要素スタックを適切に操作していなかったため、新しく追加された要素が期待される親要素の下に正しくネストされなかったり、不適切な要素がスタックに残ったりした可能性が高いです。結果として、<isindex> から生成されるはずの <form><input> などの要素が、HTMLの仕様に沿った正しい階層構造を持たなかったと考えられます。

この問題を解決するために、parseImpliedToken メソッドが導入され、addSyntheticElement の代わりに使用されました。parseImpliedToken は、単にノードを追加するのではなく、指定されたトークン(この場合は開始タグトークンや終了タグトークン)が入力ストリームから読み込まれたかのようにパーサーに処理させます。これにより、パーサーはHTML5のパースアルゴリズムに定義されている「暗黙のトークン」の処理ルールを適用し、要素スタックの操作、挿入モードの変更、そして結果として正しいネスト構造の構築を自動的に行います。

具体的には、<isindex> の処理中に、<form><hr><label> の開始タグが parseImpliedToken(StartTagToken, ...) を介して「読み込まれた」かのように処理されます。これにより、これらの要素が適切にスタックにプッシュされ、正しい親子の関係が確立されます。また、<label><form> の終了タグも parseImpliedToken(EndTagToken, ...) を介して処理されることで、これらの要素が適切にスタックからポップされ、そのスコープが正しく閉じられます。

a.Input 要素に関しては、parseImpliedToken ではなく p.addChild を直接使用して追加されています。これは、<input> が自己終了タグであり、特定のコンテキストではスタック操作が不要、または addChild で十分な場合があるためと考えられます。しかし、他の要素が parseImpliedToken に変更されたことで、全体的なネストの整合性が保たれるようになっています。

この変更により、exp/html パーサーは、廃止された <isindex> タグに対しても、HTML5の仕様に準拠した、より正確で堅牢なDOMツリーを生成できるようになりました。これにより、パーサーの信頼性が向上し、様々なHTMLドキュメントに対する互換性が確保されます。

コアとなるコードの変更箇所

変更は主に src/pkg/exp/html/parse.go ファイルの parser 構造体と inBodyIM 関数に集中しています。

--- a/src/pkg/exp/html/parse.go
+++ b/src/pkg/exp/html/parse.go
@@ -19,9 +19,8 @@ type parser struct {
 	tokenizer *Tokenizer
 	// tok is the most recently read token.
 	tok Token
-	// Self-closing tags like <hr/> are re-interpreted as a two-token sequence:
-	// <hr> followed by </hr>. hasSelfClosingToken is true if we have just read
-	// the synthetic start tag and the next one due is the matching end tag.
+	// Self-closing tags like <hr/> are treated as start tags, except that
+	// hasSelfClosingToken is set while they are being processed.
 	hasSelfClosingToken bool
 	// doc is the document root element.
 	doc *Node
@@ -313,16 +312,6 @@ func (p *parser) addElement() {
 	})\n }\n \n-// addSyntheticElement adds a child element with the given tag and attributes.\n-func (p *parser) addSyntheticElement(tagAtom a.Atom, attr []Attribute) {\n-\tp.addChild(&Node{\n-\t\tType:     ElementNode,\n-\t\tDataAtom: tagAtom,\n-\t\tData:     tagAtom.String(),\n-\t\tAttr:     attr,\n-\t})\n-}\n-\n // Section 12.2.3.3.\n func (p *parser) addFormattingElement() {\n \ttagAtom, attr := p.tok.DataAtom, p.tok.Attr\n@@ -935,22 +924,23 @@ func inBodyIM(p *parser) bool {\n \t\t\t}\n \t\t\tp.acknowledgeSelfClosingTag()\n \t\t\tp.popUntil(buttonScope, a.P)\n-\t\t\tp.addSyntheticElement(a.Form, nil)\n-\t\t\tp.form = p.top()\n+\t\t\tp.parseImpliedToken(StartTagToken, a.Form, a.Form.String())\n \t\t\tif action != \"\" {\n \t\t\t\tp.form.Attr = []Attribute{{Key: \"action\", Val: action}}\n \t\t\t}\n-\t\t\tp.addSyntheticElement(a.Hr, nil)\n-\t\t\tp.oe.pop()\n-\t\t\tp.addSyntheticElement(a.Label, nil)\n+\t\t\tp.parseImpliedToken(StartTagToken, a.Hr, a.Hr.String())\n+\t\t\tp.parseImpliedToken(StartTagToken, a.Label, a.Label.String())\n \t\t\tp.addText(prompt)\n-\t\t\tp.addSyntheticElement(a.Input, attr)\n-\t\t\tp.oe.pop()\n-\t\t\tp.oe.pop()\n-\t\t\tp.addSyntheticElement(a.Hr, nil)\n+\t\t\tp.addChild(&Node{\n+\t\t\t\tType:     ElementNode,\n+\t\t\t\tDataAtom: a.Input,\n+\t\t\t\tData:     a.Input.String(),\n+\t\t\t\tAttr:     attr,\n+\t\t\t})\n \t\t\tp.oe.pop()\n-\t\t\tp.oe.pop()\n-\t\t\tp.form = nil\n+\t\t\tp.parseImpliedToken(EndTagToken, a.Label, a.Label.String())\n+\t\t\tp.parseImpliedToken(StartTagToken, a.Hr, a.Hr.String())\n+\t\t\tp.parseImpliedToken(EndTagToken, a.Form, a.Form.String())\n \t\tcase a.Textarea:\n \t\t\tp.addElement()\n \t\t\tp.setOriginalIM()\n@@ -1036,7 +1026,7 @@ func inBodyIM(p *parser) bool {\n \t\t\tp.oe.remove(node)\n \t\tcase a.P:\n \t\t\tif !p.elementInScope(buttonScope, a.P) {\n-\t\t\t\tp.addSyntheticElement(a.P, nil)\n+\t\t\t\tp.parseImpliedToken(StartTagToken, a.P, a.P.String())\n \t\t\t}\n \t\t\tp.popUntil(buttonScope, a.P)\n \t\tcase a.Li:\n```

また、テストログファイル `src/pkg/exp/html/testlogs/webkit02.dat.log` も更新されています。

```diff
--- a/src/pkg/exp/html/testlogs/webkit02.dat.log
+++ b/src/pkg/exp/html/testlogs/webkit02.dat.log
@@ -9,5 +9,5 @@ PASS "<table><thead><td></tbody>A"
 PASS "<legend>test</legend>"
 PASS "<table><input>"
 PASS "<b><em><dcell><postfield><postfield><postfield><postfield><missing_glyph><missing_glyph><missing_glyph><missing_glyph><hkern><aside></b></em>"
-FAIL "<isindex action=\"x\">"
+PASS "<isindex action=\"x\">"
 PASS "<option><XH<optgroup></optgroup>"

コアとなるコードの解説

addSyntheticElement メソッドの削除

まず、parser 構造体のメソッドとして定義されていた addSyntheticElement が完全に削除されています。このメソッドは、指定されたタグと属性を持つ新しい ElementNode を作成し、それを現在のノードの子として追加するだけのシンプルなものでした。コミットメッセージにあるように、このメソッドが「誤ったネスト」を引き起こしていたため、より適切な処理を行う parseImpliedToken に置き換えられました。

inBodyIM 関数内の変更

inBodyIM 関数は、HTMLパーサーが「in body」挿入モード(HTMLコンテンツの大部分がパースされるモード)で <isindex> タグを処理する際のロジックを含んでいます。この関数内で、<isindex> タグが検出された際の合成要素の生成方法が大きく変更されています。

  1. <form> 要素の生成:

    • 変更前: p.addSyntheticElement(a.Form, nil)
    • 変更後: p.parseImpliedToken(StartTagToken, a.Form, a.Form.String())
    • これは、<form> の開始タグが入力ストリームに存在するかのようにパーサーに処理させることを意味します。これにより、パーサーの要素スタックに <form> が適切にプッシュされ、その後の要素が <form> の子として正しくネストされるようになります。
  2. <hr> 要素の生成 (1回目):

    • 変更前: p.addSyntheticElement(a.Hr, nil)
    • 変更後: p.parseImpliedToken(StartTagToken, a.Hr, a.Hr.String())
    • 同様に、<hr> の開始タグが暗黙的に処理され、正しいネストが保証されます。
  3. <label> 要素の生成:

    • 変更前: p.addSyntheticElement(a.Label, nil)
    • 変更後: p.parseImpliedToken(StartTagToken, a.Label, a.Label.String())
    • <label> の開始タグも暗黙的に処理されます。
  4. <input> 要素の生成:

    • 変更前: p.addSyntheticElement(a.Input, attr)
    • 変更後: 新しい Node を直接 p.addChild で追加する形式に変更。
    • p.addChild(&Node{Type: ElementNode, DataAtom: a.Input, Data: a.Input.String(), Attr: attr,})
    • <input> は自己終了タグであり、HTML5のパースルールでは特別なスタック操作を必要としない場合があるため、addChild で直接追加する形が選択されたと考えられます。しかし、周囲の要素が parseImpliedToken に変更されたことで、全体的なコンテキストでのネストは正しくなります。
  5. 要素のポップ処理の変更:

    • 変更前は、p.oe.pop() が複数回呼び出され、要素スタックから手動で要素をポップしていました。これは、addSyntheticElement がスタック操作を行わないため、手動で調整する必要があったことを示唆しています。
    • 変更後、これらの手動ポップの多くが削除され、代わりに parseImpliedToken を使用した終了タグの処理が追加されています。
    • p.parseImpliedToken(EndTagToken, a.Label, a.Label.String())
    • p.parseImpliedToken(EndTagToken, a.Form, a.Form.String())
    • これは、<label><form> の終了タグが入力ストリームに存在するかのようにパーサーに処理させることを意味します。これにより、パーサーは自動的に要素スタックからこれらの要素をポップし、そのスコープを正しく閉じます。このアプローチは、手動で pop を呼び出すよりも、HTML5のパースアルゴリズムに準拠した、より堅牢でエラー回復能力の高い方法です。
  6. <hr> 要素の生成 (2回目):

    • 変更前: p.addSyntheticElement(a.Hr, nil)
    • 変更後: p.parseImpliedToken(StartTagToken, a.Hr, a.Hr.String())
    • これも同様に、暗黙の開始タグとして処理されます。
  7. <p> 要素の生成:

    • 変更前: p.addSyntheticElement(a.P, nil)
    • 変更後: p.parseImpliedToken(StartTagToken, a.P, a.P.String())
    • <isindex> 以外のコンテキストでも、addSyntheticElementparseImpliedToken に置き換えられています。これは、addSyntheticElement が抱えていたネストの問題が、<isindex> 以外の合成要素生成にも影響を与えていた可能性を示唆しています。

テストログの変更

src/pkg/exp/html/testlogs/webkit02.dat.log ファイルでは、<isindex action="x"> というテストケースの結果が FAIL から PASS に変更されています。これは、上記のコード変更によって、<isindex> タグのパースと代替要素の生成が正しく行われるようになったことを直接的に示しています。

これらの変更により、exp/html パッケージは、HTML5のパースアルゴリズムにさらに厳密に準拠し、特に廃止された要素の処理において、より正確で堅牢なDOMツリーを構築できるようになりました。

関連リンク

参考にした情報源リンク