[インデックス 10332] ファイルの概要
このコミットは、Go言語の標準ライブラリである html パッケージにおけるHTMLパーサーの改善に関するものです。特に、HTML5の仕様に準拠した frameset 要素のパース処理を正確に行うための変更が含まれています。これにより、特定のHTML構造(特に frameset と noframes を含むもの)が正しくDOMツリーとして構築されるようになり、関連するテストケースがパスするようになりました。
コミット
commit e9e874b7fcc722e2e9af942761b8fc2cd8e2c240
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Thu Nov 10 23:56:13 2011 +1100
html: parse framesets
Pass tests1.dat, test 106:
<frameset><frame><frameset><frame></frameset><noframes></noframes></frameset>
| <html>
| <head>
| <frameset>
| <frame>
| <frameset>
| <frame>
| <noframes>
Also pass test 107:
<h1><table><td><h3></table><h3></h1>
R=nigeltao
CC=golang-dev
https://golang.org/cl/5373050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e9e874b7fcc722e2e9af942761b8fc2cd8e2c240
元コミット内容
このコミットは、Go言語の html パッケージにおいて、frameset 要素のパース処理を実装し、関連するテストケース(tests1.dat のテスト106およびテスト107)をパスするように修正するものです。
具体的には、以下のHTML構造が正しくパースされることを目的としています。
- テスト106:
<frameset><frame><frameset><frame></frameset><noframes></noframes></frameset>- この構造が、期待されるDOMツリー(
<html><head><frameset><frame><frameset><frame><noframes>)として表現されることを確認します。
- この構造が、期待されるDOMツリー(
- テスト107:
<h1><table><td><h3></table><h3></h1>- このテストは、
framesetとは直接関係ありませんが、HTMLパーサーの堅牢性を確認するためのものです。
- このテストは、
変更の背景
HTMLのパースは、ウェブブラウザがウェブページを表示するために不可欠なプロセスです。特にHTML5の仕様では、エラーを含む不完全なHTMLであっても、一貫した方法でパースするための詳細なアルゴリズムが定義されています。Go言語の html パッケージは、このHTML5パースアルゴリズムに準拠することを目指しています。
このコミットが行われた当時、frameset 要素のパース処理が不完全であったため、特定のHTML構造が正しくDOMツリーとして表現されない問題がありました。frameset は、複数のHTMLドキュメントを一つのウィンドウ内に表示するための古いHTML要素ですが、HTML5でも互換性のためにそのパースルールが定義されています。
このコミットの目的は、frameset 要素とその関連要素(frame, noframes)がHTML5のパースアルゴリズムに従って正確に処理されるように、パーサーの「挿入モード(insertion mode)」ロジックを拡張することでした。これにより、Goの html パッケージがより堅牢で標準準拠のHTMLパーサーとなることが期待されました。
前提知識の解説
HTML5パースアルゴリズム
HTML5のパースアルゴリズムは、ウェブブラウザがHTMLドキュメントを解析し、DOM(Document Object Model)ツリーを構築するための詳細な手順を定めたものです。このアルゴリズムは、非常に堅牢であり、構文エラーを含むHTMLでも一貫した結果を生成するように設計されています。
主要な概念として、以下のものがあります。
- トークナイゼーション(Tokenization): 入力されたHTML文字列を、タグ、属性、テキストなどの「トークン」に分解するプロセスです。
- ツリー構築(Tree Construction): トークナイザーから受け取ったトークンを基に、DOMツリーを構築するプロセスです。
- 挿入モード(Insertion Mode): ツリー構築アルゴリズムの中心的な概念です。パーサーは常に特定の「挿入モード」にあり、このモードによって、次に受け取るトークンがどのように処理されるかが決定されます。例えば、
<head>タグ内では「in head」モード、<body>タグ内では「in body」モードなどがあります。各モードには、特定のタグが来た場合の処理(要素の挿入、スタックからのポップ、エラー処理など)が詳細に定義されています。
frameset, frame, noframes 要素
これらの要素は、HTML4以前でウェブページを分割するために使用されていました。HTML5では非推奨とされていますが、後方互換性のためにパースルールが定義されています。
<frameset>: 複数のフレームを定義するためのコンテナ要素です。<body>要素の代わりに使用され、ウィンドウを複数の領域に分割します。<frame>:frameset内で、個々のフレーム(別のHTMLドキュメントを表示する領域)を定義します。<noframes>:framesetをサポートしないブラウザ向けに、代替コンテンツを提供するための要素です。frameset内に配置されます。
これらの要素は、通常のHTML要素とは異なるパースルールを持つため、パーサーはこれらの要素を検出した際に、適切な挿入モードに切り替える必要があります。
技術的詳細
このコミットの技術的な核心は、Go言語の html パッケージにおけるHTML5パースアルゴリズムの実装に、frameset 関連の挿入モードとそれらの遷移ルールを追加した点にあります。
HTML5パースアルゴリズムのセクション11.2.5.4には、様々な挿入モードが定義されており、このコミットでは特に以下のモードが追加または修正されています。
inFramesetIM(In frameset insertion mode):frameset要素がオープンされているときにパーサーが遷移するモードです。このモードでは、frameやネストされたframeset、あるいはnoframesなどの要素が特別に処理されます。afterFramesetIM(After frameset insertion mode):frameset要素が閉じられた後にパーサーが遷移するモードです。このモードでは、htmlやnoframesなどの特定の要素が処理されます。afterAfterFramesetIM(After after frameset insertion mode):afterFramesetIMからさらに遷移する可能性のあるモードで、ドキュメントの終わりに近い状態でのframeset関連の処理を扱います。
コミットの変更点を見ると、parse.go 内の resetInsertionMode 関数が frameset 要素を検出した際に inFramesetIM に遷移するように修正されています。また、afterHeadIM 関数も frameset タグを検出した場合に inFramesetIM に遷移し、要素を追加するロジックが追加されています。
新しい挿入モード関数 (inFramesetIM, afterFramesetIM, afterAfterFramesetIM) は、それぞれ以下のルールに従ってトークンを処理します。
- コメントトークン: コメントノードとして子に追加されます。
- 開始タグトークン:
html:inBodyIMまたはinHeadIMのルールを適用して処理されます。frameset: 新しいframeset要素が追加されます。frame:frame要素が追加され、すぐにポップされます(自己終了タグとして扱われるため)。noframes:inHeadIMのルールを適用して処理されます。
- 終了タグトークン:
frameset: 現在の要素スタックのトップがframesetであればポップされ、必要に応じてafterFramesetIMに遷移します。html:afterAfterFramesetIMに遷移します。
- その他のトークン: 基本的に無視されます。
これらの変更により、frameset を含む複雑なHTML構造が、HTML5の仕様に厳密に従ってDOMツリーとして構築されるようになります。
コアとなるコードの変更箇所
--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -321,7 +321,7 @@ func (p *parser) resetInsertionMode() insertionMode {
case "body":
return inBodyIM
case "frameset":
- // TODO: return inFramesetIM
+ return inFramesetIM
case "html":
return beforeHeadIM
}
@@ -517,7 +517,8 @@ func afterHeadIM(p *parser) (insertionMode, bool) {
attr = p.tok.Attr
framesetOK = false
case "frameset":
- // TODO.
+ p.addElement(p.tok.Data, p.tok.Attr)
+ return inFramesetIM, true
case "base", "basefont", "bgsound", "link", "meta", "noframes", "script", "style", "title":
p.oe = append(p.oe, p.head)
defer p.oe.pop()
@@ -646,7 +647,7 @@ func inBodyIM(p *parser) (insertionMode, bool) {
break
}
p.popUntil(buttonScopeStopTags, "p")
- p.addElement("li", p.tok.Attr)
+ p.addElement(p.tok.Data, p.tok.Attr)
case "optgroup", "option":
if p.top().Data == "option" {
p.oe.pop()
@@ -1169,6 +1170,69 @@ func afterBodyIM(p *parser) (insertionMode, bool) {
return afterBodyIM, true
}
+// Section 11.2.5.4.19.
+func inFramesetIM(p *parser) (insertionMode, bool) {
+ switch p.tok.Type {
+ case CommentToken:
+ p.addChild(&Node{
+ Type: CommentNode,
+ Data: p.tok.Data,
+ })
+ case StartTagToken:
+ switch p.tok.Data {
+ case "html":
+ return useTheRulesFor(p, inFramesetIM, inBodyIM)
+ case "frameset":
+ p.addElement(p.tok.Data, p.tok.Attr)
+ case "frame":
+ p.addElement(p.tok.Data, p.tok.Attr)
+ p.oe.pop()
+ p.acknowledgeSelfClosingTag()
+ case "noframes":
+ return useTheRulesFor(p, inFramesetIM, inHeadIM)
+ }
+ case EndTagToken:
+ switch p.tok.Data {
+ case "frameset":
+ if p.oe.top().Data != "html" {
+ p.oe.pop()
+ if p.oe.top().Data != "frameset" {
+ return afterFramesetIM, true
+ }
+ }
+ }
+ default:
+ // Ignore the token.
+ }
+ return inFramesetIM, true
+}
+
+// Section 11.2.5.4.20.
+func afterFramesetIM(p *parser) (insertionMode, bool) {
+ switch p.tok.Type {
+ case CommentToken:
+ p.addChild(&Node{
+ Type: CommentNode,
+ Data: p.tok.Data,
+ })
+ case StartTagToken:
+ switch p.tok.Data {
+ case "html":
+ return useTheRulesFor(p, inFramesetIM, inBodyIM)
+ case "noframes":
+ return useTheRulesFor(p, inFramesetIM, inHeadIM)
+ }
+ case EndTagToken:
+ switch p.tok.Data {
+ case "html":
+ return afterAfterFramesetIM, true
+ }
+ default:
+ // Ignore the token.
+ }
+ return afterFramesetIM, true
+}
+
// Section 11.2.5.4.21.
func afterAfterBodyIM(p *parser) (insertionMode, bool) {
switch p.tok.Type {
@@ -1191,6 +1255,27 @@ func afterAfterBodyIM(p *parser) (insertionMode, bool) {
return inBodyIM, false
}
+// Section 11.2.5.4.22.
+func afterAfterFramesetIM(p *parser) (insertionMode, bool) {
+ switch p.tok.Type {
+ case CommentToken:
+ p.addChild(&Node{
+ Type: CommentNode,
+ Data: p.tok.Data,
+ })
+ case StartTagToken:
+ switch p.tok.Data {
+ case "html":
+ return useTheRulesFor(p, afterAfterFramesetIM, inBodyIM)
+ case "noframes":
+ return useTheRulesFor(p, afterAfterFramesetIM, inHeadIM)
+ }
+ default:
+ // Ignore the token.
+ }
+ return afterAfterFramesetIM, true
+}
+
// Parse returns the parse tree for the HTML from the given Reader.
// The input is assumed to be UTF-8 encoded.
func Parse(r io.Reader) (*Node, error) {
diff --git a/src/pkg/html/parse_test.go b/src/pkg/html/parse_test.go
index 8cef0fa8e3..0e93a9de84 100644
--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -133,7 +133,7 @@ func TestParser(t *testing.T) {
\tn int
}{
// TODO(nigeltao): Process all the test cases from all the .dat files.
- {"tests1.dat", 106},
+ {"tests1.dat", 108},
{"tests2.dat", 0},
{"tests3.dat", 0},
}
コアとなるコードの解説
src/pkg/html/parse.go
-
resetInsertionMode関数の修正:- 以前は
framesetタグが検出された際に// TODO: return inFramesetIMとコメントアウトされていましたが、このコミットでreturn inFramesetIMが有効化されました。これにより、パーサーがframeset要素の内部にいることを正しく認識し、適切な挿入モードに切り替わるようになります。
- 以前は
-
afterHeadIM関数の修正:framesetタグがafterHeadIMモード(<head>タグの直後)で検出された場合、以前は// TODO.となっていましたが、このコミットでp.addElement(p.tok.Data, p.tok.Attr)を呼び出してframeset要素をDOMツリーに追加し、その後inFramesetIMに遷移するように変更されました。これは、HTML5の仕様でframesetがheadの後に続く場合の処理を反映しています。
-
inBodyIM関数の修正:li要素の追加ロジックがp.addElement("li", p.tok.Attr)からp.addElement(p.tok.Data, p.tok.Attr)に変更されています。これは、liだけでなく、現在のトークンのデータ(タグ名)を汎用的に使用するように修正されたもので、より柔軟な要素追加を可能にします。この変更はframesetと直接関係ありませんが、パーサーの一般的な改善の一部です。
-
新しい挿入モード関数の追加:
inFramesetIM: HTML5仕様のセクション11.2.5.4.19「In frameset insertion mode」に対応する関数です。- コメントトークンは子ノードとして追加されます。
- 開始タグ
htmlはinBodyIMのルールで処理されます。 - 開始タグ
framesetは新しいframeset要素として追加されます。 - 開始タグ
frameはframe要素として追加され、すぐにスタックからポップされます(HTML5ではframeは自己終了要素として扱われるため)。また、p.acknowledgeSelfClosingTag()が呼び出され、自己終了タグとして認識されます。 - 開始タグ
noframesはinHeadIMのルールで処理されます。 - 終了タグ
framesetは、要素スタックのトップがhtmlでない限り、スタックからポップされます。もしポップされた要素がframesetでなければ、afterFramesetIMに遷移します。 - その他のトークンは無視されます。
afterFramesetIM: HTML5仕様のセクション11.2.5.4.20「After frameset insertion mode」に対応する関数です。- コメントトークンは子ノードとして追加されます。
- 開始タグ
htmlはinBodyIMのルールで処理されます。 - 開始タグ
noframesはinHeadIMのルールで処理されます。 - 終了タグ
htmlはafterAfterFramesetIMに遷移します。 - その他のトークンは無視されます。
afterAfterFramesetIM: HTML5仕様のセクション11.2.5.4.22「After after frameset insertion mode」に対応する関数です。- コメントトークンは子ノードとして追加されます。
- 開始タグ
htmlはinBodyIMのルールで処理されます。 - 開始タグ
noframesはinHeadIMのルールで処理されます。 - その他のトークンは無視されます。
これらの新しい挿入モードと既存のモードからの遷移ルールの追加により、frameset を含むHTMLドキュメントがHTML5の仕様に厳密に従ってパースされ、正しいDOMツリーが構築されるようになります。
src/pkg/html/parse_test.go
- テストケース番号の更新:
TestParser関数内のtests1.datのテストケース数が106から108に変更されています。これは、tests1.datファイル内のテストケースの総数が増加したか、またはこのコミットで追加されたテストケースがtests1.datの末尾に追加されたことを示唆しています。この変更自体はパースロジックの変更ではなく、テストスイートの更新です。
関連リンク
- Go CL 5373050: https://golang.org/cl/5373050
参考にした情報源リンク
- HTML5 Parsing Algorithm (W3C Recommendation): https://html.spec.whatwg.org/multipage/parsing.html
- 特に、セクション11.2.5.4「The tree construction dispatcher」およびそのサブセクション(例: 11.2.5.4.19「In frameset insertion mode」)を参照しました。
- HTML
framesetelement: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/frameset - HTML
frameelement: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/frame - HTML
noframeselement: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/noframes - Go
htmlpackage documentation: https://pkg.go.dev/golang.org/x/net/html (当時のパッケージパスはsrc/pkg/htmlでしたが、現在はgolang.org/x/net/htmlに移動しています) - Go
htmlpackage source code (relevant files):parse.go: https://github.com/golang/go/blob/master/src/html/parse.goparse_test.go: https://github.com/golang/go/blob/master/src/html/parse_test.go (注: 上記リンクは現在のGoリポジトリのパスであり、コミット当時のパスとは異なる場合がありますが、内容は関連しています。)