[インデックス 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仕様には、以下のような多数の挿入モードが定義されています。
initial
before html
before head
in head
in head noscript
after head
in body
text
in table
in table text
in caption
in column group
in table body
in row
in cell
in select
in select in table
in template
after body
(本コミットで関連)in frameset
after frameset
after 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.log
src/pkg/exp/html/testlogs/tests19.dat.log
src/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/html
package documentation (古い情報である可能性あり): https://pkg.go.dev/exp/html (現在はgolang.org/x/net/html
に統合されている可能性が高いです) - Go
golang.org/x/net/html
package: 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の変更リストへのリンク)