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

[インデックス 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パーサーは、要素の開始と終了を追跡するためにスタックデータ構造を使用します。

  1. 開始タグを読み込むと、その要素をスタックにプッシュします。
  2. 終了タグを読み込むと、スタックのトップにある要素がその終了タグに対応していれば、その要素をスタックからポップします。
  3. もし、終了タグがスタックのトップの要素に対応していない場合(例えば、<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>要素が検出されると、以下の処理が行われます。

  1. p.framesetOK = false: これは、パーサーがフレームセットモードに入ることを許可しないことを示します。これはHTML5のパースアルゴリズムの一部であり、特定の要素が検出された場合にフレームセットモードへの移行を禁止するルールです。
  2. オープン要素スタックの走査: for i := len(p.oe) - 1; i >= 0; i-- ループを使って、オープン要素スタック(p.oe)を逆順に走査します。これは、現在開いている要素の中から、特定の条件に合致する要素を探すためです。
  3. 要素のチェック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してループを終了します。これは、適切なクローズポイントが見つかったことを意味します。
  4. p.popUntil(buttonScopeStopTags, "p"): この関数呼び出しは、特定の「スコープ停止タグ」(buttonScopeStopTags)または<p>要素が見つかるまで、オープン要素スタックから要素をポップします。これは、HTML5のパースアルゴリズムにおける「インサートモード」のルールの一部であり、特定の要素が検出された場合に、その要素が挿入される前にスタックをクリーンアップするために使用されます。
  5. 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>要素の「インサートモード」のルールを実装しています。

  1. p.framesetOK = false: これは、パーサーがフレームセットモードに切り替わることを防ぐためのフラグです。HTML5の仕様では、特定の要素がボディ内で検出された場合、フレームセットモードへの移行が禁止されます。
  2. for i := len(p.oe) - 1; i >= 0; i--: このループは、オープン要素スタック(p.oe)を逆順に走査します。これは、現在開いている要素の中から、新しい<dd>または<dt>要素が挿入される前に閉じるべき要素を探すためです。
  3. 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してループを終了します。これは、適切なクローズポイントが見つかったことを意味します。
  4. p.popUntil(buttonScopeStopTags, "p"): この行は、HTML5のパースアルゴリズムにおける「インサートモード」のルールに従い、特定の要素(buttonScopeStopTagsに含まれる要素や<p>要素)が見つかるまで、オープン要素スタックから要素をポップします。これは、新しい要素が挿入される前にスタックを適切な状態にクリーンアップするために行われます。
  5. 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>の自動クローズに関連するテスト)が正しく処理されるようになり、テストがパスするようになったことを示しています。

関連リンク

参考にした情報源リンク