Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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つの主要なフェーズに分かれています。

  1. トークナイザー: 入力ストリーム(HTML文字列)を読み込み、個々のトークン(開始タグ、終了タグ、テキスト、コメント、DOCTYPEなど)に分解します。
  2. ツリー構築器: トークナイザーから受け取ったトークンに基づいて、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 関数の内部ロジックの簡素化と正確性の向上にあります。

  1. ローカル変数の削除とパーサー状態への移行: 以前の afterHeadIM 関数では、add, attr, framesetOK, implied といった複数のブール型ローカル変数が宣言され、トークンの種類に応じてこれらの変数を設定し、関数の最後でこれらの変数に基づいて <body> タグの暗黙的な生成やパーサーの状態遷移を行っていました。 このコミットでは、これらのローカル変数が削除され、代わりに parser 構造体 (p) のフィールド (p.framesetOK, p.im) を直接操作するように変更されています。これにより、関数の内部状態管理が簡素化され、パーサー全体の状態管理が一貫したものになります。

  2. <html> タグの処理の修正: 以前は case "html": // TODO. となっていた部分が return inBodyIM(p) に変更されました。これは、afterHeadIM モードで <html> タグが検出された場合、パーサーは直ちに inBodyIM (body要素内挿入モード) に遷移すべきであるというHTML5パース仕様に準拠するための修正です。これにより、ネストされた <html> タグや、誤って <head> の後に <html> が出現した場合でも、パーサーが適切にDOMツリー構築を継続できるようになります。

  3. <body> タグの処理の改善: StartTagTokenbody が検出された場合、以前はローカル変数 add, attr, framesetOK を設定していましたが、変更後は直接 p.addElement("body", p.tok.Attr)<body> 要素を追加し、p.framesetOK = false を設定し、p.im = inBodyIM で挿入モードを inBodyIM に変更し、return true で処理を完了しています。これにより、<body> タグの明示的な開始がより直接的に処理されるようになりました。

  4. DOCTYPE宣言の無視: case DoctypeToken: が新たに追加され、// Ignore the token. return true となっています。afterHeadIM モードでDOCTYPE宣言が検出された場合、それは無視されるべきであるという仕様に準拠しています。これは、HTMLドキュメントの先頭以外でDOCTYPE宣言が出現した場合の堅牢性を高めます。

  5. 暗黙的な <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 のフィールドを直接更新するようにしました。

  • StartTagTokenhtml ケース: return inBodyIM(p) に変更されたことで、afterHeadIM モードで <html> 開始タグが検出された場合、パーサーは直ちに inBodyIM モードに遷移し、そこでこの <html> タグを処理するようになります。これは、HTML5のパース仕様において、<html> タグはドキュメントのルート要素であり、<head> の後に再度出現しても、それは <body> の開始を意味すると解釈されるためです。

  • StartTagTokenbody ケース: 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 (W3C勧告): HTML5のパースアルゴリズムに関する詳細な仕様。
  • Go言語の html パッケージのドキュメント: exp/html の後継である現在の html パッケージの挙動を理解する上で参考になります。
  • Go言語のGerritコードレビューシステム: コミットメッセージに記載されている https://golang.org/cl/6072047 は、この変更がGerrit上でレビューされた際のChange-IDです。
  • Go言語のソースコードリポジトリ: 実際のコード変更を詳細に確認しました。