[インデックス 12919] ファイルの概要
このコミットは、Go言語の実験的なHTMLパーサーパッケージ exp/html
内の parse.go
ファイルにおける afterHeadIM
関数を改善するものです。具体的には、HTMLドキュメントの <head>
要素の解析が完了した後のパーサーの挙動を修正し、制御フローを整理しています。
コミット
commit 7d63ff09a5ce65c91021acaf79b1d281cba55f07
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Fri Apr 20 10:48:10 2012 +1000
exp/html: improve afterHeadIM
Clean up the flow of control.
Fix the TODO for handling <html> tags.
Add a case to ignore doctype declarations.
Pass one additional test.
R=nigeltao
CC=golang-dev
https://golang.org/cl/6072047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7d63ff09a5ce65c91021acaf79b1d281cba55f07
元コミット内容
exp/html: improve afterHeadIM
- 制御フローを整理する。
<html>
タグの処理に関するTODOを修正する。- DOCTYPE宣言を無視するケースを追加する。
- 追加で1つのテストがパスするようになる。
変更の背景
このコミットは、Go言語の標準ライブラリの一部となることを目指していた実験的なHTMLパーサー (exp/html
パッケージ) の改善の一環として行われました。HTMLのパースは非常に複雑であり、W3Cによって詳細なパースアルゴリズムが定義されています。このアルゴリズムは、パーサーが現在どの「挿入モード (insertion mode)」にあるかに基づいて、受け取ったトークン(タグ、テキストなど)をどのように処理するかを決定するステートマシンとして機能します。
afterHeadIM
関数は、パーサーがHTMLドキュメントの <head>
要素の解析を終え、次の要素を期待している状態(挿入モード)を処理します。このモードでは、通常は <body>
タグや <html>
タグ、あるいは特定のメタデータタグなどが期待されます。
以前の実装では、この afterHeadIM
モードにおける制御フローが複雑で、特に <html>
タグの再出現やDOCTYPE宣言の扱いに関して、HTML5のパース仕様に完全に準拠していない部分や、TODOコメントとして残されていた未実装の挙動がありました。このコミットは、これらの問題を解決し、パーサーの堅牢性と仕様への準拠度を高めることを目的としています。特に、テストケース <!doctype html><html a=b><head></head><html c=d>
が以前は失敗していたことから、afterHeadIM
モードで <html>
タグが再度出現した場合の処理に問題があったことが示唆されます。
前提知識の解説
HTML5パースアルゴリズムと挿入モード
HTML5の仕様では、ウェブブラウザがHTMLドキュメントをどのように解析し、DOMツリーを構築するかについて、非常に詳細なアルゴリズムが定義されています。このアルゴリズムは、トークナイザーとツリー構築器の2つの主要なフェーズに分かれています。
- トークナイザー: 入力ストリーム(HTML文字列)を読み込み、個々のトークン(開始タグ、終了タグ、テキスト、コメント、DOCTYPEなど)に分解します。
- ツリー構築器: トークナイザーから受け取ったトークンに基づいて、DOMツリーを構築します。ツリー構築器は、現在の「挿入モード (insertion mode)」と呼ばれる状態に基づいて、各トークンをどのように処理するかを決定します。
挿入モードは、HTMLドキュメントのどの部分を解析しているかに応じてパーサーの挙動を変化させるための状態です。例えば、<head>
タグの中では特定のタグ(<meta>
, <link>
, <title>
など)のみが有効であり、それ以外のタグが出現した場合はエラーとして扱われたり、暗黙的に <head>
を閉じたりするなどの挙動が定義されています。
本コミットで変更される afterHeadIM
は、ツリー構築器の挿入モードの一つで、<head>
要素が閉じられた直後の状態を指します。このモードでは、通常は <body>
要素の開始が期待されますが、それ以外の様々なトークン(テキスト、コメント、<html>
タグの再出現など)も適切に処理する必要があります。
Go言語の exp/html
パッケージ
exp/html
は、Go言語の標準ライブラリ html
パッケージの前身となる実験的なパッケージでした。このパッケージは、HTML5のパースアルゴリズムをGoで実装することを目的としていました。HTMLのパースは、ブラウザの互換性を確保するために非常に厳密な仕様に準拠する必要があり、このパッケージはその複雑なロジックをGoで実現しようとしていました。
parser
構造体と状態管理
HTMLパーサーは、現在の状態(挿入モード、DOMツリーの現在のノード、エラーフラグなど)を管理するための構造体(この場合は parser
)を持ちます。このコミットの変更点から、以前はローカル変数として管理されていた add
, attr
, framesetOK
, implied
といったフラグが、parser
構造体のフィールドとして一元的に管理されるようになったことが示唆されます。これにより、制御フローがより明確になり、状態の引き渡しが容易になります。
framesetOK
:<frameset>
タグが許可されるかどうかを示すフラグ。im
(insertion mode): 現在の挿入モードを示す関数ポインタまたは列挙型。
技術的詳細
このコミットの主要な変更は、afterHeadIM
関数の内部ロジックの簡素化と正確性の向上にあります。
-
ローカル変数の削除とパーサー状態への移行: 以前の
afterHeadIM
関数では、add
,attr
,framesetOK
,implied
といった複数のブール型ローカル変数が宣言され、トークンの種類に応じてこれらの変数を設定し、関数の最後でこれらの変数に基づいて<body>
タグの暗黙的な生成やパーサーの状態遷移を行っていました。 このコミットでは、これらのローカル変数が削除され、代わりにparser
構造体 (p
) のフィールド (p.framesetOK
,p.im
) を直接操作するように変更されています。これにより、関数の内部状態管理が簡素化され、パーサー全体の状態管理が一貫したものになります。 -
<html>
タグの処理の修正: 以前はcase "html": // TODO.
となっていた部分がreturn inBodyIM(p)
に変更されました。これは、afterHeadIM
モードで<html>
タグが検出された場合、パーサーは直ちにinBodyIM
(body要素内挿入モード) に遷移すべきであるというHTML5パース仕様に準拠するための修正です。これにより、ネストされた<html>
タグや、誤って<head>
の後に<html>
が出現した場合でも、パーサーが適切にDOMツリー構築を継続できるようになります。 -
<body>
タグの処理の改善:StartTagToken
でbody
が検出された場合、以前はローカル変数add
,attr
,framesetOK
を設定していましたが、変更後は直接p.addElement("body", p.tok.Attr)
で<body>
要素を追加し、p.framesetOK = false
を設定し、p.im = inBodyIM
で挿入モードをinBodyIM
に変更し、return true
で処理を完了しています。これにより、<body>
タグの明示的な開始がより直接的に処理されるようになりました。 -
DOCTYPE宣言の無視:
case DoctypeToken:
が新たに追加され、// Ignore the token. return true
となっています。afterHeadIM
モードでDOCTYPE宣言が検出された場合、それは無視されるべきであるという仕様に準拠しています。これは、HTMLドキュメントの先頭以外でDOCTYPE宣言が出現した場合の堅牢性を高めます。 -
暗黙的な
<body>
タグの生成ロジックの整理: 関数の最後にある、暗黙的な<body>
タグを生成するロジックが大幅に簡素化されました。 以前はif add || implied { ... }
という条件分岐がありましたが、変更後はp.parseImpliedToken(StartTagToken, "body", nil)
とp.framesetOK = true
、そしてreturn false
に置き換えられています。 これは、afterHeadIM
モードで特定のタグが処理されなかった場合(例えば、テキストノードや認識されない開始/終了タグなど)、HTML5のパース仕様では暗黙的に<body>
要素が生成され、パーサーがinBodyIM
に遷移するというルールがあるためです。p.parseImpliedToken
はこの暗黙的な要素生成とモード遷移をカプセル化したヘルパー関数であると考えられます。return false
は、現在のトークンが新しいモードで再処理される必要があることを示唆しています。
これらの変更により、afterHeadIM
関数の制御フローがより線形になり、ローカル変数の状態管理の複雑さが解消され、HTML5パース仕様への準拠が向上しています。
コアとなるコードの変更箇所
src/pkg/exp/html/parse.go
ファイルの afterHeadIM
関数が主な変更箇所です。
--- a/src/pkg/exp/html/parse.go
+++ b/src/pkg/exp/html/parse.go
@@ -539,16 +539,7 @@ func inHeadIM(p *parser) bool {
// Section 12.2.5.4.6.
func afterHeadIM(p *parser) bool {
-\tvar (\n-\t\tadd bool\n-\t\tattr []Attribute\n-\t\tframesetOK bool\n-\t\timplied bool\n-\t)\n \tswitch p.tok.Type {
-\tcase ErrorToken:\n-\t\timplied = true\n-\t\tframesetOK = true
\tcase TextToken:\n \t\ts := strings.TrimLeft(p.tok.Data, whitespace)\n \t\tif len(s) < len(p.tok.Data) {\n@@ -559,16 +550,15 @@ func afterHeadIM(p *parser) bool {
\t\t\t}\n \t\t\tp.tok.Data = s\n \t\t}\n-\t\timplied = true\n-\t\tframesetOK = true
\tcase StartTagToken:\n \t\tswitch p.tok.Data {\n \t\tcase "html":
-\t\t\t// TODO.
+\t\t\treturn inBodyIM(p)
\t\tcase "body":
-\t\t\tadd = true
-\t\t\tattr = p.tok.Attr
-\t\t\tframesetOK = false
+\t\t\tp.addElement(p.tok.Data, p.tok.Attr)
+\t\t\tp.framesetOK = false
+\t\t\tp.im = inBodyIM
+\t\t\treturn true
\t\tcase "frameset":
\t\t\tp.addElement(p.tok.Data, p.tok.Attr)\n \t\t\tp.im = inFramesetIM
@@ -580,15 +570,11 @@ func afterHeadIM(p *parser) bool {
\t\tcase "head":
\t\t\t// Ignore the token.\n \t\t\treturn true
-\t\tdefault:\n-\t\t\timplied = true\n-\t\t\tframesetOK = true
\t\t}\n \tcase EndTagToken:\n \t\tswitch p.tok.Data {\n \t\tcase "body", "html", "br":
-\t\t\timplied = true
-\t\t\tframesetOK = true
+\t\t\t// Drop down to creating an implied <body> tag.
\t\tdefault:\n \t\t\t// Ignore the token.\n \t\t\treturn true
@@ -599,13 +585,14 @@ func afterHeadIM(p *parser) bool {\n \t\t\tData: p.tok.Data,\n \t\t})\n \t\treturn true
+\tcase DoctypeToken:\n+\t\t// Ignore the token.\n+\t\treturn true
\t}\n-\tif add || implied {\n-\t\tp.addElement("body", attr)\n-\t\tp.framesetOK = framesetOK\n-\t}\n-\tp.im = inBodyIM
-\treturn !implied
+\n+\tp.parseImpliedToken(StartTagToken, "body", nil)
+\tp.framesetOK = true
+\treturn false
}\n
// copyAttributes copies attributes of src not found on dst to dst.
また、テストログファイル src/pkg/exp/html/testlogs/tests19.dat.log
も変更されています。
--- a/src/pkg/exp/html/testlogs/tests19.dat.log
+++ b/src/pkg/exp/html/testlogs/tests19.dat.log
@@ -85,7 +85,7 @@ PASS "<!doctype html><math></html>"
PASS "<!doctype html><meta charset=\"ascii\">"
FAIL "<!doctype html><meta http-equiv=\"content-type\" content=\"text/html;charset=ascii\">"
PASS "<!doctype html><head><!--aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa--><meta charset=\"utf8\">"
-FAIL "<!doctype html><html a=b><head></head><html c=d>"
+PASS "<!doctype html><html a=b><head></head><html c=d>"
PASS "<!doctype html><image/>"
PASS "<!doctype html>a<i>b<table>c<b>d</i>e</b>f"
PASS "<!doctype html><table><i>a<b>b<div>c<a>d</i>e</b>f"
コアとなるコードの解説
afterHeadIM
関数は、HTMLパーサーのツリー構築器が <head>
要素の処理を終えた後の状態を管理する関数です。この関数は、トークナイザーから受け取った次のトークン (p.tok
) の種類に基づいて、適切な処理を行います。
変更前は、add
, attr
, framesetOK
, implied
といったローカル変数を介して状態を管理し、関数の最後にまとめて <body>
要素の暗黙的な生成やモード遷移を行っていました。この方式は、各 case
ブロックでこれらの変数を設定する必要があり、制御フローが複雑になりがちでした。
変更後は、これらのローカル変数を廃止し、parser
構造体 p
のフィールドを直接更新するようにしました。
-
StartTagToken
のhtml
ケース:return inBodyIM(p)
に変更されたことで、afterHeadIM
モードで<html>
開始タグが検出された場合、パーサーは直ちにinBodyIM
モードに遷移し、そこでこの<html>
タグを処理するようになります。これは、HTML5のパース仕様において、<html>
タグはドキュメントのルート要素であり、<head>
の後に再度出現しても、それは<body>
の開始を意味すると解釈されるためです。 -
StartTagToken
のbody
ケース:p.addElement(p.tok.Data, p.tok.Attr)
で<body>
要素をDOMツリーに追加し、p.framesetOK = false
で<frameset>
の許可フラグをオフにし、p.im = inBodyIM
で挿入モードをinBodyIM
に設定しています。そしてreturn true
で、現在のトークンが完全に処理され、次のトークンに進むことを示します。これにより、明示的な<body>
タグの処理がより直接的になりました。 -
DoctypeToken
の追加:case DoctypeToken:
が追加され、// Ignore the token. return true
となっています。これは、HTML5のパース仕様で、afterHeadIM
モードでDOCTYPEトークンが検出された場合は無視されるべきであるというルールに準拠しています。 -
暗黙的な
<body>
生成ロジックの簡素化: 関数の最後にある、どのswitch
ケースにもマッチしなかった場合のフォールバックロジックがp.parseImpliedToken(StartTagToken, "body", nil)
とp.framesetOK = true
、そしてreturn false
に変更されました。p.parseImpliedToken
は、指定されたタグ(ここでは<body>
)を暗黙的に生成し、適切な挿入モードに遷移させるためのヘルパー関数です。return false
は、現在のトークンが新しい挿入モード(inBodyIM
)で再処理される必要があることを示します。これにより、テキストノードやその他の予期せぬタグが<head>
の直後に出現した場合でも、パーサーが自動的に<body>
を生成して処理を継続できるようになります。
これらの変更により、afterHeadIM
関数のロジックはよりHTML5のパース仕様に忠実になり、コードの可読性と保守性が向上しています。特に、<!doctype html><html a=b><head></head><html c=d>
のような、<head>
の後に <html>
が再度出現するエッジケースが正しく処理されるようになったことが、テストログの変更から確認できます。
関連リンク
- HTML Living Standard - 13.2.6.4.6 The "after head" insertion mode: https://html.spec.whatwg.org/multipage/parsing.html#the-after-head-insertion-mode
- Go言語の
html
パッケージ (このexp/html
の後継): https://pkg.go.dev/golang.org/x/net/html
参考にした情報源リンク
- HTML Living Standard (W3C勧告): HTML5のパースアルゴリズムに関する詳細な仕様。
- Go言語の
html
パッケージのドキュメント:exp/html
の後継である現在のhtml
パッケージの挙動を理解する上で参考になります。 - Go言語のGerritコードレビューシステム: コミットメッセージに記載されている
https://golang.org/cl/6072047
は、この変更がGerrit上でレビューされた際のChange-IDです。 - Go言語のソースコードリポジトリ: 実際のコード変更を詳細に確認しました。