[インデックス 13107] ファイルの概要
このコミットは、Go言語の実験的なHTMLパーサーライブラリ exp/html
における、HTML5仕様に準拠するための修正です。具体的には、HTMLパーシングアルゴリズムの「in cell」挿入モード(inCellIM
)の挙動を調整し、テーブル要素の処理におけるフロー制御を改善しています。これにより、特定の終了タグ(</table>
, </tbody>
, </tfoot>
, </thead>
, </tr>
)がテーブルスコープ内に適切な要素がない場合に無視されるようになり、さらに3つのテストケースがパスするようになりました。
コミット
commit 7648f61c7d7d1cd05a507086821133fae61c37af
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Tue May 22 10:31:08 2012 +1000
exp/html: adjust inCellIM to match spec
Clean up flow of control.
Ignore </table>, </tbody>, </tfoot>, </thead>, </tr> if there is not
an appropriate element in table scope.
Pass 3 more tests.
R=golang-dev, nigeltao
CC=golang-dev
https://golang.org/cl/6206093
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7648f61c7d7d1cd05a507086821133fae61c37af
元コミット内容
このコミットは、exp/html
パッケージの parse.go
ファイルにおける inCellIM
関数(in cell insertion mode)のロジックを修正するものです。主な変更点は以下の通りです。
inCellIM
関数内のフロー制御の整理。</table>
,</tbody>
,</tfoot>
,</thead>
,</tr>
といった終了タグが、テーブルスコープ内に対応する要素が存在しない場合に無視されるように修正。- これにより、
webkit02.dat.log
に記録されている3つのテストケースが新たにパスするようになった。
変更の背景
HTMLのパースは非常に複雑なプロセスであり、特にエラーのあるHTML(ブラウザが許容するような不完全なマークアップ)を正確に処理するためには、W3Cによって定義されたHTML5のパースアルゴリズムに厳密に準拠する必要があります。このアルゴリズムは、トークン化された入力に基づいてパーサーの状態(挿入モード)を遷移させ、DOMツリーを構築します。
exp/html
パッケージは、Go言語でHTML5のパースアルゴリズムを実装するための実験的なライブラリでした。このコミットが行われた2012年当時、HTML5の仕様はまだ進化途上にあり、実装もそれに合わせて調整が必要でした。
「in cell」挿入モードは、パーサーが <td>
または <th>
要素の内部にいるときに適用されるモードです。このモードでは、特定のタグが検出された場合に、現在のセルを暗黙的に閉じ、パーサーの状態を「in row」挿入モード(inRowIM
)に遷移させる必要があります。また、仕様には、特定の終了タグがテーブルスコープ内に適切な開始タグがない場合に無視するというルールも存在します。
このコミットの背景には、これらのHTML5パース仕様の細部、特にテーブル関連要素の処理に関するルールへの準拠を強化し、より堅牢で正確なHTMLパーサーを構築するという目的があります。既存の実装がこれらの仕様に完全に合致していなかったため、テストが失敗していたと考えられます。
前提知識の解説
HTML5パースアルゴリズム
HTML5のパースアルゴリズムは、ブラウザがHTMLドキュメントをどのように解析し、DOMツリーを構築するかを詳細に定義したものです。これは、トークン化フェーズとツリー構築フェーズの2つの主要なフェーズから構成されます。
- トークン化フェーズ: 入力ストリーム(HTML文字列)を読み込み、個々のトークン(開始タグ、終了タグ、テキスト、コメントなど)に分解します。
- ツリー構築フェーズ: トークナイザーから受け取ったトークンに基づいて、DOMツリーを構築します。このフェーズは、パーサーの状態を管理する「挿入モード(Insertion Mode)」によって制御されます。
挿入モード(Insertion Mode)
挿入モードは、ツリー構築アルゴリズムの現在の状態を定義します。HTML5の仕様には多数の挿入モードが定義されており、それぞれが特定のトークンが検出されたときにDOMツリーにどのように影響するかを決定する独自のルールセットを持っています。パーサーは、現在のトークンと現在の挿入モードに基づいて、次の挿入モードに遷移したり、要素をスタックにプッシュ/ポップしたり、DOMツリーにノードを追加したりします。
このコミットで関連するのは以下の挿入モードです。
- in cell insertion mode (
inCellIM
):<td>
または<th>
要素の内部にいるときに適用されます。 - in row insertion mode (
inRowIM
):<tr>
要素の内部にいるときに適用されます。
テーブルスコープ(Table Scope)
HTML5パースアルゴリズムにおける「スコープ」は、特定の要素がDOMツリーのどこまで影響を及ぼすか、またはどこまで遡って要素を探すかを示す概念です。特にテーブル関連の要素では、「テーブルスコープ」という概念が重要になります。
- 要素が特定のスコープ内にあるかどうかのチェック: パーサーは、スタック上の要素をルート要素まで遡り、特定の要素(例:
<td>
,<th>
,<table>
)が存在するかどうかを確認します。 - テーブルスコープ:
<table>
,<tbody>
,<tfoot>
,<thead>
,<tr>
,<td>
,<th>
などのテーブル関連要素が、特定のルールに基づいてスタック上に存在するかどうかを判断するために使用されます。
暗黙的なセル閉じ(Implicit Cell Closing)
HTMLでは、一部の要素は明示的な終了タグがなくても、特定の開始タグや終了タグが検出されたときに自動的に閉じられる(暗黙的に閉じられる)ことがあります。例えば、<td>
要素の内部で別の <td>
が開始された場合、最初の <td>
は自動的に閉じられます。このコミットは、この暗黙的なセル閉じのルールを inCellIM
で正確に適用することに関わっています。
アクティブフォーマット要素(Active Formatting Elements)
HTMLパーサーは、<b>
, <i>
, <a>
などのフォーマット要素の開始タグを検出すると、それらを「アクティブフォーマット要素のリスト」に追加します。これは、ネストされたフォーマット要素が正しく処理されるようにするために使用されます。要素が閉じられたり、特定の状況でパーサーの状態がリセットされたりすると、このリストはクリアされる必要があります。clearActiveFormattingElements()
はこのリストをクリアする操作です。
技術的詳細
このコミットは、src/pkg/exp/html/parse.go
内の inCellIM
関数を、HTML5仕様の「in cell insertion mode」のルールに厳密に合わせるための変更です。
inCellIM
の主要な変更点
-
フロー制御の簡素化:
- 以前は
closeTheCellAndReprocess
というブール変数を使用して、セルの暗黙的な閉じと再処理が必要かどうかをフラグで管理していました。 - 今回の変更では、この変数を削除し、条件が満たされた場合に直接
p.popUntil(...)
,p.clearActiveFormattingElements()
,p.im = inRowIM
を呼び出すようにロジックをインライン化しました。これにより、コードの可読性と直接性が向上しています。
- 以前は
-
StartTagToken
の処理 (caption
,col
,colgroup
,tbody
,td
,tfoot
,th
,thead
,tr
):- これらのタグが
inCellIM
で検出された場合、仕様では現在のセル(<td>
または<th>
)を閉じ、パーサーをinRowIM
に遷移させる必要があります。 - 変更前は
// TODO: check for "td" or "th" in table scope.
とコメントがあり、closeTheCellAndReprocess = true
が設定されていました。 - 変更後、
p.popUntil(tableScope, "td", "th")
を呼び出して、テーブルスコープ内で最も近い<td>
または<th>
をポップ(閉じ)ます。この操作が成功した場合(つまり、セルが閉じられた場合)、p.clearActiveFormattingElements()
でアクティブフォーマット要素のリストをクリアし、p.im = inRowIM
で挿入モードをinRowIM
に変更し、return false
で現在のトークンを再処理するように指示します。 p.popUntil
がfalse
を返した場合(つまり、テーブルスコープ内に<td>
または<th>
が見つからなかった場合)、これは不正なマークアップであり、仕様に従ってトークンを無視しreturn true
します。
- これらのタグが
-
EndTagToken
の処理 (body
,caption
,col
,colgroup
,html
):- これらの終了タグが
inCellIM
で検出された場合、仕様では単にトークンを無視するように指示されています。 - 変更前は
// TODO.
とコメントされていました。 - 変更後、
// Ignore the token.
とコメントが追加され、return true
でトークンを無視するようになりました。
- これらの終了タグが
-
EndTagToken
の処理 (table
,tbody
,tfoot
,thead
,tr
):- これらの終了タグが
inCellIM
で検出された場合、仕様には2つの主要なルールがあります。- もし、対応する要素がテーブルスコープ内に存在しない場合、トークンを無視します。
- 対応する要素がテーブルスコープ内に存在する場合、現在のセルを閉じ、パーサーを
inRowIM
に遷移させ、現在のトークンを再処理します。
- 変更前は
// TODO: check for matching element in table scope.
とコメントがあり、closeTheCellAndReprocess = true
が設定されていました。 - 変更後、まず
!p.elementInScope(tableScope, p.tok.Data)
をチェックします。これは、現在の終了タグに対応する開始タグがテーブルスコープ内に存在しないかどうかを確認します。存在しない場合、トークンを無視しreturn true
します。 - 対応する要素がテーブルスコープ内に存在する場合、
p.popUntil(tableScope, "td", "th")
を呼び出してセルを閉じ、p.clearActiveFormattingElements()
でアクティブフォーマット要素をクリアし、p.im = inRowIM
で挿入モードをinRowIM
に変更し、return false
で現在のトークンを再処理するように指示します。
- これらの終了タグが
-
CommentToken
処理の削除:- 以前は
CommentToken
を検出した場合にCommentNode
を追加するロジックがありましたが、これはinCellIM
の仕様とは直接関係なく、より一般的なツリー構築ロジックで処理されるべきものです。このコミットで削除されました。
- 以前は
これらの変更により、inCellIM
はHTML5のパース仕様に厳密に準拠するようになり、特にテーブル構造の不正なネストや終了タグの欠落に対するブラウザの挙動をより正確にエミュレートできるようになりました。
コアとなるコードの変更箇所
src/pkg/exp/html/parse.go
ファイルの inCellIM
関数が主な変更箇所です。
--- a/src/pkg/exp/html/parse.go
+++ b/src/pkg/exp/html/parse.go
@@ -1451,15 +1451,18 @@ func inRowIM(p *parser) bool {
// Section 12.2.5.4.15.
func inCellIM(p *parser) bool {
- var (
- closeTheCellAndReprocess bool
- )
switch p.tok.Type {
case StartTagToken:
switch p.tok.Data {
case "caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr":
- // TODO: check for "td" or "th" in table scope.
- closeTheCellAndReprocess = true
+ if p.popUntil(tableScope, "td", "th") {
+ // Close the cell and reprocess.
+ p.clearActiveFormattingElements()
+ p.im = inRowIM
+ return false
+ }
+ // Ignore the token.
+ return true
case "select":
p.reconstructActiveFormattingElements()
p.addElement(p.tok.Data, p.tok.Attr)
@@ -1478,20 +1481,15 @@ func inCellIM(p *parser) bool {
p.im = inRowIM
return true
case "body", "caption", "col", "colgroup", "html":
- // TODO.
+ // Ignore the token.
+ return true
case "table", "tbody", "tfoot", "thead", "tr":
- // TODO: check for matching element in table scope.
- closeTheCellAndReprocess = true
- }
- case CommentToken:
- p.addChild(&Node{
- Type: CommentNode,
- Data: p.tok.Data,
- })
- return true
- }
- if closeTheCellAndReprocess {
- if p.popUntil(tableScope, "td") || p.popUntil(tableScope, "th") {
+ if !p.elementInScope(tableScope, p.tok.Data) {
+ // Ignore the token.
+ return true
+ }
+ // Close the cell and reprocess.
+ p.popUntil(tableScope, "td", "th")
p.clearActiveFormattingElements()
p.im = inRowIM
return false
また、src/pkg/exp/html/testlogs/webkit02.dat.log
ファイルも更新され、以下のテストケースが FAIL
から PASS
に変更されました。
--- a/src/pkg/exp/html/testlogs/webkit02.dat.log
+++ b/src/pkg/exp/html/testlogs/webkit02.dat.log
@@ -3,7 +3,7 @@ PASS "<p id=\"status\"><noscript><strong>A</strong></noscript><span>B</span></p>
PASS "<div><sarcasm><div></div></sarcasm></div>"
FAIL "<html><body><img src=\"\" border=\"0\" alt=\"><div>A</div></body></html>"
PASS "<table><td></tbody>A"
-FAIL "<table><td></thead>A"
-FAIL "<table><td></tfoot>A"
-FAIL "<table><thead><td></tbody>A"
+PASS "<table><td></thead>A"
+PASS "<table><td></tfoot>A"
+PASS "<table><thead><td></tbody>A"
PASS "<legend>test</legend>"
コアとなるコードの解説
inCellIM
関数は、HTMLパーサーが <td>
または <th>
要素の内部にいるときに呼び出される挿入モードハンドラです。この関数は、現在のトークン(p.tok
)のタイプとデータに基づいて、DOMツリーの構築とパーサーの状態遷移を制御します。
StartTagToken
の処理
-
case "caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr":
- これらの開始タグがセル内で検出された場合、HTML5仕様では現在のセルを暗黙的に閉じる必要があります。
p.popUntil(tableScope, "td", "th")
は、スタックを遡ってテーブルスコープ内で最も近い<td>
または<th>
要素を見つけ、それらをスタックからポップ(閉じ)ます。- もし
popUntil
がtrue
を返した場合(つまり、セルが正常に閉じられた場合)、以下の処理が行われます。p.clearActiveFormattingElements()
: アクティブフォーマット要素のリストをクリアします。これは、セルの閉じに伴うコンテキストのリセットに必要な処理です。p.im = inRowIM
: 挿入モードをinRowIM
(in row insertion mode)に遷移させます。これは、セルが閉じられた後、パーサーがテーブルの行のコンテキストに戻ることを意味します。return false
: 現在のトークンを再処理するようにパーサーに指示します。これにより、閉じられたセルの後に検出された新しい開始タグが、新しい挿入モード(inRowIM
)で適切に処理されます。
- もし
popUntil
がfalse
を返した場合(テーブルスコープ内に<td>
または<th>
が見つからなかった場合)、これは不正なマークアップであり、仕様に従ってトークンは無視されreturn true
します。
-
case "select":
select
タグは特殊なケースで、アクティブフォーマット要素の再構築を行い、要素をDOMに追加します。これはこのコミットの主要な変更点ではありませんが、inCellIM
の一部として存在します。
EndTagToken
の処理
-
case "body", "caption", "col", "colgroup", "html":
- これらの終了タグが
inCellIM
で検出された場合、HTML5仕様では単にトークンを無視するように指示されています。return true
でトークンを消費し、何も処理しないことを示します。
- これらの終了タグが
-
case "table", "tbody", "tfoot", "thead", "tr":
- これらの終了タグが
inCellIM
で検出された場合、パーサーはまず!p.elementInScope(tableScope, p.tok.Data)
をチェックします。p.elementInScope(tableScope, p.tok.Data)
は、現在の終了タグ(例:</table>
)に対応する開始タグがテーブルスコープ内に存在するかどうかを確認します。- もし
!
が付いているため、対応する要素がスコープ内に存在しない場合、トークンは無視されreturn true
します。これは、例えば<td></table>
のように、<table>
が<td>
の内部で不適切に閉じられようとしているが、対応する<table>
がスタック上にない場合に適用されます。
- 対応する要素がスコープ内に存在する場合、以下の処理が行われます。
p.popUntil(tableScope, "td", "th")
: 現在のセルを閉じます。p.clearActiveFormattingElements()
: アクティブフォーマット要素のリストをクリアします。p.im = inRowIM
: 挿入モードをinRowIM
に遷移させます。return false
: 現在のトークンを再処理するように指示します。これにより、閉じられたセルの後に検出された終了タグが、新しい挿入モード(inRowIM
)で適切に処理されます。
- これらの終了タグが
削除された CommentToken
処理
- 以前は
CommentToken
を検出した場合にCommentNode
を追加するロジックがinCellIM
内に直接記述されていましたが、これはinCellIM
の特定のルールではなく、より一般的なツリー構築ロジックの一部として扱われるべきです。このコミットで削除され、パーサーの他の部分でコメントが適切に処理されることが期待されます。
これらの変更により、inCellIM
はHTML5の複雑なテーブルパースルール、特に暗黙的な要素の閉じと状態遷移をより正確に実装し、ブラウザの挙動との一貫性を高めています。
関連リンク
- HTML5 Parsing Algorithm (W3C Recommendation): https://www.w3.org/TR/html5/syntax.html#parsing
- HTML5 "in cell" insertion mode (Section 12.2.5.4.15): https://www.w3.org/TR/html5/syntax.html#the-in-cell-insertion-mode (このリンクはHTML5の最終勧告版であり、コミット当時の草案とは異なる可能性がありますが、基本的なルールは共通しています。)
- Go
exp/html
package (GoDoc): https://pkg.go.dev/exp/html (現在のgolang.org/x/net/html
に相当)
参考にした情報源リンク
- W3C HTML5仕様書
- Go言語の
exp/html
パッケージのソースコード - HTMLパーシングに関する一般的な知識とドキュメント
- WebKitのテストログ(
webkit02.dat.log
)は、実際のブラウザの挙動をテストケースとして反映しており、仕様準拠の検証に役立ちます。