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

[インデックス 10414] ファイルの概要

本コミットは、Go言語のHTMLパーサーにおいて、HTMLの<caption>要素のパース処理を正確に実装するための変更です。これにより、<table>要素内で<caption>タグが適切に処理され、HTML5のパース仕様に準拠したDOMツリーが構築されるようになります。

コミット

commit 28546ed56a37c7d4a384c1e9ae69c61d16e4ea94
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Wed Nov 16 12:18:11 2011 +1100

    html: parse <caption> elements

    Pass tests2.dat, test 33:
    <!DOCTYPE html><table><caption>test TEST</caption><td>test

    | <!DOCTYPE html>
    | <html>
    |   <head>
    |   <body>
    |     <table>
    |       <caption>
    |         "test TEST"
    |       <tbody>
    |         <tr>
    |           <td>
    |             "test"

    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/5371099

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/28546ed56a37c7d4a384c1e9ae69c61d16e4ea94

元コミット内容

このコミットは、Go言語のHTMLパーサーが<caption>要素を正しくパースできるようにするものです。具体的には、tests2.datのテスト33(<!DOCTYPE html><table><caption>test TEST</caption><td>testというHTMLスニペット)が期待されるDOM構造(<caption><table>の子要素として適切に配置され、その中にテキストコンテンツが含まれる)を生成するように修正されています。

変更の背景

HTML5の仕様では、HTMLドキュメントのパース方法が厳密に定義されており、特にエラー回復や不完全なマークアップの処理について詳細なアルゴリズムが規定されています。<table>要素内の<caption>要素は、テーブルのタイトルや説明を提供する重要な要素ですが、そのパースルールは特定の「挿入モード(insertion mode)」と関連付けられています。

このコミット以前のGoのHTMLパーサーは、<caption>要素のパースに関して不完全な実装であったと考えられます。コミットメッセージにある// TODO: p.im = inCaptionIMというコメントは、この機能が未実装であったことを示唆しています。この未実装のため、<table>内に<caption>が存在する場合に、HTML5の仕様に沿った正しいDOMツリーが構築されず、テストケースが失敗していました。

この変更の目的は、HTML5のパースアルゴリズム、特に「in caption」挿入モード(Section 11.2.5.4.11.)を正確に実装し、<caption>要素がテーブル構造内で適切に処理されるようにすることです。これにより、GoのHTMLパーサーの堅牢性と標準準拠性が向上します。

前提知識の解説

HTML5のパースアルゴリズム

HTML5のパースアルゴリズムは、非常に複雑なステートマシンとして設計されています。これは、ブラウザがHTMLドキュメントをどのように読み込み、DOMツリーを構築するかを定義しています。主要な概念は以下の通りです。

  • トークナイゼーション(Tokenization): 入力ストリーム(HTML文字列)をトークン(開始タグ、終了タグ、テキスト、コメントなど)に変換するプロセス。
  • ツリー構築(Tree Construction): トークンストリームを受け取り、DOMツリーを構築するプロセス。このプロセスは「挿入モード」と呼ばれる状態に基づいて動作します。
  • 挿入モード(Insertion Mode): 現在パース中のHTMLのコンテキストに応じて、新しいトークンをどのように処理するかを決定する状態。例えば、<body>内では「in body」モード、<table>内では「in table」モードなどがあります。各モードには、特定のタグが来た場合の処理ルールが定義されています。
  • 要素スタック(Stack of Open Elements): 現在開いているHTML要素のスタック。新しい要素が追加されるとプッシュされ、要素が閉じられるとポップされます。このスタックは、現在のパースコンテキストを決定し、要素の親子関係を追跡するために使用されます。
  • アクティブフォーマット要素リスト(List of Active Formatting Elements): <b>, <i>, <a>などのフォーマット要素がネストされた場合に、それらの状態を追跡するために使用されるリスト。パース中に特定の条件でクリアされることがあります。
  • スコープ(Scope): 要素が特定のスコープ内にあるかどうかを判断するための概念。例えば、テーブル関連の要素は「table scope」内に存在する必要があります。

<caption>要素

<caption>要素は、HTMLの<table>要素の最初の子要素として使用され、テーブルのタイトルや説明を提供します。HTML5の仕様では、<caption><table>の直後にのみ出現でき、他のテーブル関連要素(<thead>, <tbody>, <tfoot>, <tr>など)よりも前にパースされる必要があります。

技術的詳細

このコミットの技術的詳細は、GoのHTMLパーサーにおけるHTML5パースアルゴリズムの「in table」モードと「in caption」モードの実装に焦点を当てています。

  1. resetInsertionMode関数の修正:

    • この関数は、パーサーが特定の要素(この場合は<caption>)を処理した後に、適切な挿入モードにリセットするために使用されます。
    • 以前は<caption>に対して// TODO: p.im = inCaptionIMとコメントアウトされていましたが、このコミットでp.im = inCaptionIMが有効化され、<caption>要素が検出された際にパーサーが「in caption」モードに移行するようになりました。
  2. inTableIM関数の修正:

    • inTableIMは、パーサーが「in table」挿入モードにあるときにトークンを処理するロジックを定義します。
    • StartTagToken"caption"である場合、以下の処理が追加されました。
      • p.clearStackToContext(tableScopeStopTags): 要素スタックをtableScopeStopTagstable, html, bodyなど、テーブルスコープを終了させるタグ)までクリアします。これにより、<caption><table>の直接の子として適切に配置されるように、不要な要素がスタックからポップされます。
      • p.afe = append(p.afe, &scopeMarker): アクティブフォーマット要素リストにスコープマーカーを追加します。これは、<caption>内のフォーマット要素が<caption>のスコープ外に影響を与えないようにするためのHTML5パースアルゴリズムの要件です。
      • p.addElement(p.tok.Data, p.tok.Attr): <caption>要素をDOMツリーに追加し、要素スタックにプッシュします。
      • p.im = inCaptionIM: パーサーの挿入モードを「in caption」に設定します。
      • return true: トークンが処理されたことを示します。
  3. inCaptionIM関数の新規追加:

    • この関数は、HTML5の仕様Section 11.2.5.4.11.「The "in caption" insertion mode」を実装しています。
    • StartTagTokenの処理:
      • "caption", "col", "colgroup", "tbody", "td", "tfoot", "thead", "tr"などのタグが「in caption」モードで開始タグとして現れた場合、これは<caption>要素が予期せず閉じられたことを意味します。
      • p.popUntil(tableScopeStopTags, "caption"): 要素スタックから<caption>要素をポップし、tableScopeStopTagsのいずれかの要素に到達するまでスタックをクリアします。これにより、<caption>が閉じられ、パーサーはテーブルコンテキストに戻ります。
      • p.clearActiveFormattingElements(): アクティブフォーマット要素リストをクリアします。
      • p.im = inTableIM: 挿入モードを「in table」に戻します。
      • return false: 現在のトークンを再処理する必要があることを示します(新しい挿入モードで)。
      • それ以外のタグは無視されます。
    • EndTagTokenの処理:
      • "caption"タグが終了タグとして現れた場合、p.popUntil(tableScopeStopTags, "caption")<caption>要素をスタックからポップし、p.clearActiveFormattingElements()でアクティブフォーマット要素リストをクリアし、p.im = inTableIMで挿入モードを「in table」に戻します。
      • "table"タグが終了タグとして現れた場合も同様に処理されますが、これは<caption>が閉じられていない状態で<table>が閉じられた場合の特殊なケースです。
      • その他の終了タグ("body", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr")は無視されます。
    • 上記以外のトークンは、inBodyIM(p)(「in body」モードの処理)にフォールバックされます。これは、<caption>内のコンテンツが通常のボディコンテンツとして扱われることを意味します。
  4. テストファイルの修正:

    • src/pkg/html/parse_test.goTestParser関数内で、tests2.datのテストケースのインデックスが33から34に更新されています。これは、おそらく新しいテストケースが追加されたか、既存のテストケースの順序が変更されたため、<caption>関連のテストが正しく参照されるように調整されたことを示しています。

これらの変更により、GoのHTMLパーサーはHTML5の複雑なパースルール、特に<table><caption>の相互作用を正確に処理できるようになり、より堅牢で標準準拠のDOMツリーを生成することが可能になりました。

コアとなるコードの変更箇所

src/pkg/html/parse.go

--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -298,7 +298,7 @@ func (p *parser) resetInsertionMode() {
 		case "tbody", "thead", "tfoot":
 			p.im = inTableBodyIM
 		case "caption":
-			// TODO: p.im = inCaptionIM
+			p.im = inCaptionIM
 		case "colgroup":
 			p.im = inColumnGroupIM
 		case "table":
@@ -887,6 +887,12 @@ func inTableIM(p *parser) bool {
 		// TODO.
 	case StartTagToken:
 		switch p.tok.Data {
+		case "caption":
+			p.clearStackToContext(tableScopeStopTags)
+			p.afe = append(p.afe, &scopeMarker)
+			p.addElement(p.tok.Data, p.tok.Attr)
+			p.im = inCaptionIM
+			return true
 		case "tbody", "tfoot", "thead":
 			p.clearStackToContext(tableScopeStopTags)
 			p.addElement(p.tok.Data, p.tok.Attr)
@@ -960,6 +966,46 @@ func (p *parser) clearStackToContext(stopTags []string) {
 	}
 }

+// Section 11.2.5.4.11.
+func inCaptionIM(p *parser) bool {
+	switch p.tok.Type {
+	case StartTagToken:
+		switch p.tok.Data {
+		case "caption", "col", "colgroup", "tbody", "td", "tfoot", "thead", "tr":
+			if p.popUntil(tableScopeStopTags, "caption") {
+				p.clearActiveFormattingElements()
+				p.im = inTableIM
+				return false
+			} else {
+				// Ignore the token.
+				return true
+			}
+		}
+	case EndTagToken:
+		switch p.tok.Data {
+		case "caption":
+			if p.popUntil(tableScopeStopTags, "caption") {
+				p.clearActiveFormattingElements()
+				p.im = inTableIM
+			}
+			return true
+		case "table":
+			if p.popUntil(tableScopeStopTags, "caption") {
+				p.clearActiveFormattingElements()
+				p.im = inTableIM
+				return false
+			} else {
+				// Ignore the token.
+				return true
+			}
+		case "body", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr":
+			// Ignore the token.
+			return true
+		}
+	}
+	return inBodyIM(p)
+}
+
 // Section 11.2.5.4.12.
 func inColumnGroupIM(p *parser) bool {
 	switch p.tok.Type {

src/pkg/html/parse_test.go

--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -134,7 +134,7 @@ func TestParser(t *testing.T) {
 	}{\n \t\t// TODO(nigeltao): Process all the test cases from all the .dat files.\n \t\t{\"tests1.dat\", -1},\n-\t\t{\"tests2.dat\", 33},\n+\t\t{\"tests2.dat\", 34},\n \t\t{\"tests3.dat\", 0},\n \t}\n \tfor _, tf := range testFiles {

コアとなるコードの解説

src/pkg/html/parse.go

  • resetInsertionModeの変更:

    • case "caption": p.im = inCaptionIM
      • この変更は、パーサーが<caption>要素を処理する際に、その後のトークンを「in caption」モードのルールに従って処理するように、挿入モードを明示的に設定します。以前はTODOコメントで示されていた未実装部分が解消されました。
  • inTableIMの変更:

    • case "caption":ブロックの追加
      • p.clearStackToContext(tableScopeStopTags): 現在の要素スタックを、<table>要素のスコープを終了させるタグ(例: <html>, <body>, <table>自身)までクリアします。これは、<caption><table>の直接の子として正しく挿入されるように、スタック上の不要な要素を削除する役割があります。
      • p.afe = append(p.afe, &scopeMarker): アクティブフォーマット要素リストにスコープマーカーを追加します。これは、<caption>要素の内部で適用されるフォーマット(例: <b>タグ)が、<caption>の外部に影響を与えないようにするためのHTML5パースアルゴリズムの規則です。
      • p.addElement(p.tok.Data, p.tok.Attr): <caption>タグに対応する要素をDOMツリーに追加し、同時にその要素を要素スタックにプッシュします。
      • p.im = inCaptionIM: 挿入モードを「in caption」に切り替えます。これにより、パーサーは<caption>要素の内部コンテンツのパースに特化したルールを適用するようになります。
      • return true: 現在のトークン(<caption>の開始タグ)が正常に処理されたことをパーサーに伝えます。
  • inCaptionIM関数の新規追加:

    • この関数は、HTML5の仕様で定義されている「in caption」挿入モードの動作を正確にモデル化しています。
    • StartTagTokenの処理:
      • "caption", "col", "colgroup", "tbody", "td", "tfoot", "thead", "tr"などの開始タグが「in caption」モードで現れた場合、これは<caption>要素が暗黙的に閉じられるべき状況を示します。
      • if p.popUntil(tableScopeStopTags, "caption"): 要素スタックから<caption>要素をポップし、tableScopeStopTagsのいずれかの要素に到達するまでスタックをクリアします。これにより、<caption>が閉じられ、パーサーはテーブルコンテキストに戻ります。
      • p.clearActiveFormattingElements(): アクティブフォーマット要素リストをクリアします。
      • p.im = inTableIM: 挿入モードを「in table」に戻します。
      • return false: 現在のトークンを新しい挿入モード(「in table」)で再処理する必要があることを示します。
      • else { return true }: popUntilが失敗した場合(つまり、<caption>がスタックに見つからなかった場合など)、トークンは無視されます。
    • EndTagTokenの処理:
      • "caption"の終了タグが来た場合、popUntil<caption>をスタックからポップし、アクティブフォーマット要素をクリアし、挿入モードを「in table」に戻します。これは<caption>の正常な終了処理です。
      • "table"の終了タグが来た場合も同様に処理されます。これは、<caption>が明示的に閉じられていない状態で<table>が閉じられた場合に、<caption>を暗黙的に閉じるためのエラー回復メカニズムです。
      • その他の終了タグ("body", "col", colgroup, html, tbody, td, tfoot, th, thead, tr)は、<caption>の内部では無視されます。
    • return inBodyIM(p): 上記の特定のタグに該当しないトークンは、一般的な「in body」モードのルールに従って処理されます。これは、<caption>内のテキストコンテンツなどが通常のボディコンテンツとしてパースされることを意味します。

src/pkg/html/parse_test.go

  • {"tests2.dat", 33}から{"tests2.dat", 34}への変更は、tests2.datファイル内のテストケースのインデックスが更新されたことを示しています。これは、新しいテストケースが追加されたか、既存のテストケースの順序が変更された結果として、<caption>関連のテストが正しく参照されるように調整されたものです。

これらの変更により、GoのHTMLパーサーはHTML5の仕様に厳密に準拠し、<caption>要素を含むHTMLドキュメントをより正確にパースできるようになりました。

関連リンク

参考にした情報源リンク

  • HTML5仕様書 (WHATWG): 特に「Parsing HTML documents」セクション。
  • MDN Web Docs: <caption>要素に関する情報。
  • Go言語の公式ドキュメントおよび関連するx/net/htmlパッケージのソースコード。
  • コミットメッセージ内のGo CLリンク: https://golang.org/cl/5371099 (これはGoのGerritコードレビューシステムへのリンクであり、詳細な議論や変更履歴が含まれている可能性がありますが、直接アクセスは試みていません。)