[インデックス 10827] ファイルの概要
このコミットは、Go言語の標準ライブラリであるhtml
パッケージにおけるHTMLパーサーの挙動を改善するものです。具体的には、HTMLドキュメント内に埋め込まれた「外部オブジェクト(Foreign Objects)」、特にSVG内のforeignObject
要素における終了タグの処理に関するバグ修正と機能強化が行われています。これにより、HTML5のパース仕様にさらに準拠し、より堅牢なHTMLパースが可能になります。
コミット
commit a369004e2318ad0f139f967c764918bd939980ce
Author: Nigel Tao <nigeltao@golang.org>
Date: Fri Dec 16 09:36:50 2011 +1100
html: handle end tags in foreign objects.
I'm not 100% sure I get all the corner cases right, for end tags, but
I'll let the test suite smoke it out.
Pass tests10.dat, test 1:
<!DOCTYPE html><svg></svg><![CDATA[a]]>
| <!DOCTYPE html>
| <html>
| <head>
| <body>
| <svg svg>
| <!-- [CDATA[a]] -->
Also pass tests through test 5:
<!DOCTYPE html><body><table><svg></svg></table>
R=andybalholm
CC=golang-dev
https://golang.org/cl/5495044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a369004e2318ad0f139f967c764918bd939980ce
元コミット内容
このコミットは、HTMLパーサーが外部オブジェクト(例えばSVGやMathML)のコンテキスト内で終了タグを適切に処理できるようにするためのものです。特に、SVGの<foreignObject>
要素のように、外部名前空間内にHTMLコンテンツが埋め込まれる場合の終了タグの挙動に焦点を当てています。
コミットメッセージでは、終了タグの全てのコーナーケースを完全に網羅しているか確信はないとしつつも、テストスイートがその正確性を検証するだろうと述べています。
具体的なテストケースとして、以下の2つが挙げられています。
<!DOCTYPE html><svg></svg><![CDATA[a]]>
: このHTML断片が正しくパースされ、<![CDATA[a]]>
がSVG要素のコメントとして扱われることを示唆しています。<!DOCTYPE html><body><table><svg></svg></table>
: テーブル内にSVG要素が埋め込まれた場合のパースが正しく行われることを示唆しています。
これらの変更により、tests10.dat
のテスト1とテスト5がパスするようになったと報告されています。
変更の背景
HTML5のパース仕様は非常に複雑であり、特にHTML名前空間以外の要素(SVGやMathMLなどの外部オブジェクト)がHTMLドキュメント内に埋め込まれる場合の挙動は、厳密に定義されています。これらの外部オブジェクト内では、HTMLの通常のパースルールとは異なるルールが適用されることがあります。
以前のGoのhtml
パーサーは、外部オブジェクト内での終了タグの処理において、HTML5仕様の特定の要件を満たしていなかった可能性があります。例えば、外部オブジェクト内で予期せぬ終了タグが出現した場合や、外部オブジェクトの終了タグが正しく処理されず、パーサーが誤った状態に遷移してしまうなどの問題が考えられます。
このコミットの目的は、これらの外部オブジェクト、特にSVGのforeignObject
要素における終了タグの処理をHTML5仕様に準拠させることで、パーサーの堅牢性と正確性を向上させることにあります。これにより、より多様な、あるいは複雑なHTMLドキュメントを正しくパースできるようになります。
前提知識の解説
このコミットを理解するためには、以下の前提知識が必要です。
-
HTML5パースアルゴリズム:
- トークナイゼーション: 入力バイトストリームをトークン(開始タグ、終了タグ、テキスト、コメントなど)に変換するプロセス。
- ツリー構築: トークンストリームをDOMツリーに変換するプロセス。
- 挿入モード (Insertion Modes): ツリー構築アルゴリズムの主要な状態機械。HTMLドキュメントの現在の位置(例:
head
内、body
内、テーブル内など)に応じて、異なるトークン処理ルールが適用されます。例えば、inBody
モード、inTable
モード、inForeignContent
モードなどがあります。 - オープン要素のスタック (Stack of Open Elements): 現在開いている要素(まだ終了タグが来ていない要素)を追跡するためのスタック構造。要素が開始されるとスタックにプッシュされ、終了するとポップされます。このスタックは、要素の親子関係を決定し、パースエラーからの回復にも使用されます。
- 特殊要素 (Special Elements): HTML5仕様で「特別なパースルールを持つ要素」として定義されている要素群。これらは、特定の挿入モードにおいて、その子孫要素のパース挙動に影響を与えたり、特定の終了タグが来た場合にスタックから一気にポップされるなどの特殊な挙動を示します。例えば、
html
,body
,p
,table
,svg
などが含まれます。
-
外部オブジェクト (Foreign Objects):
- HTMLドキュメント内に埋め込まれた、HTML名前空間ではない要素のこと。主にSVG (Scalable Vector Graphics) と MathML (Mathematical Markup Language) がこれに該当します。
- これらの要素内では、HTMLの通常のパースルールとは異なるXMLベースのパースルールが適用されることがあります。
foreignObject
要素 (SVG): SVGの名前空間に属する要素ですが、その内部にHTMLコンテンツを埋め込むことを可能にします。これにより、SVGグラフィック内にリッチなHTMLテキストやフォーム要素などを配置できます。foreignObject
の内部では、再びHTMLのパースルールが適用されるため、パーサーは外部コンテンツモードからHTMLコンテンツモードへと遷移する必要があります。
-
Go言語の
html
パッケージ:- Go言語の標準ライブラリの一部で、HTML5ドキュメントをパースし、DOMツリーを構築するための機能を提供します。
Node
構造体: DOMツリーの各ノードを表します。Type
(要素、テキスト、コメントなど)、Data
(タグ名やテキスト内容)、Namespace
(要素の名前空間、例:html
,svg
,mathml
)などのフィールドを持ちます。parser
構造体: パースの状態を管理し、トークンを処理してDOMツリーを構築する主要なロジックを含みます。
技術的詳細
このコミットの技術的な核心は、HTMLパーサーが外部オブジェクト(特にSVG)のコンテキスト内で終了タグを検出した際の挙動を、HTML5仕様に沿って調整することにあります。
変更は主に以下の2つのファイルに集中しています。
-
src/pkg/html/const.go
:isSpecialElement
というmap[string]bool
型の変数がisSpecialElementMap
にリネームされました。これは、既存のマップがHTML名前空間の特殊要素のみを扱うことを明確にするためと考えられます。- 新たに
func isSpecialElement(element *Node) bool
という関数が導入されました。この関数は、与えられたNode
が「特殊要素」であるかどうかを判断します。element.Namespace
が空文字列または"html"
の場合、従来のisSpecialElementMap
を使用して要素のData
(タグ名)が特殊要素であるかをチェックします。element.Namespace
が"svg"
の場合、その要素のData
が**"foreignObject"
**である場合にのみtrue
を返します。これは、SVG名前空間においてはforeignObject
が特別なパース挙動を持つ要素として扱われるべきであることを示しています。- その他の名前空間の場合、デフォルトで
false
を返します。
- この変更により、
isSpecialElement
のチェックが、要素の名前空間を考慮したより汎用的なものになりました。
-
src/pkg/html/parse.go
:- 既存のコードベースで
isSpecialElement[node.Data]
のように直接マップを参照していた箇所が、新しく定義された関数isSpecialElement(node)
を呼び出すように変更されました。これにより、特殊要素の判定ロジックが一元化され、名前空間に応じた適切な判定が行われるようになります。 - 最も重要な変更は、
inForeignContentIM
(外部コンテンツ挿入モード)関数内のEndTagToken
(終了タグトークン)の処理ロジックです。- 以前は
// TODO.
とコメントされていた部分に、具体的な終了タグ処理が実装されました。 - この新しいロジックは、オープン要素のスタック(
p.oe
)を逆順に走査します。 - HTML名前空間への復帰: スタックを走査中に、名前空間が空文字列(
""
)の要素(これは通常HTML名前空間の要素を意味します)が見つかった場合、パーサーはinBodyIM(p)
を呼び出し、HTMLのinBody
挿入モードに遷移します。これは、外部コンテンツ内でHTMLコンテンツが埋め込まれており、そのHTMLコンテンツの終了タグが来た場合に、パーサーがHTMLのパースルールに戻る必要があることを示唆しています。 - 外部オブジェクトの終了タグ処理: 現在の終了タグトークン(
p.tok.Data
)と、スタック上の要素のData
(タグ名)がケースインセンシティブで一致する場合、その要素がスタックからポップされ、ループが終了します。これは、外部オブジェクトの通常の終了タグ処理です。 - これらの処理の後、
p.resetInsertionMode()
が呼び出され、現在のパース状態に基づいて適切な挿入モードにリセットされます。
- 以前は
- 既存のコードベースで
これらの変更により、パーサーは外部オブジェクト内での終了タグをより正確に解釈し、必要に応じてHTMLパースモードに適切に切り替えることができるようになります。
コアとなるコードの変更箇所
src/pkg/html/const.go
--- a/src/pkg/html/const.go
+++ b/src/pkg/html/const.go
@@ -7,7 +7,7 @@ package html
// Section 12.2.3.2 of the HTML5 specification says "The following elements
// have varying levels of special parsing rules".
// http://www.whatwg.org/specs/web-apps/current-work/multipage/parsing.html#the-stack-of-open-elements
-var isSpecialElement = map[string]bool{
+var isSpecialElementMap = map[string]bool{
"address": true,
"applet": true,
"area": true,
@@ -88,3 +88,13 @@ var isSpecialElement = map[string]bool{
"wbr": true,
"xmp": true,
}
+
+func isSpecialElement(element *Node) bool {
+ switch element.Namespace {
+ case "", "html":
+ return isSpecialElementMap[element.Data]
+ case "svg":
+ return element.Data == "foreignObject"
+ }
+ return false
+}
src/pkg/html/parse.go
--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -705,7 +705,7 @@ func inBodyIM(p *parser) bool {
case "address", "div", "p":
continue
default:
- if !isSpecialElement[node.Data] {
+ if !isSpecialElement(node) {
continue
}
}
@@ -723,7 +723,7 @@ func inBodyIM(p *parser) bool {
case "address", "div", "p":
continue
default:
- if !isSpecialElement[node.Data] {
+ if !isSpecialElement(node) {
continue
}
}
@@ -895,7 +895,7 @@ func (p *parser) inBodyEndTagFormatting(tag string) {
// Steps 5-6. Find the furthest block.
var furthestBlock *Node
for _, e := range p.oe[feIndex:] {
- if isSpecialElement[e.Data] {
+ if isSpecialElement(e) {
furthestBlock = e
break
}
@@ -988,7 +988,7 @@ func (p *parser) inBodyEndTagOther(tag string) {
p.oe = p.oe[:i]
break
}
- if isSpecialElement[p.oe[i].Data] {
+ if isSpecialElement(p.oe[i]) {
break
}
}
@@ -1606,7 +1606,18 @@ func inForeignContentIM(p *parser) bool {
// TODO: adjust foreign attributes.
p.addElement(p.tok.Data, p.tok.Attr)
case EndTagToken:
- // TODO.
+ for i := len(p.oe) - 1; i >= 0; i-- {
+ if p.oe[i].Namespace == "" {
+ inBodyIM(p)
+ break
+ }
+ if strings.EqualFold(p.oe[i].Data, p.tok.Data) {
+ p.oe = p.oe[:i]
+ break
+ }
+ }
+ p.resetInsertionMode()
+ return true
default:
// Ignore the token.
}
src/pkg/html/parse_test.go
--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -173,6 +173,7 @@ func TestParser(t *testing.T) {
{"tests4.dat", -1},
{"tests5.dat", -1},
{"tests6.dat", 36},
+ {"tests10.dat", 6},
}
for _, tf := range testFiles {
f, err := os.Open("testdata/webkit/" + tf.filename)
コアとなるコードの解説
src/pkg/html/const.go
の変更
isSpecialElementMap
へのリネーム: これは、HTML名前空間の特殊要素を定義するマップであることを明確にするための命名変更です。isSpecialElement(element *Node) bool
関数の追加:- この関数は、HTML5パース仕様における「特殊要素」の概念をより正確に実装します。
switch element.Namespace
文により、要素の名前空間に基づいて異なるロジックを適用します。case "", "html"
: HTML名前空間の要素の場合、従来のisSpecialElementMap
を使用して、そのタグ名が特殊要素であるかをチェックします。case "svg"
: SVG名前空間の要素の場合、**foreignObject
**タグのみを特殊要素として扱います。これは、SVG内でHTMLコンテンツを埋め込むforeignObject
が、パースモードの切り替えに影響を与える特別な要素であるためです。- これにより、パーサーは要素の名前空間を考慮して、より正確に特殊要素を識別できるようになりました。
src/pkg/html/parse.go
の変更
-
isSpecialElement
関数の利用:inBodyIM
、inBodyEndTagFormatting
、inBodyEndTagOther
といった関数内で、以前は直接isSpecialElement
マップを参照していた箇所が、新しく定義されたisSpecialElement(node)
関数を呼び出すように変更されました。これにより、特殊要素の判定ロジックがカプセル化され、名前空間を考慮した判定が自動的に行われるようになります。
-
inForeignContentIM
関数内のEndTagToken
処理の追加:- この部分がこのコミットの最も重要な変更点です。
inForeignContentIM
は、パーサーがSVGやMathMLなどの外部コンテンツをパースしている際の挿入モードです。 for i := len(p.oe) - 1; i >= 0; i--
ループは、オープン要素のスタックを最新の要素から遡って走査します。if p.oe[i].Namespace == ""
:- スタックを遡る途中で、名前空間が空の要素(つまりHTML名前空間の要素)が見つかった場合、これは外部コンテンツ内にHTMLコンテンツが埋め込まれており、そのHTMLコンテンツの終了タグが来たことを意味します。
- この場合、
inBodyIM(p)
が呼び出され、パーサーはHTMLのinBody
挿入モードに切り替わります。これにより、HTMLコンテンツのパースルールが再開されます。 break
によりループを終了します。
if strings.EqualFold(p.oe[i].Data, p.tok.Data)
:- 現在の終了タグトークン(
p.tok.Data
)と、スタック上の要素のタグ名(p.oe[i].Data
)がケースを無視して一致する場合、それは対応する開始タグが見つかったことを意味します。 p.oe = p.oe[:i]
により、一致した要素とその上位の要素をスタックからポップします。これにより、要素が閉じられた状態になります。break
によりループを終了します。
- 現在の終了タグトークン(
- ループの終了後、
p.resetInsertionMode()
が呼び出されます。これは、現在のパース状態(オープン要素のスタックのトップなど)に基づいて、パーサーの挿入モードを適切にリセットする重要なステップです。これにより、パーサーは次のトークンを正しいコンテキストで処理できるようになります。 return true
は、トークンが処理されたことを示します。
- この部分がこのコミットの最も重要な変更点です。
src/pkg/html/parse_test.go
の変更
{"tests10.dat", 6}
の追加:- これは、新しいテストデータファイル
tests10.dat
のテストケース6をTestParser
に追加するものです。このテストケースは、外部オブジェクト内での終了タグ処理に関する新しいロジックを検証するために作成されたと考えられます。コミットメッセージで言及されているtests10.dat, test 1
とtests through test 5
は、このテストファイルが複数のテストケースを含んでいることを示唆しています。
- これは、新しいテストデータファイル
これらの変更により、Goのhtml
パーサーは、HTML5仕様の複雑な外部オブジェクトのパースルール、特にforeignObject
要素内でのHTMLコンテンツの終了タグ処理をより正確に扱えるようになりました。
関連リンク
- HTML5仕様 - 12.2.3.2 The stack of open elements: https://html.spec.whatwg.org/multipage/parsing.html#the-stack-of-open-elements
- HTML5仕様 - 12.2.5 The tree construction dispatcher: https://html.spec.whatwg.org/multipage/parsing.html#the-tree-construction-dispatcher
- HTML5仕様 - 12.2.5.4.1 The "in foreign content" insertion mode: https://html.spec.whatwg.org/multipage/parsing.html#in-foreign-content
- SVG
foreignObject
要素: https://developer.mozilla.org/ja/docs/Web/SVG/Element/foreignObject - Go
html
パッケージ ドキュメント: https://pkg.go.dev/golang.org/x/net/html (注: このコミットは古いGoのバージョンであり、現在のhtml
パッケージはgolang.org/x/net/html
に移動しています。)
参考にした情報源リンク
- HTML5仕様 (WHATWG): 上記の関連リンクに記載されているHTML5の公式仕様書。
- MDN Web Docs: SVG
foreignObject
要素に関する情報。 - Go言語の公式ドキュメントおよびソースコード。
- コミットメッセージ内のGo CL (Code Review) リンク:
https://golang.org/cl/5495044
(現在はアクセスできない可能性がありますが、当時のコードレビュープロセスを示唆しています。) - WebKitのHTMLテストスイート: コミットメッセージで参照されている
tests10.dat
のようなテストファイルは、WebKitプロジェクトのHTMLパーステストスイートの一部である可能性が高いです。これらのテストは、HTML5仕様の複雑なコーナーケースを検証するために広く使用されています。