[インデックス 10512] ファイルの概要
このコミットは、Go言語の標準ライブラリ html
パッケージにおけるHTMLパーサーの改善に関するものです。具体的には、HTML5のパースアルゴリズムに従い、非標準要素である <nobr>
(No Break) タグの処理を追加し、関連するテストケースを更新しています。これにより、パーサーが <nobr>
要素を正しく解釈し、DOMツリーに反映できるようになります。
コミット
commit 68e7363b56a0a42414620a5a5cb756fb0edf82e7
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Mon Nov 28 10:55:31 2011 +1100
html: parse <nobr> elements
Pass tests3.dat, test 20:
<!doctype html><nobr><nobr><nobr>
| <!DOCTYPE html>
| <html>
| <head>
| <body>
| <nobr>
| <nobr>
| <nobr>
Also pass tests through test 22:
<!doctype html><html><body><p><table></table></body></html>
R=nigeltao
CC=golang-dev
https://golang.org/cl/5438056
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/68e7363b56a0a42414620a5a5cb756fb0edf82e7
元コミット内容
Go言語の html
パッケージにおいて、<nobr>
要素のパース処理を追加しました。これにより、tests3.dat
のテスト20 (<!doctype html><nobr><nobr><nobr>
) が期待通りにパースされ、ネストされた <nobr>
タグが正しくDOMツリーに表現されるようになります。また、テストの範囲をテスト22まで拡張し、既存のパースロジックが引き続き正しく機能することを確認しています。
変更の背景
この変更の主な背景は、Go言語の html
パッケージがHTML5のパース仕様に準拠し、より堅牢なHTMLパーサーを提供することにあります。特に、HTML5の仕様では、ブラウザの互換性維持のために、非標準的または廃止された要素であっても特定のルールに従ってパースすることが求められます。
<nobr>
要素は、HTML 4.01 TransitionalおよびFrameset DTDsで定義されていた非標準要素であり、テキストの改行を抑制するために使用されました。しかし、HTML5では廃止されており、CSSの white-space: nowrap
プロパティの使用が推奨されています。それでも、既存のウェブコンテンツには <nobr>
が含まれている場合があるため、パーサーはこれを適切に処理する必要があります。
このコミット以前のパーサーは、<nobr>
要素を正しく処理できていなかったため、tests3.dat
のテスト20のような特定のテストケースで失敗していました。このテストケースは、ネストされた <nobr>
要素がどのようにパースされるべきかを示しており、パーサーがHTML5の「アクティブなフォーマット要素のリスト」の概念を正しく実装しているかを検証するものです。
このコミットは、パーサーが <nobr>
要素を「アクティブなフォーマット要素」として認識し、HTML5のパースアルゴリズムに従って適切に処理することで、これらのテストをパスし、より広範なHTMLドキュメントのパースに対応できるようにすることを目的としています。
前提知識の解説
HTML5のパースアルゴリズム
HTML5のパースアルゴリズムは、非常に複雑で、エラー耐性を持つように設計されています。これは、不正なHTMLであってもブラウザが可能な限り表示できるようにするためです。主要な概念には以下が含まれます。
- トークナイゼーション (Tokenization): 入力ストリームをタグ、属性、テキストなどのトークンに分解するプロセス。
- ツリー構築 (Tree Construction): トークンストリームをDOMツリーに変換するプロセス。
- 挿入モード (Insertion Modes): ツリー構築アルゴリズムの状態を定義するもので、現在のコンテキストに基づいてトークンをどのように処理するかを決定します。例えば、"in body" モードは
<body>
タグ内のコンテンツを処理します。 - アクティブなフォーマット要素のリスト (List of Active Formatting Elements): 特定のフォーマット要素(例:
<b>
,<i>
,<font>
,<nobr>
など)が開始タグで開かれ、まだ対応する終了タグで閉じられていない場合に、それらを追跡するためのリストです。このリストは、要素のネストが不正な場合でも、ブラウザがどのようにDOMツリーを構築するかを決定する上で重要です。例えば、<b><i></b>
のようにタグが正しくネストされていない場合でも、ブラウザは<b><i></i></b>
のように解釈しようとします。 - 要素のスコープ (Element in Scope): 特定の要素が現在のスコープ内にあるかどうかをチェックするメカニズム。これは、特定のタグが特定のコンテキストで許可されているかどうか、または特定の終了タグがどの開始タグに対応するかを決定するために使用されます。
<nobr>
要素
<nobr>
要素は "No Break" の略で、その内容が改行されないように指定するために使用されました。これは、長い単語やフレーズが途中で改行されるのを防ぐ目的で使われましたが、HTML5では廃止されています。現代のウェブ開発では、CSSの white-space: nowrap;
プロパティを使用することが推奨されます。しかし、古いウェブページとの互換性のために、HTMLパーサーは依然としてこの要素を認識し、適切に処理する必要があります。HTML5のパースアルゴリズムでは、<nobr>
は「アクティブなフォーマット要素」として扱われます。
Go言語の html
パッケージ
Go言語の html
パッケージは、HTML5の仕様に準拠したHTMLパーサーを提供します。このパッケージは、ウェブスクレイピング、HTMLテンプレートの処理、HTMLコンテンツのサニタイズなど、様々な用途で利用されます。内部的には、HTML5のパースアルゴリズムを実装しており、トークナイザーとツリーコンストラクタの2つの主要なコンポーネントで構成されています。
parse.go
: HTMLのパースロジックの主要部分が含まれています。parser
構造体がパースの状態を管理し、inBodyIM
のような関数が特定の挿入モードでのトークン処理を定義します。parse_test.go
: パーサーの動作を検証するためのテストケースが含まれています。tests3.dat
のようなデータファイルは、Webkitのテストスイートから派生したもので、様々なHTMLスニペットとその期待されるパース結果を含んでいます。
技術的詳細
このコミットは、src/pkg/html/parse.go
の inBodyIM
関数に <nobr>
要素の処理を追加することで、HTML5のパースアルゴリズムにおける「アクティブなフォーマット要素のリスト」の管理を改善しています。
inBodyIM
関数は、HTML5のツリー構築アルゴリズムにおける「in body」挿入モードに対応しています。このモードでは、<body>
要素内のコンテンツが処理されます。
<nobr>
要素が開始タグとして現れた場合、以下のステップが実行されます。
p.reconstructActiveFormattingElements()
: これは、アクティブなフォーマット要素のリストを再構築する重要なステップです。HTML5のパースアルゴリズムでは、特定の状況下でこのリストを再構築することで、要素のネストが不正な場合でもDOMツリーの一貫性を保ちます。例えば、<b><i></b>
のような不正なネストがあった場合、このステップで<b><i></i></b>
のように修正されることがあります。if p.elementInScope(defaultScopeStopTags, "nobr")
: ここでは、現在のスコープ内に<nobr>
要素が存在するかどうかをチェックしています。defaultScopeStopTags
は、特定の要素がスコープを停止させるタグのセットを定義します。もしスコープ内に<nobr>
が存在する場合、それは以前に開かれた<nobr>
タグがまだ閉じられていないことを意味します。p.inBodyEndTagFormatting("nobr")
: もしスコープ内に<nobr>
が存在した場合、この関数が呼び出されます。これは、あたかも<nobr>
の終了タグが検出されたかのように処理を行い、アクティブなフォーマット要素のリストから<nobr>
を削除し、対応するDOMノードを閉じます。これは、HTML5のパースアルゴリズムにおける「アクティブなフォーマット要素のリストに要素を追加する」際の特殊なケースに対応するためです。具体的には、同じフォーマット要素が既にリストに存在する場合、新しい要素を追加する前に既存の要素をリストから削除し、対応するノードを閉じる必要があります。p.reconstructActiveFormattingElements()
: 上記のinBodyEndTagFormatting
の呼び出しにより、アクティブなフォーマット要素のリストが変更された可能性があるため、再度リストを再構築します。p.addFormattingElement(p.tok.Data, p.tok.Attr)
: 最後に、現在の<nobr>
開始タグをアクティブなフォーマット要素のリストに追加し、対応するDOMノードを作成します。
この一連の処理により、ネストされた <nobr>
要素や、既存の <nobr>
要素が存在する状況でも、HTML5の仕様に従って正しくパースされるようになります。
src/pkg/html/parse_test.go
の変更は、tests3.dat
のテスト範囲を20から23に拡張しています。これは、<nobr>
要素のパースロジックが追加されたことで、より多くのテストケースをパスできるようになったことを示しています。tests3.dat
は、Webkitのテストスイートから派生したもので、HTML5のパースアルゴリズムの様々なエッジケースを検証するために使用されます。
コアとなるコードの変更箇所
diff --git a/src/pkg/html/parse.go b/src/pkg/html/parse.go
index 723f65a4d7..adfac61edc 100644
--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -679,6 +679,13 @@ func inBodyIM(p *parser) bool {
case "b", "big", "code", "em", "font", "i", "s", "small", "strike", "strong", "tt", "u":
p.reconstructActiveFormattingElements()
p.addFormattingElement(p.tok.Data, p.tok.Attr)
+ case "nobr":
+ p.reconstructActiveFormattingElements()
+ if p.elementInScope(defaultScopeStopTags, "nobr") {
+ p.inBodyEndTagFormatting("nobr")
+ p.reconstructActiveFormattingElements()
+ }
+ p.addFormattingElement(p.tok.Data, p.tok.Attr)
case "applet", "marquee", "object":
p.reconstructActiveFormattingElements()
p.addElement(p.tok.Data, p.tok.Attr)
diff --git a/src/pkg/html/parse_test.go b/src/pkg/html/parse_test.go
index 4a088c74b1..9e02173b80 100644
--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -152,7 +152,7 @@ func TestParser(t *testing.T) {
{"doctype01.dat", -1},
{"tests1.dat", -1},
{"tests2.dat", -1},
- {"tests3.dat", 20},
+ {"tests3.dat", 23},
}
for _, tf := range testFiles {
f, err := os.Open("testdata/webkit/" + tf.filename)
コアとなるコードの解説
src/pkg/html/parse.go
の変更
inBodyIM
関数内の switch p.tok.Data
ブロックに、"nobr"
ケースが追加されました。
case "nobr":
p.reconstructActiveFormattingElements()
if p.elementInScope(defaultScopeStopTags, "nobr") {
p.inBodyEndTagFormatting("nobr")
p.reconstructActiveFormattingElements()
}
p.addFormattingElement(p.tok.Data, p.tok.Attr)
p.reconstructActiveFormattingElements()
: これは、HTML5のパースアルゴリズムにおける「アクティブなフォーマット要素のリストを再構築する」ステップに対応します。新しいフォーマット要素を追加する前に、このリストが正しい状態であることを保証します。if p.elementInScope(defaultScopeStopTags, "nobr")
: 現在のスコープ内に<nobr>
要素が存在するかどうかをチェックします。defaultScopeStopTags
は、特定の要素がスコープを停止させるタグのセットを定義します。p.inBodyEndTagFormatting("nobr")
: もしスコープ内に<nobr>
が存在した場合、この関数が呼び出されます。これは、HTML5のパースアルゴリズムにおいて、同じフォーマット要素が既にアクティブなフォーマット要素のリストに存在する場合に、新しい要素を追加する前に既存の要素をリストから削除し、対応するノードを閉じるという特殊な処理を模倣します。これにより、ネストされた<nobr>
タグが正しく処理されます。p.reconstructActiveFormattingElements()
:inBodyEndTagFormatting
の呼び出しにより、アクティブなフォーマット要素のリストが変更された可能性があるため、再度リストを再構築します。p.addFormattingElement(p.tok.Data, p.tok.Attr)
: 最後に、現在の<nobr>
開始タグをアクティブなフォーマット要素のリストに追加し、対応するDOMノードを作成します。
この追加により、GoのHTMLパーサーは、HTML5の仕様に従って <nobr>
要素を正しく処理できるようになり、特にネストされた <nobr>
要素のケースで正確なDOMツリーを構築できるようになります。
src/pkg/html/parse_test.go
の変更
--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -152,7 +152,7 @@ func TestParser(t *testing.T) {
{"doctype01.dat", -1},
{"tests1.dat", -1},
{"tests2.dat", -1},
- {"tests3.dat", 20},
+ {"tests3.dat", 23},
}
for _, tf := range testFiles {
f, err := os.Open("testdata/webkit/" + tf.filename)
tests3.dat
のテストケースの実行範囲が 20
から 23
に変更されました。これは、<nobr>
要素のパースロジックが追加されたことで、パーサーが tests3.dat
内のより多くのテストケース(特に <nobr>
に関連するものや、その後のテスト)をパスできるようになったことを示しています。この変更は、新しい機能が正しく動作し、既存の機能に悪影響を与えていないことを検証するためのものです。
関連リンク
- https://golang.org/cl/5438056 (Go Code Review)
参考にした情報源リンク
- HTML5 Parsing Algorithm (WHATWG HTML Standard)
- List of active formatting elements (WHATWG HTML Standard)
- The
<nobr>
element (MDN Web Docs) - Go html package documentation (GoDoc -
golang.org/x/net/html
はsrc/pkg/html
の後継) - HTML 4.01 Specification (W3C Recommendation)
- Webkit Test Suite (WebkitのHTMLパーステストに関する情報源)