[インデックス 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
frameset
element: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/frameset - HTML
frame
element: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/frame - HTML
noframes
element: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/noframes - Go
html
package documentation: https://pkg.go.dev/golang.org/x/net/html (当時のパッケージパスはsrc/pkg/html
でしたが、現在はgolang.org/x/net/html
に移動しています) - Go
html
package 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リポジトリのパスであり、コミット当時のパスとは異なる場合がありますが、内容は関連しています。)