[インデックス 13136] ファイルの概要
このコミットは、Go言語の実験的なHTMLパーサー (exp/html) において、HTML5仕様に準拠するように特定の挿入モード(afterBodyIM, afterAfterBodyIM, afterAfterFramesetIM)でのテキスト、コメント、およびDOCTYPEトークンの処理を調整するものです。これにより、いくつかのテストがパスするようになります。
コミット
commit 33a89b5fdad1917e292b7a8aea5f164c1460177d
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Wed May 23 11:11:34 2012 +1000
exp/html: adjust the last few insertion modes to match the spec
Handle text, comment, and doctype tokens in afterBodyIM, afterAfterBodyIM,
and afterAfterFramesetIM.
Pass three more tests.
R=nigeltao
CC=golang-dev
https://golang.org/cl/6231043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/33a89b5fdad1917e292b7a8aea5f164c1460177d
元コミット内容
exp/html: adjust the last few insertion modes to match the spec
Handle text, comment, and doctype tokens in afterBodyIM, afterAfterBodyIM,
and afterAfterFramesetIM.
Pass three more tests.
R=nigeltao
CC=golang-dev
https://golang.org/cl/6231043
変更の背景
この変更の背景には、HTML5の複雑なパースアルゴリズムへの正確な準拠があります。HTMLドキュメントのパースは、単にタグを読み込むだけでなく、現在のコンテキスト(「挿入モード」)に基づいて、予期せぬトークンや不正なマークアップをどのように処理するかを厳密に定義しています。Go言語のexp/htmlパッケージは、HTML5仕様に準拠したパーサーを提供することを目指しており、このコミットは、特にドキュメントの終盤や特定の構造(framesetなど)の後に現れる可能性のあるテキスト、コメント、DOCTYPEトークンの処理が仕様と一致していない点を修正することを目的としています。
HTML5のパース仕様は非常に詳細であり、ブラウザ間の互換性を保証するために、すべてのエッジケースを正確に処理する必要があります。このコミットは、既存のテストが失敗していた特定のシナリオを修正し、パーサーの堅牢性と仕様への準拠を向上させるものです。
前提知識の解説
HTML5パースアルゴリズム
HTML5のパースアルゴリズムは、W3Cによって厳密に定義されており、ウェブブラウザがHTMLドキュメントをどのように解析し、DOMツリーを構築するかを規定しています。このアルゴリズムは、ステートマシンとして機能し、入力ストリームからトークンを読み込み、現在の「挿入モード」に基づいて次のアクションを決定します。
トークン化とツリー構築
HTMLパースは大きく分けて二つのフェーズがあります。
- トークン化 (Tokenization): 入力されたHTML文字列を、意味のある単位(トークン)に分解します。例えば、
<p>は「開始タグトークン」、Helloは「テキストトークン」、<!-- comment -->は「コメントトークン」、<!DOCTYPE html>は「DOCTYPEトークン」となります。 - ツリー構築 (Tree Construction): トークナイザーから受け取ったトークンを基に、DOMツリーを構築します。このフェーズが「挿入モード」の概念と密接に関連しています。
挿入モード (Insertion Modes)
挿入モードは、HTML5パースアルゴリズムの中心的な概念です。これは、パーサーが現在ドキュメントのどの部分を処理しているかを示す状態であり、受け取ったトークンに対してどのようなアクションを取るべきかを決定します。HTML5仕様には、以下のような多数の挿入モードが定義されています。
initialbefore htmlbefore headin headin head noscriptafter headin bodytextin tablein table textin captionin column groupin table bodyin rowin cellin selectin select in tablein templateafter body(本コミットで関連)in framesetafter framesetafter after body(本コミットで関連)after after frameset(本コミットで関連)
各挿入モードは、特定のトークンタイプ(開始タグ、終了タグ、テキスト、コメント、DOCTYPEなど)が与えられたときに、DOMツリーにノードを追加したり、モードを切り替えたり、エラーを処理したりする具体的なルールを持っています。
afterBodyIM, afterAfterBodyIM, afterAfterFramesetIM
これらの挿入モードは、ドキュメントの終盤や特定の構造の後にパーサーが到達した状態を表します。
afterBodyIM:<body>タグが閉じられた後、または暗黙的に閉じられた後にパーサーが到達するモードです。通常、このモードでは、HTMLドキュメントの残りの部分(例えば、</html>タグやコメント、空白文字など)を処理します。afterAfterBodyIM:<body>タグと<html>タグの両方が閉じられた後にパーサーが到達するモードです。このモードでは、ドキュメントの末尾にある可能性のあるコメントや空白文字などを処理します。afterAfterFramesetIM:frameset要素が閉じられた後、かつ<html>タグが閉じられた後にパーサーが到達するモードです。このモードも、ドキュメントの末尾に近い部分での特定の要素の処理に関連します。
これらのモードでは、通常は新しい要素が追加されることは稀ですが、仕様では特定のトークン(特にテキスト、コメント、DOCTYPE)がどのように扱われるべきかが厳密に定義されています。例えば、空白文字のみのテキストトークンは無視されるべきか、それともinBodyIMに切り替えて処理されるべきか、といった詳細なルールが存在します。
トークンの種類
- TextToken: HTMLコンテンツ内の通常のテキストを表します。
- CommentToken:
<!-- ... -->形式のHTMLコメントを表します。 - DoctypeToken:
<!DOCTYPE ...>宣言を表します。
技術的詳細
このコミットは、Go言語のexp/htmlパッケージ内のHTMLパーサーのparse.goファイルに対して行われています。具体的には、afterBodyIM、afterAfterBodyIM、afterAfterFramesetIMという3つの挿入モード関数における、TextToken、CommentToken、DoctypeTokenの処理ロジックが修正されています。
afterBodyIM の変更点
以前のafterBodyIMでは、TextTokenが来た場合の処理が不足していました。このコミットでは、TextTokenが来た場合に、そのデータから先頭の空白文字をトリムし、残りの文字列の長さが0(つまり、すべて空白文字だった場合)であれば、パーサーの挿入モードをinBodyIMに切り替えるように修正されています。これは、HTML5仕様において、after bodyモードで空白文字のみのテキストトークンが来た場合、in bodyモードに切り替えて処理を継続するというルールに準拠するためです。
afterAfterBodyIM の変更点
afterAfterBodyIMも同様に、TextTokenの処理が不足していました。afterBodyIMと同様に、テキストトークンから空白文字をトリムし、残りが空であればinBodyIMに切り替えるロジックが追加されています。
さらに、DoctypeTokenが来た場合の処理も追加されました。DoctypeTokenが来た場合も、パーサーの挿入モードをinBodyIMに切り替えるように修正されています。これは、after after bodyモードでDOCTYPEトークンが来た場合の仕様に合わせたものです。
afterAfterFramesetIM の変更点
afterAfterFramesetIMでは、CommentTokenとTextTokenの処理が修正されています。
CommentToken: 以前はp.addChildを使ってコメントノードを追加していましたが、p.doc.Addに変更されています。これは、コメントノードの追加方法をより適切にするための修正と考えられます。TextToken: 以前はテキストを処理した後、p.reconstructActiveFormattingElements()とp.addText(s)を呼び出していましたが、これらが削除され、代わりにトークンのデータ(空白文字をトリムした後)をp.tok.Dataに再設定し、パーサーの挿入モードをinBodyIMに切り替えるように変更されています。これは、after after framesetモードでテキストトークンが来た場合のHTML5仕様の挙動に合わせたものです。DoctypeToken:afterAfterFramesetIMでもDoctypeTokenが来た場合にinBodyIMに切り替えるロジックが追加されました。
これらの変更は、HTML5パースアルゴリズムの厳密なルールに従い、特定の挿入モードで予期されるトークンが来た場合に、パーサーが正しい状態遷移を行い、DOMツリーを正確に構築できるようにするためのものです。特に、空白文字の扱い、コメントの追加、DOCTYPEの再処理といった細かな挙動が仕様に合致するように調整されています。
コアとなるコードの変更箇所
変更は主に src/pkg/exp/html/parse.go ファイルに集中しています。
--- a/src/pkg/exp/html/parse.go
+++ b/src/pkg/exp/html/parse.go
@@ -1597,6 +1597,12 @@ func afterBodyIM(p *parser) bool {
case ErrorToken:
// Stop parsing.
return true
+ case TextToken:
+ s := strings.TrimLeft(p.tok.Data, whitespace)
+ if len(s) == 0 {
+ // It was all whitespace.
+ return inBodyIM(p)
+ }
case StartTagToken:
if p.tok.Data == "html" {
return inBodyIM(p)
@@ -1717,7 +1723,11 @@ func afterAfterBodyIM(p *parser) bool {
// Stop parsing.
return true
case TextToken:
- // TODO.
+ s := strings.TrimLeft(p.tok.Data, whitespace)
+ if len(s) == 0 {
+ // It was all whitespace.
+ return inBodyIM(p)
+ }
case StartTagToken:
if p.tok.Data == "html" {
return inBodyIM(p)
@@ -1728,6 +1738,8 @@ func afterAfterBodyIM(p *parser) bool {
Data: p.tok.Data,
})
return true
+ case DoctypeToken:
+ return inBodyIM(p)
}\n
p.im = inBodyIM
return false
@@ -1737,7 +1749,7 @@ func afterAfterFramesetIM(p *parser) bool {
switch p.tok.Type {
case CommentToken:
- p.addChild(&Node{
+ p.doc.Add(&Node{
Type: CommentNode,
Data: p.tok.Data,
})
@@ -1751,8 +1763,8 @@ func afterAfterFramesetIM(p *parser) bool {
}, p.tok.Data)
if s != "" {
- p.reconstructActiveFormattingElements()
- p.addText(s)
+ p.tok.Data = s
+ return inBodyIM(p)
}
case StartTagToken:
switch p.tok.Data {
@@ -1761,6 +1773,8 @@ func afterAfterFramesetIM(p *parser) bool {
case "noframes":
return inHeadIM(p)
}
+ case DoctypeToken:
+ return inBodyIM(p)
default:
// Ignore the token.
}
また、以下のテストログファイルが更新され、以前失敗していたテストがパスするようになったことが示されています。
src/pkg/exp/html/testlogs/tests18.dat.logsrc/pkg/exp/html/testlogs/tests19.dat.logsrc/pkg/exp/html/testlogs/webkit01.dat.log
コアとなるコードの解説
afterBodyIM および afterAfterBodyIM の TextToken 処理
case TextToken:
s := strings.TrimLeft(p.tok.Data, whitespace)
if len(s) == 0 {
// It was all whitespace.
return inBodyIM(p)
}
このコードブロックは、afterBodyIMとafterAfterBodyIMの両方に追加されています。
s := strings.TrimLeft(p.tok.Data, whitespace): 現在のトークンデータ(テキスト)の先頭から空白文字(whitespace定数で定義されている文字)を削除します。if len(s) == 0: トリムした結果、文字列sの長さが0であれば、それは元のテキストトークンがすべて空白文字で構成されていたことを意味します。return inBodyIM(p): この場合、パーサーの挿入モードをinBodyIM(in body insertion mode)に切り替えます。HTML5仕様では、after bodyやafter after bodyモードで空白文字のみのテキストトークンが来た場合、in bodyモードに切り替えて処理を継続するよう規定されています。これにより、ドキュメントの末尾にある余分な空白文字が適切に処理されるようになります。
afterAfterBodyIM および afterAfterFramesetIM の DoctypeToken 処理
case DoctypeToken:
return inBodyIM(p)
このコードブロックは、afterAfterBodyIMとafterAfterFramesetIMに追加されています。
DoctypeTokenがこれらのモードで現れた場合、パーサーはinBodyIMに切り替えます。これは、HTML5仕様において、これらのモードでDOCTYPEトークンが来た場合の挙動を定義しており、通常はエラーとして扱われるか、特定の条件下でin bodyモードに遷移することがあります。この修正は、その仕様に合わせたものです。
afterAfterFramesetIM の CommentToken 処理
case CommentToken:
p.doc.Add(&Node{
Type: CommentNode,
Data: p.tok.Data,
})
以前はp.addChildが使われていましたが、p.doc.Addに変更されました。p.docはドキュメントのルートノードを表す可能性が高く、Addメソッドはドキュメントツリーに直接ノードを追加するためのより適切な方法であると考えられます。これにより、コメントノードがDOMツリーに正しく追加されることが保証されます。
afterAfterFramesetIM の TextToken 処理の変更
case TextToken:
s := strings.TrimLeftFunc(p.tok.Data, func(r rune) bool {
return unicode.IsSpace(r) || r == '\t' || r == '\n' || r == '\r' || r == '\f'
}, p.tok.Data) // この行は元のdiffと異なりますが、意図を推測して記述
if s != "" {
p.tok.Data = s
return inBodyIM(p)
}
元のコードではp.reconstructActiveFormattingElements()とp.addText(s)が呼び出されていましたが、これらが削除され、代わりにトークンのデータが更新され、inBodyIMに切り替わるようになりました。
これは、after after framesetモードでテキストトークンが来た場合のHTML5仕様の挙動に合わせたものです。このモードでは、テキストが空白文字でない場合、そのテキストをin bodyモードで処理するように再キューイングされるか、または直接in bodyモードに遷移して処理されることが期待されます。この変更は、その挙動を模倣しています。
これらの変更により、GoのHTMLパーサーは、HTML5仕様の複雑なエッジケース、特にドキュメントの終盤における空白文字、コメント、DOCTYPE宣言の処理において、より正確に動作するようになります。これにより、パーサーの堅牢性が向上し、より広範なHTMLドキュメントを正しく解析できるようになります。
関連リンク
- HTML5 Parsing algorithm: https://html.spec.whatwg.org/multipage/parsing.html#parsing
- Go
exp/htmlpackage documentation (古い情報である可能性あり): https://pkg.go.dev/exp/html (現在はgolang.org/x/net/htmlに統合されている可能性が高いです) - Go
golang.org/x/net/htmlpackage: https://pkg.go.dev/golang.org/x/net/html
参考にした情報源リンク
- HTML5仕様 (W3C): https://www.w3.org/TR/html5/
- HTML Standard (WHATWG): https://html.spec.whatwg.org/
- Go言語の
expパッケージに関する情報 (一般的な概念): https://go.dev/doc/go1.4#exp (Go 1.4のリリースノートですが、expパッケージの意図を理解するのに役立ちます) - Go言語の
stringsパッケージ: https://pkg.go.dev/strings - Go言語の
unicodeパッケージ: https://pkg.go.dev/unicode - Go言語の
htmlパッケージの歴史 (exp/htmlからgolang.org/x/net/htmlへの移行など): https://go.dev/blog/go1.4-html (Go 1.4でのhtmlパッケージの変更に関するブログ記事) - Go CL 6231043 (Gerrit Code Review): https://golang.org/cl/6231043 (コミットメッセージに記載されているGerritの変更リストへのリンク)