[インデックス 10360] ファイルの概要
このコミットは、Go言語のhtml
パッケージにおけるHTMLパーサーの挙動を修正し、<dd>
および<dt>
要素の自動クローズ処理を適切に行うように変更したものです。これにより、HTML5の仕様に準拠したパース結果が得られるようになり、特定のテストケース(tests2.dat
のテスト8およびテスト9)がパスするようになりました。
コミット
- コミットハッシュ:
06ef97e15d8952d46118427d4e93b490d0366fa8
- 作者: Andrew Balholm (
andybalholm@gmail.com
) - コミット日時: 2011年11月13日(日)23:27:20 +1100
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/06ef97e15d8952d46118427d4e93b490d0366fa8
元コミット内容
html: auto-close <dd> and <dt> elements
Pass tests2.dat, test 8:
<!DOCTYPE html><dt><div><dd>
| <!DOCTYPE html>
| <html>
| <head>
| <body>
| <dt>
| <div>
| <dd>
Also pass tests through test 9:
<script></x
R=nigeltao
CC=golang-dev
https://golang.org/cl/5373083
変更の背景
このコミットの主な背景は、Go言語のhtml
パッケージがHTML5のパース仕様に完全に準拠していなかった点にあります。特に、<dd>
(description details)と<dt>
(description term)要素の自動クローズに関する挙動が問題でした。
HTML5の仕様では、これらの要素は特定の条件下で終了タグが省略可能であり、ブラウザのHTMLパーサーはそれらを自動的にクローズする(または、新しい要素が開始されたときに前の要素を暗黙的に終了させる)必要があります。例えば、<dt>
要素の直後に別の<dt>
要素や<dd>
要素が続く場合、最初の<dt>
は自動的に閉じられます。同様に、<dd>
要素の直後に別の<dd>
要素や<dt>
要素が続く場合も、最初の<dd>
は自動的に閉じられます。
元のパーサーは、このようなHTML5の自動クローズルールを適切に処理できていなかったため、tests2.dat
というテストファイル内の特定のテストケース(テスト8: <!DOCTYPE html><dt><div><dd>
や テスト9: <script></x
)で誤ったパース結果を生成していました。このコミットは、これらのテストケースをパスするようにパーサーのロジックを修正することを目的としています。
具体的には、<!DOCTYPE html><dt><div><dd>
のようなマークアップが与えられた場合、<div>
要素が<dt>
要素の子として不正に配置され、その後に<dd>
要素が続くことで、パーサーが期待通りのDOMツリーを構築できない問題がありました。この修正により、<div>
要素が<dt>
要素を暗黙的に閉じ、その後に<dd>
要素が適切に配置されるようになります。
前提知識の解説
HTMLのパキュメント構造と要素
HTMLドキュメントは、要素のツリー構造で構成されます。各要素は開始タグと終了タグを持ち、その間にコンテンツや子要素を含みます。例えば、<p>これは段落です。</p>
。
<dl>
, <dt>
, <dd>
要素
これらは定義リスト(Description List)を構成する要素です。
<dl>
: 定義リスト全体を囲むコンテナ要素です。<dt>
: 定義される用語(Description Term)を表します。<dd>
: 用語の定義や説明(Description Details)を表します。
例:
<dl>
<dt>コーヒー</dt>
<dd>カフェインを含む飲み物。</dd>
<dt>紅茶</dt>
<dd>茶葉から作られる飲み物。</dd>
</dl>
HTML5のパースルールとタグの省略
HTML5の仕様は、ブラウザがHTMLドキュメントをどのように解析し、DOMツリーを構築するかを厳密に定義しています。この仕様には、特定の要素の終了タグが省略可能であるというルールが含まれています。これは、開発者がより簡潔なHTMLを書けるようにするため、また、ブラウザが不完全なHTMLをより堅牢に処理できるようにするためです。
<dt>
と<dd>
要素は、この終了タグ省略が可能な要素の典型例です。
<dt>
の終了タグ省略:<dt>
要素の直後に別の<dt>
要素、または<dd>
要素が続く場合、最初の<dt>
の終了タグは省略できます。パーサーは、新しい<dt>
または<dd>
が開始された時点で、前の<dt>
が終了したと解釈します。<dd>
の終了タグ省略:<dd>
要素の直後に別の<dd>
要素、または<dt>
要素が続く場合、または親の<dl>
要素のコンテンツが終了する場合、最初の<dd>
の終了タグは省略できます。
この「自動クローズ」の挙動は、ブラウザがHTMLをレンダリングする際に、開発者が明示的に終了タグを記述しなくても正しいDOMツリーを構築するために不可欠です。Goのhtml
パッケージのようなHTMLパーサーは、このHTML5の仕様に準拠して動作する必要があります。
HTMLパーサーの内部動作(スタックベースの処理)
多くのHTMLパーサーは、要素の開始と終了を追跡するためにスタックデータ構造を使用します。
- 開始タグを読み込むと、その要素をスタックにプッシュします。
- 終了タグを読み込むと、スタックのトップにある要素がその終了タグに対応していれば、その要素をスタックからポップします。
- もし、終了タグがスタックのトップの要素に対応していない場合(例えば、
<div><span></div>
のような場合)、パーサーはエラー回復ロジックを適用し、スタック上の適切な要素を見つけるか、暗黙的に要素を閉じます。
このコミットで修正されたのは、まさにこの「暗黙的に要素を閉じる」ロジック、特に<dd>
と<dt>
要素に関する部分です。
技術的詳細
このコミットは、Go言語のsrc/pkg/html/parse.go
ファイル内のHTMLパーサーの主要な関数であるinBodyIM
に修正を加えています。inBodyIM
関数は、HTMLドキュメントの<body>
要素内でのトークン(タグやテキストなど)の処理を担当します。
Goのhtml
パッケージのパーサーは、HTML5のパースアルゴリズムに厳密に従って実装されています。このアルゴリズムは、入力ストリームからトークンを読み込み、それらを基にDOMツリーを構築します。パーサーは内部的に「オープン要素スタック(Open Elements Stack)」と呼ばれるデータ構造を保持しており、これは現在開いている(まだ終了タグが処理されていない)要素のリストを管理します。
修正の中心は、<dd>
と<dt>
要素が検出された際の処理ロジックです。
元のパーサーでは、これらの要素が検出された際に、HTML5の仕様で定められているような特定の親要素(例えば、<address>
, <div>
, <p>
など)が存在する場合に、それらを適切に処理して現在の<dd>
または<dt>
要素を自動的にクローズするロジックが不足していました。
新しいロジックでは、<dd>
または<dt>
要素が検出されると、以下の処理が行われます。
p.framesetOK = false
: これは、パーサーがフレームセットモードに入ることを許可しないことを示します。これはHTML5のパースアルゴリズムの一部であり、特定の要素が検出された場合にフレームセットモードへの移行を禁止するルールです。- オープン要素スタックの走査:
for i := len(p.oe) - 1; i >= 0; i--
ループを使って、オープン要素スタック(p.oe
)を逆順に走査します。これは、現在開いている要素の中から、特定の条件に合致する要素を探すためです。 - 要素のチェックError flushing log events: Error: getaddrinfo ENOTFOUND play.googleapis.com
at GetAddrInfoReqWrap.onlookupall [as oncomplete] (node:dns:120:26) {
errno: -3008,
code: 'ENOTFOUND',
syscall: 'getaddrinfo',
hostname: 'play.googleapis.com'
}
とポップ:
- もしスタック上の要素が
"dd"
または"dt"
であれば、その要素より上位の要素をスタックからポップします(p.oe = p.oe[:i]
)。これは、新しい<dd>
または<dt>
が開始される前に、以前の<dd>
または<dt>
を暗黙的に閉じるための処理です。 - もしスタック上の要素が
"address"
,"div"
,"p"
であれば、continue
して次の要素のチェックに進みます。これらの要素は、<dd>
や<dt>
の自動クローズに影響を与えない、または特定のルールに従って処理されるべき要素です。 - それ以外の要素で、かつ
isSpecialElement
マップで「特殊な要素」としてマークされていない場合もcontinue
します。 - 上記のいずれの条件にも合致しない場合、
break
してループを終了します。これは、適切なクローズポイントが見つかったことを意味します。
- もしスタック上の要素が
p.popUntil(buttonScopeStopTags, "p")
: この関数呼び出しは、特定の「スコープ停止タグ」(buttonScopeStopTags
)または<p>
要素が見つかるまで、オープン要素スタックから要素をポップします。これは、HTML5のパースアルゴリズムにおける「インサートモード」のルールの一部であり、特定の要素が検出された場合に、その要素が挿入される前にスタックをクリーンアップするために使用されます。p.addElement(p.tok.Data, p.tok.Attr)
: 最後に、現在処理中の<dd>
または<dt>
要素をDOMツリーに追加します。
この修正により、パーサーは<dd>
や<dt>
要素が検出された際に、HTML5の仕様に従って適切に既存の要素を閉じ、正しいDOMツリーを構築できるようになりました。
コアとなるコードの変更箇所
diff --git a/src/pkg/html/parse.go b/src/pkg/html/parse.go
index d6505c6913..e8edcf956f 100644
--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -637,6 +637,24 @@ func inBodyIM(p *parser) bool {
}\n \t\t\tp.popUntil(buttonScopeStopTags, "p")
\t\t\tp.addElement(p.tok.Data, p.tok.Attr)
+\t\tcase "dd", "dt":
+\t\t\tp.framesetOK = false
+\t\t\tfor i := len(p.oe) - 1; i >= 0; i-- {
+\t\t\t\tnode := p.oe[i]
+\t\t\t\tswitch node.Data {
+\t\t\t\tcase "dd", "dt":
+\t\t\t\t\tp.oe = p.oe[:i]
+\t\t\t\tcase "address", "div", "p":
+\t\t\t\t\tcontinue
+\t\t\t\tdefault:
+\t\t\t\t\tif !isSpecialElement[node.Data] {
+\t\t\t\t\t\tcontinue
+\t\t\t\t\t}
+\t\t\t\t}
+\t\t\t\tbreak
+\t\t\t}
+\t\t\tp.popUntil(buttonScopeStopTags, "p")
+\t\t\tp.addElement(p.tok.Data, p.tok.Attr)
\t\tcase "optgroup", "option":
\t\t\tif p.top().Data == "option" {
\t\t\t\tp.oe.pop()
diff --git a/src/pkg/html/parse_test.go b/src/pkg/html/parse_test.go
index 13c50a99bc..992f73b060 100644
--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -134,7 +134,7 @@ func TestParser(t *testing.T) {
\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", 0},\n+\t\t{"tests2.dat", 10},\n \t\t{"tests3.dat", 0},\n \t}\n \tfor _, tf := range testFiles {
コアとなるコードの解説
src/pkg/html/parse.go
の変更
inBodyIM
関数内のswitch
文に、"dd"
と"dt"
の新しいcase
ブロックが追加されました。
case "dd", "dt":
p.framesetOK = false
for i := len(p.oe) - 1; i >= 0; i-- {
node := p.oe[i]
switch node.Data {
case "dd", "dt":
p.oe = p.oe[:i] // 現在のdd/dtより前のdd/dtをポップ
case "address", "div", "p":
continue // これらの要素はスキップ
default:
if !isSpecialElement[node.Data] {
continue // 特殊でない要素はスキップ
}
}
break // 適切な要素が見つかったらループを終了
}
p.popUntil(buttonScopeStopTags, "p") // 特定のスコープ停止タグまたはpが見つかるまでポップ
p.addElement(p.tok.Data, p.tok.Attr) // 新しいdd/dt要素を追加
このコードブロックは、HTML5のパースアルゴリズムにおける<dd>
および<dt>
要素の「インサートモード」のルールを実装しています。
p.framesetOK = false
: これは、パーサーがフレームセットモードに切り替わることを防ぐためのフラグです。HTML5の仕様では、特定の要素がボディ内で検出された場合、フレームセットモードへの移行が禁止されます。for i := len(p.oe) - 1; i >= 0; i--
: このループは、オープン要素スタック(p.oe
)を逆順に走査します。これは、現在開いている要素の中から、新しい<dd>
または<dt>
要素が挿入される前に閉じるべき要素を探すためです。switch node.Data
: スタック上の各要素のタグ名をチェックします。case "dd", "dt"
: もしスタック上の要素が既に開いている<dd>
または<dt>
であれば、その要素より上位の要素をスタックから削除します(p.oe = p.oe[:i]
)。これにより、新しい<dd>
または<dt>
が挿入される前に、以前の同種要素が暗黙的に閉じられます。case "address", "div", "p"
: これらの要素は、<dd>
や<dt>
の自動クローズの文脈では特殊な扱いを受けます。これらの要素が見つかった場合、ループは続行され、さらにスタックを遡ってチェックします。default
: その他の要素の場合、isSpecialElement
マップでその要素が「特殊な要素」(例えば、HTML5のセクショニングコンテンツやフローコンテンツなど、特定のパースルールを持つ要素)として定義されていない限り、ループは続行されます。特殊な要素が見つかった場合、または上記のいずれの条件にも合致しない場合は、break
してループを終了します。これは、適切なクローズポイントが見つかったことを意味します。
p.popUntil(buttonScopeStopTags, "p")
: この行は、HTML5のパースアルゴリズムにおける「インサートモード」のルールに従い、特定の要素(buttonScopeStopTags
に含まれる要素や<p>
要素)が見つかるまで、オープン要素スタックから要素をポップします。これは、新しい要素が挿入される前にスタックを適切な状態にクリーンアップするために行われます。p.addElement(p.tok.Data, p.tok.Attr)
: 最後に、現在処理中の<dd>
または<dt>
要素をDOMツリーに追加します。
この一連の処理により、パーサーは<dd>
や<dt>
要素が検出された際に、HTML5の仕様に準拠した自動クローズ動作を実現し、より正確なDOMツリーを構築できるようになります。
src/pkg/html/parse_test.go
の変更
テストファイルparse_test.go
では、TestParser
関数内のtestFiles
スライスが変更されています。
- {"tests2.dat", 0},
+ {"tests2.dat", 10},
これは、tests2.dat
というテストデータファイルに対して、期待されるテストケースの数が0
から10
に変更されたことを意味します。つまり、このコミットによって、tests2.dat
内のより多くのテストケース(特に<dd>
や<dt>
の自動クローズに関連するテスト)が正しく処理されるようになり、テストがパスするようになったことを示しています。
関連リンク
- Go CL 5373083: https://golang.org/cl/5373083
参考にした情報源リンク
- HTML Standard - 13.2.6.4.1 The "in body" insertion mode (dd, dt elements): https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody
- HTML Standard - 13.2.5.4 Optional tags: https://html.spec.whatwg.org/multipage/syntax.html#optional-tags
- MDN Web Docs -
<dt>
: The Description Term element: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dt - MDN Web Docs -
<dd>
: The Description Details element: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dd - W3C HTML5.2 - 8.2.5.4.7 The "in body" insertion mode: https://www.w3.org/TR/html52/syntax.html#the-in-body-insertion-mode
- Y Combinator - HTML5 parsing rules for dt and dd: https://news.ycombinator.com/item?id=10000000 (Note: This link was from the search results, but the content might be a discussion rather than a direct specification. It's included as a reference from the search.)