[インデックス 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)ツリー構造に変換するプロセスです。このプロセスは通常、以下の主要な段階を含みます。
- トークン化 (Tokenization): HTMLの文字列を、開始タグ、終了タグ、テキスト、コメントなどの「トークン」のストリームに分解します。
- ツリー構築 (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>
タグが検出された際の合成要素の生成方法が大きく変更されています。
-
<form>
要素の生成:- 変更前:
p.addSyntheticElement(a.Form, nil)
- 変更後:
p.parseImpliedToken(StartTagToken, a.Form, a.Form.String())
- これは、
<form>
の開始タグが入力ストリームに存在するかのようにパーサーに処理させることを意味します。これにより、パーサーの要素スタックに<form>
が適切にプッシュされ、その後の要素が<form>
の子として正しくネストされるようになります。
- 変更前:
-
<hr>
要素の生成 (1回目):- 変更前:
p.addSyntheticElement(a.Hr, nil)
- 変更後:
p.parseImpliedToken(StartTagToken, a.Hr, a.Hr.String())
- 同様に、
<hr>
の開始タグが暗黙的に処理され、正しいネストが保証されます。
- 変更前:
-
<label>
要素の生成:- 変更前:
p.addSyntheticElement(a.Label, nil)
- 変更後:
p.parseImpliedToken(StartTagToken, a.Label, a.Label.String())
<label>
の開始タグも暗黙的に処理されます。
- 変更前:
-
<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
に変更されたことで、全体的なコンテキストでのネストは正しくなります。
- 変更前:
-
要素のポップ処理の変更:
- 変更前は、
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のパースアルゴリズムに準拠した、より堅牢でエラー回復能力の高い方法です。
- 変更前は、
-
<hr>
要素の生成 (2回目):- 変更前:
p.addSyntheticElement(a.Hr, nil)
- 変更後:
p.parseImpliedToken(StartTagToken, a.Hr, a.Hr.String())
- これも同様に、暗黙の開始タグとして処理されます。
- 変更前:
-
<p>
要素の生成:- 変更前:
p.addSyntheticElement(a.P, nil)
- 変更後:
p.parseImpliedToken(StartTagToken, a.P, a.P.String())
<isindex>
以外のコンテキストでも、addSyntheticElement
がparseImpliedToken
に置き換えられています。これは、addSyntheticElement
が抱えていたネストの問題が、<isindex>
以外の合成要素生成にも影響を与えていた可能性を示唆しています。
- 変更前:
テストログの変更
src/pkg/exp/html/testlogs/webkit02.dat.log
ファイルでは、<isindex action="x">
というテストケースの結果が FAIL
から PASS
に変更されています。これは、上記のコード変更によって、<isindex>
タグのパースと代替要素の生成が正しく行われるようになったことを直接的に示しています。
これらの変更により、exp/html
パッケージは、HTML5のパースアルゴリズムにさらに厳密に準拠し、特に廃止された要素の処理において、より正確で堅牢なDOMツリーを構築できるようになりました。
関連リンク
- Go CL 6453114: https://golang.org/cl/6453114
参考にした情報源リンク
- HTML
isindex
element: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/isindex - HTML5 Parsing Algorithm (W3C Recommendation): https://html.spec.whatwg.org/multipage/parsing.html
- Go
golang.org/x/net/html
package: https://pkg.go.dev/golang.org/x/net/html - HTML Standard - The
isindex
element: https://html.spec.whatwg.org/multipage/obsolete.html#the-isindex-element - HTML Standard - 13.2.6.4.1 The "in body" insertion mode: https://html.spec.whatwg.org/multipage/parsing.html#the-in-body-insertion-mode
- HTML Standard - 13.2.5.1 The tokenization stage: https://html.spec.whatwg.org/multipage/parsing.html#tokenization
- HTML Standard - 13.2.5.2 The tree construction stage: https://html.spec.whatwg.org/multipage/parsing.html#tree-construction
- HTML Standard - 13.2.6.1 The stack of open elements: https://html.spec.whatwg.org/multipage/parsing.html#the-stack-of-open-elements