[インデックス 11090] ファイルの概要
コミット
commit aa033c20b356b608e2fcc51e284cf711f952309b
Author: Nigel Tao <nigeltao@golang.org>
Date: Wed Jan 11 10:15:40 2012 +1100
html: propagate foreign namespaces only when adding foreign content.
Pass tests10.dat, test 31:
<div><svg><path><foreignObject><p></div>a
| <html>
| <head>
| <body>
| <div>
| <svg svg>
| <svg path>
| <svg foreignObject>
| <p>
| "a"
Also pass test 32:
<!DOCTYPE html><svg><desc><div><svg><ul>a
R=andybalholm
CC=golang-dev
https://golang.org/cl/5527064
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/aa033c20b356b608e2fcc51e284cf711f952309b
元コミット内容
このコミットは、Go言語のhtml
パッケージにおけるHTMLパーサーの挙動を修正するものです。具体的には、外部(foreign)名前空間(例:SVGやMathML)の要素をHTMLコンテンツに追加する際に、名前空間の伝播(propagate)が正しく行われるように変更しています。これにより、tests10.dat
のテスト31およびテスト32がパスするようになります。
テスト31の例:
<div><svg><path><foreignObject><p></div>a
この入力に対して、パーサーは以下のような構造を生成することが期待されます。
<html>
<head>
<body>
<div>
<svg svg>
<svg path>
<svg foreignObject>
<p>
"a"
テスト32の例:
<!DOCTYPE html><svg><desc><div><svg><ul>a
変更の背景
HTML5のパース仕様では、HTMLコンテンツ内にSVGやMathMLのようなXML名前空間を持つ要素(これらを「外部コンテンツ」または「foreign content」と呼びます)が埋め込まれる場合、その要素とその子孫要素は適切な名前空間に属する必要があります。従来のパーサーの実装では、この名前空間の伝播が常に適切に行われていなかった可能性があります。
特に、foreignObject
要素のように、外部名前空間内にHTML要素を埋め込むことができる特殊なケースでは、名前空間の切り替えと伝播のロジックが複雑になります。このコミットは、このようなシナリオにおいて、要素が追加される際に親要素の名前空間を無条件に継承するのではなく、外部コンテンツの追加時のみに名前空間を適切に設定し直すことで、正しいDOMツリーが構築されるように問題を解決しています。
これにより、HTML5のパース仕様に準拠し、より堅牢で正確なHTMLパーサーを提供することが目的です。
前提知識の解説
HTMLパーシングとDOMツリー
HTMLパーシングとは、HTMLドキュメントのテキストを読み込み、それをブラウザが理解できる構造化されたデータ(DOMツリー)に変換するプロセスです。DOM(Document Object Model)ツリーは、HTMLドキュメントの論理的な構造を表現するツリー構造であり、各ノードはHTML要素、属性、テキストなどを表します。
XML名前空間 (Namespaces)
XML名前空間は、XMLドキュメント内で要素名や属性名の衝突を避けるためのメカニズムです。異なるXML語彙(例:HTML、SVG、MathML)からの要素が同じドキュメント内で使用される場合、名前空間はどの語彙に属するかを識別します。
- HTML名前空間: 通常、HTML要素は名前空間を持ちませんが、内部的には「HTML名前空間」に属すると見なされます。
- SVG名前空間: Scalable Vector Graphics (SVG) 要素は、
http://www.w3.org/2000/svg
という名前空間に属します。 - MathML名前空間: Mathematical Markup Language (MathML) 要素は、
http://www.w3.org/1998/Math/MathML
という名前空間に属します。
HTML5のパースアルゴリズムと外部コンテンツ (Foreign Content)
HTML5のパースアルゴリズムは非常に複雑で、特定のルールに基づいて要素の名前空間を決定します。特に重要な概念が「外部コンテンツ(Foreign Content)」です。
- 外部コンテンツ: HTMLドキュメント内に埋め込まれたSVGやMathMLの要素を指します。これらの要素はHTML名前空間ではなく、それぞれのXML名前空間に属します。
- 名前空間の切り替え: HTMLパーサーは、
<svg>
タグや<math>
タグを検出すると、現在の名前空間をHTML名前空間から対応するSVG名前空間やMathML名前空間に切り替えます。 foreignObject
要素: SVGの名前空間に属する特殊な要素で、その内部にHTML名前空間のコンテンツを埋め込むことができます。この要素の存在は、パーサーが名前空間を一時的にHTML名前空間に戻す必要があることを意味します。
このコミットは、特にforeignObject
のような要素が絡む複雑な名前空間の切り替えと伝播のシナリオにおいて、パーサーが正しく動作するようにするためのものです。
技術的詳細
このコミットの技術的な核心は、HTMLパーサーが要素をDOMツリーに追加する際の「名前空間の伝播」のロジックを修正することにあります。
以前の実装では、addElement
関数が新しい要素を追加する際に、無条件に現在のパーサーの状態(p.top()
)から名前空間を継承していました。これは、ほとんどのHTML要素では問題ありませんが、外部コンテンツ(SVGやMathML)の内部で、さらにHTMLコンテンツが埋め込まれるような特殊なケース(例: <svg><foreignObject><div>...</div></foreignObject></svg>
)では問題を引き起こす可能性がありました。
具体的には、<foreignObject>
要素の内部に入った場合、パーサーは一時的にHTML名前空間に戻る必要があります。しかし、addElement
が常に親の名前空間を継承してしまうと、<foreignObject>
内の<div>
のようなHTML要素が誤ってSVG名前空間に属してしまう、といった不整合が発生する可能性がありました。
このコミットでは、以下の2つの変更によってこの問題を解決しています。
addElement
からの名前空間継承の削除:addElement
関数から、新しい要素のNamespace
フィールドをp.top().Namespace
から初期化する行が削除されました。これにより、addElement
は要素の名前空間を自動的に設定しなくなります。parseForeignContent
での明示的な名前空間設定:parseForeignContent
関数内で、外部コンテンツの要素(例:<foreignObject>
)が追加される直前に現在の名前空間を一時変数namespace
に保存し、p.addElement
の呼び出し後に、その保存しておいた名前空間を明示的にp.top().Namespace
に設定し直しています。
この変更により、foreignObject
のような要素がパースされる際に、その要素自体は正しい外部名前空間に属しつつ、その子要素(特にHTMLコンテンツ)がパースされる際には、パーサーの状態が一時的にHTML名前空間に切り替わり、その後、親の外部名前空間に正しく戻されるという、より正確な名前空間の管理が可能になります。
これにより、HTML5の仕様に厳密に準拠したDOMツリーが構築され、ブラウザのレンダリングやJavaScriptによるDOM操作において予期せぬ挙動が発生するのを防ぎます。
コアとなるコードの変更箇所
diff --git a/src/pkg/html/parse.go b/src/pkg/html/parse.go
index 7077612e7a..43c04727ab 100644
--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -243,10 +243,9 @@ func (p *parser) addText(text string) {
// addElement calls addChild with an element node.
func (p *parser) addElement(tag string, attr []Attribute) {
p.addChild(&Node{
- Type: ElementNode,
- Data: tag,
- Namespace: p.top().Namespace,
- Attr: attr,
+ Type: ElementNode,
+ Data: tag,
+ Attr: attr,
})
}
@@ -1736,7 +1735,9 @@ func parseForeignContent(p *parser) bool {
panic("html: bad parser state: unexpected namespace")
}\n adjustForeignAttributes(p.tok.Attr)\n+\t\tnamespace := p.top().Namespace
\tp.addElement(p.tok.Data, p.tok.Attr)\n+\t\tp.top().Namespace = namespace
case EndTagToken:\n \tfor i := len(p.oe) - 1; i >= 0; i-- {\n \t\tif p.oe[i].Namespace == "" {\ndiff --git a/src/pkg/html/parse_test.go b/src/pkg/html/parse_test.go
index 91c8388b3a..c929c25772 100644
--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -184,7 +184,7 @@ func TestParser(t *testing.T) {
{"tests4.dat", -1},
{"tests5.dat", -1},
{"tests6.dat", -1},
- {"tests10.dat", 31},
+ {"tests10.dat", 33},
}
for _, tf := range testFiles {
f, err := os.Open("testdata/webkit/" + tf.filename)
コアとなるコードの解説
src/pkg/html/parse.go
-
func (p *parser) addElement(tag string, attr []Attribute)
の変更:- 変更前:
p.addChild(&Node{ Type: ElementNode, Data: tag, Namespace: p.top().Namespace, // ここで親の名前空間を継承していた Attr: attr, })
- 変更後:
p.addChild(&Node{ Type: ElementNode, Data: tag, Attr: attr, })
- この変更により、
addElement
関数は新しい要素を作成する際に、自動的に親要素の名前空間を継承しなくなりました。名前空間の設定は、より上位のパースロジック(特に外部コンテンツを扱う部分)で明示的に制御されるようになります。
- 変更前:
-
func parseForeignContent(p *parser) bool
の変更:- この関数は、SVGやMathMLなどの外部コンテンツをパースする際に呼び出されます。
- 変更前は、
p.addElement
が呼び出されると、その中で名前空間が設定されていました。 - 変更後:
adjustForeignAttributes(p.tok.Attr) namespace := p.top().Namespace // 現在の名前空間を一時的に保存 p.addElement(p.tok.Data, p.tok.Attr) p.top().Namespace = namespace // addElement後に保存した名前空間を再設定
p.addElement
を呼び出す前に、現在のパーサーのトップ要素の名前空間(つまり、外部コンテンツの名前空間)をnamespace
変数に保存しています。p.addElement
が呼び出された後(この時点では、新しい要素は名前空間を持たない状態で追加されています)、保存しておいたnamespace
の値を、新しく追加された要素(p.top()
)の名前空間として明示的に設定し直しています。- この修正により、
addElement
が名前空間を自動継承しないようになったことと合わせて、外部コンテンツの要素が追加される際に、その要素が正しい名前空間に属することが保証されます。特に、foreignObject
のような要素の内部でHTMLコンテンツがパースされる際に、名前空間の切り替えがより正確に行われるようになります。
src/pkg/html/parse_test.go
{"tests10.dat", 31}
が{"tests10.dat", 33}
に変更されています。これは、tests10.dat
ファイル内のテストケースのインデックスが変更されたことを示唆しています。コミットメッセージでテスト31と32がパスすると述べられていることから、この変更はテストスイートの更新または再編成によるものと考えられます。
関連リンク
- Go CL 5527064: https://golang.org/cl/5527064
参考にした情報源リンク
- HTML Standard - 8.2.5 The parsing model: https://html.spec.whatwg.org/multipage/parsing.html#the-parsing-model
- HTML Standard - 8.2.5.5 The rules for parsing tokens in HTML content: https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody (特に"in foreign content"のセクション)
- Namespaces in XML 1.0 (Third Edition): https://www.w3.org/TR/REC-xml-names/
- Scalable Vector Graphics (SVG) 1.1 (Second Edition): https://www.w3.org/TR/SVG11/
- Mathematical Markup Language (MathML) Version 3.0 (Second Edition): https://www.w3.org/TR/MathML3/
- Go言語の
html
パッケージのドキュメント (当時のバージョン): https://pkg.go.dev/html (現在のドキュメントですが、当時の実装の理解に役立ちます) - Go言語のソースコード (当時のバージョン): https://github.com/golang/go/tree/release-branch.go1 (コミット日時に近いリリースブランチを参照)