[インデックス 10735] ファイルの概要
このコミットは、Go言語のhtml
パッケージにおけるHTMLパーサーの機能拡張に関するものです。具体的には、HTMLドキュメント内に埋め込まれたMathML (Mathematical Markup Language) およびSVG (Scalable Vector Graphics) といった「外部コンテンツ(foreign content)」の初期的なパース(解析)サポートを導入しています。これにより、パーサーはこれらのXMLベースのコンテンツを適切に識別し、HTMLとは異なる名前空間(namespace)を持つノードとして扱うことができるようになります。
コミット
commit b9064fb13287c49ba978715af6da797428dcb77d
Author: Nigel Tao <nigeltao@golang.org>
Date: Tue Dec 13 13:52:47 2011 +1100
html: a first step at parsing foreign content (MathML, SVG).
Nodes now have a Namespace field.
Pass adoption01.dat, test 12:
<a><svg><tr><input></a>
| <html>
| <head>
| <body>
| <a>
| <svg svg>
| <svg tr>
| <svg input>
The other adoption01.dat tests already passed.
R=andybalholm
CC=golang-dev
https://golang.org/cl/5467075
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b9064fb13287c49ba978715af6da797428dcb77d
元コミット内容
このコミットは、HTMLパーサーがMathMLやSVGといった外部コンテンツを解析するための第一歩を踏み出すものです。主な変更点として、ノード構造にNamespace
フィールドが追加されました。これにより、パーサーはHTML要素と外部コンテンツの要素を名前空間に基づいて区別できるようになります。
adoption01.dat
テストスイートのテスト12(<a><svg><tr><input></a>
)がこの変更によってパスするようになりました。このテストケースは、<a>
タグ内に<svg>
タグがネストされ、さらにその中に<tr>
や<input>
といったHTML要素がネストされている場合の挙動を検証しています。変更後のパース結果は、<html>
-> <body>
-> <a>
-> <svg svg>
-> <svg tr>
-> <svg input>
のように、SVG要素が適切な名前空間(svg
)を持つノードとして表現されることを示しています。
変更の背景
HTML5の仕様では、HTMLドキュメント内にSVGやMathMLといったXMLベースのコンテンツを直接埋め込むことが可能です。これらのコンテンツは、HTMLとは異なる独自の要素セットとセマンティクスを持ちます。従来のHTMLパーサーは、これらの外部コンテンツを単なる未知のHTML要素として扱ってしまうか、あるいは正しくパースできない可能性がありました。
このコミットの背景には、Go言語のhtml
パッケージがHTML5の仕様に準拠し、より堅牢で汎用的なHTMLパーサーを提供することを目指すという目的があります。特に、SVGやMathMLのような外部コンテンツを正しく解析し、DOMツリー内で適切な名前空間情報を持つノードとして表現することは、これらのコンテンツをJavaScriptなどで操作したり、レンダリングエンジンで正しく表示したりするために不可欠です。
この変更は、HTML5のパースアルゴリズムにおける「外部コンテンツ(Foreign Content)」の取り扱いに関するセクション(例えば、W3CのHTML仕様の12.2.5.5節など)に準拠するための初期実装と言えます。
前提知識の解説
HTML5パーシングアルゴリズム
HTML5の仕様は、ウェブブラウザがHTMLドキュメントをどのように解析し、DOM(Document Object Model)ツリーを構築するかについて非常に詳細なアルゴリズムを定義しています。このアルゴリズムは、トークン化(Tokenization)とツリー構築(Tree Construction)の2つの主要なフェーズに分かれます。
- トークン化: 入力ストリーム(HTMLソースコード)を読み込み、個々のトークン(開始タグ、終了タグ、テキスト、コメントなど)に分解します。
- ツリー構築: トークン化フェーズで生成されたトークンを消費し、DOMツリーを構築します。このフェーズは、現在のパーサーの状態を示す「挿入モード(Insertion Mode)」に基づいて動作します。挿入モードは、次にどの種類のトークンが期待され、どのようにDOMツリーにノードを追加すべきかを決定します。
名前空間 (Namespace)
XMLの名前空間は、XMLドキュメント内の要素名や属性名の衝突を避けるためのメカニズムです。異なるXML語彙(例えば、HTML、SVG、MathML)が同じ要素名(例: <title>
)を持つ場合、名前空間を使用することで、どの語彙の要素であるかを明確に区別できます。名前空間はURI(Uniform Resource Identifier)によって識別され、通常は要素名のプレフィックスとして関連付けられます(例: <svg:svg>
)。HTML5では、SVGやMathML要素がHTMLドキュメント内に埋め込まれる際、これらの要素はそれぞれSVG名前空間(http://www.w3.org/2000/svg
)やMathML名前空間(http://www.w3.org/1998/Math/MathML
)に属するものとして扱われます。
外部コンテンツ (Foreign Content)
HTML5のパーシングアルゴリズムにおいて、「外部コンテンツ」とは、HTML名前空間に属さない要素、具体的にはSVG要素やMathML要素を指します。パーサーが外部コンテンツの開始タグを検出すると、その要素はHTML名前空間ではなく、対応する名前空間(SVGまたはMathML)に属するものとしてDOMツリーに挿入されます。外部コンテンツ内では、HTMLのパースルールとは異なるXMLのパースルールが適用される場合があります。
挿入モード (Insertion Mode)
HTML5のツリー構築アルゴリズムの中心的な概念です。パーサーは常に特定の挿入モードにあり、このモードが次のトークンをどのように処理するかを決定します。例えば、inBodyIM
は<body>
タグ内での処理を、inForeignContentIM
は外部コンテンツ内での処理を定義します。モードは、特定のタグの開始や終了、あるいは特定の条件に基づいて切り替わります。
アクティブフォーマット要素 (Active Formatting Elements)
HTML5パーシングアルゴリズムにおけるもう一つの重要な概念です。これは、<b>
、<i>
、<a>
などのフォーマット要素の開始タグが検出されたときに、それらの要素がDOMツリーに挿入されるだけでなく、特別なリスト(アクティブフォーマット要素リスト)にも追加されることを意味します。このリストは、ネストされたフォーマット要素の正しい構造を維持し、特定の状況で要素を「再構築」するために使用されます。
技術的詳細
このコミットは、Go言語のhtml
パッケージがHTML5の仕様に準拠し、外部コンテンツ(MathML、SVG)を適切にパースするための基盤を構築しています。
-
Node
構造体へのNamespace
フィールド追加:src/pkg/html/node.go
において、Node
構造体にNamespace string
フィールドが追加されました。これは、DOMツリー内の各要素がどの名前空間に属するか(HTML、SVG、MathMLなど)を識別するための最も基本的な変更です。これにより、パーサーは同じタグ名を持つ要素でも、その名前空間に基づいて異なるセースマンティクスで処理できるようになります。
-
foreign.go
の導入とbreakout
マップ:- 新しく
src/pkg/html/foreign.go
ファイルが追加され、breakout
というmap[string]bool
型の変数が定義されました。このマップには、外部コンテンツ(SVGやMathML)の内部で出現した場合に、パーサーを外部コンテンツモードから通常のHTMLモードに「ブレイクアウト」させるHTMLタグ名がリストされています。これはHTML5仕様の「Parsing HTML fragments」や「The rules for parsing tokens in foreign content」セクションで定義されている挙動を実装するためのものです。例えば、SVG要素の内部に<body>
タグが出現した場合、それはSVGの一部ではなく、HTMLのボディ要素として扱われるべきです。
- 新しく
-
パーサーの挿入モードの変更と
inForeignContentIM
の導入:src/pkg/html/parse.go
において、パーサーの挙動が大幅に修正されました。addElement
関数は、新しく追加されるノードのNamespace
を、現在のパーサーのスタックの最上位ノードのNamespace
から継承するように変更されました。resetInsertionMode
関数は、現在のノードが名前空間を持つ場合(つまり、外部コンテンツ内にある場合)に、挿入モードをinForeignContentIM
(外部コンテンツ内挿入モード)に切り替えるロジックが追加されました。これは、外部コンテンツのパースルールが適用されるべき状況を正確に検出するために重要です。inBodyIM
関数(<body>
タグ内の処理を司る挿入モード)では、math
またはsvg
の開始タグが検出された際に、以下の処理が行われます。reconstructActiveFormattingElements()
が呼び出され、アクティブフォーマット要素リストが再構築されます。- 検出されたタグに応じて、ノードの
Namespace
が"mathml"
または"svg"
に設定されます。 - その後、挿入モードが
inForeignContentIM
に切り替わります。これにより、以降のトークンは外部コンテンツのルールに従ってパースされるようになります。
inForeignContentIM
関数が新しく追加(または大幅に拡張)されました。このモードでは、コメントトークン、開始タグトークン、終了タグトークンが処理されます。特に、開始タグがbreakout
マップに含まれる場合、特別な処理(TODOコメントで示されているように、将来的にはブレイクアウト処理が実装される)が行われます。また、現在の名前空間(MathMLまたはSVG)に基づいて、属性の調整やタグ名の調整が必要になることがTODOコメントで示されています。
-
テストの更新:
src/pkg/html/parse_test.go
では、dumpLevel
関数が変更され、ElementNode
のダンプ時にNamespace
情報も出力されるようになりました(例:<svg svg>
)。これにより、パース結果のDOMツリーが名前空間情報を正しく保持しているかを確認できるようになります。TestParser
関数にadoption01.dat
テストスイートが追加され、外部コンテンツのパースに関するテストが実行されるようになりました。
これらの変更により、GoのHTMLパーサーは、HTML5の複雑な仕様の一部である外部コンテンツの取り扱いに関して、より正確で堅牢な挙動を示すようになりました。ただし、TODOコメントが示すように、MathMLやSVG固有の属性調整、タグ名の調整など、さらなる実装が必要な部分も残されています。
コアとなるコードの変更箇所
src/pkg/html/node.go
type Node struct {
Parent *Node
Child []*Node
Type NodeType
Data string
Namespace string // 新しく追加されたフィールド
Attr []Attribute
}
src/pkg/html/foreign.go
(新規ファイル)
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package html
// Section 12.2.5.5.
var breakout = map[string]bool{
"b": true,
"big": true,
"blockquote": true,
"body": true,
"br": true,
"center": true,
"code": true,
"dd": true,
"div": true,
"dl": true,
"dt": true,
"em": true,
"embed": true,
"font": true,
"h1": true,
"h2": true,
"h3": true,
"h4": true,
"h5": true,
"h6": true,
"head": true,
"hr": true,
"i": true,
"img": true,
"li": true,
"listing": true,
"menu": true,
"meta": true,
"nobr": true,
"ol": true,
"p": true,
"pre": true,
"ruby": true,
"s": true,
"small": true,
"span": true,
"strong": true,
"strike": true,
"sub": true,
"sup": true,
"table": true,
"tt": true,
"u": true,
"ul": true,
"var": true,
}
// TODO: add look-up tables for MathML and SVG adjustments.
src/pkg/html/parse.go
addElement
関数の変更:
func (p *parser) addElement(tag string, attr []Attribute) {
p.addChild(&Node{
- Type: ElementNode,
- Data: tag,
- Attr: attr,
+ Type: ElementNode,
+ Data: tag,
+ Namespace: p.top().Namespace, // 親ノードの名前空間を継承
+ Attr: attr,
})
}
resetInsertionMode
関数の変更:
// ...
for i := len(p.stack) - 1; i >= 0; i-- {
switch p.stack[i].Data {
case "html":
p.im = beforeHeadIM
default:
- continue
+ if p.top().Namespace == "" { // 名前空間がない場合(HTML)はスキップ
+ continue
+ }
+ p.im = inForeignContentIM // 名前空間がある場合は外部コンテンツモードへ
}
return
}
inBodyIM
関数の変更(math
, svg
タグのハンドリング追加):
case "math", "svg":
p.reconstructActiveFormattingElements()
namespace := ""
if p.tok.Data == "math" {
// TODO: adjust MathML attributes.
namespace = "mathml"
} else {
// TODO: adjust SVG attributes.
namespace = "svg"
}
// TODO: adjust foreign attributes.
p.addElement(p.tok.Data, p.tok.Attr)
p.top().Namespace = namespace // 新しいノードに名前空間を設定
p.im = inForeignContentIM // 外部コンテンツモードへ移行
return true
inForeignContentIM
関数の追加:
// TODO: fix up the other IM's section numbers to match the latest spec.
// Section 12.2.5.5.
func inForeignContentIM(p *parser) bool {
switch p.tok.Type {
case CommentToken:
p.addChild(&Node{
Type: CommentNode,
Data: p.tok.Data,
})
case StartTagToken:
if breakout[p.tok.Data] {
// TODO.
}
switch p.top().Namespace {
case "mathml":
// TODO: adjust MathML attributes.
case "svg":
// TODO: adjust SVG tag names.
// TODO: adjust SVG attributes.
default:
panic("html: bad parser state: unexpected namespace")
}
// TODO: adjust foreign attributes.
p.addElement(p.tok.Data, p.tok.Attr)
case EndTagToken:
// TODO.
default:
// Ignore the token.
}
return true
}
src/pkg/html/parse_test.go
dumpLevel
関数の変更(名前空間の表示追加):
case ElementNode:
- fmt.Fprintf(w, "<%s>", n.Data)
+ if n.Namespace != "" {
+ fmt.Fprintf(w, "<%s %s>", n.Namespace, n.Data) // 名前空間があれば表示
+ } else {
+ fmt.Fprintf(w, "<%s>", n.Data)
+ }
TestParser
関数の変更(adoption01.dat
の追加):
}{
// TODO(nigeltao): Process all the test cases from all the .dat files.
{"adoption01.dat", -1}, // 追加
{"doctype01.dat", -1},
{"tests1.dat", -1},
{"tests2.dat", -1},
}
コアとなるコードの解説
このコミットの核となる変更は、HTMLパーサーがHTML、MathML、SVGといった異なるマークアップ言語の要素を、それぞれの「名前空間」に基づいて区別し、適切にDOMツリーに組み込む能力を獲得した点にあります。
-
Node
構造体のNamespace
フィールド:- これは、パースされた各要素ノードがどの名前空間に属するかを明示的に保持するための最も重要な変更です。これにより、パーサーは単にタグ名だけでなく、そのタグがHTML、SVG、MathMLのいずれの文脈で出現したかを識別できるようになります。例えば、
<title>
タグはHTMLの<title>
とSVGの<title>
で意味が異なるため、このNamespace
フィールドがその区別を可能にします。
- これは、パースされた各要素ノードがどの名前空間に属するかを明示的に保持するための最も重要な変更です。これにより、パーサーは単にタグ名だけでなく、そのタグがHTML、SVG、MathMLのいずれの文脈で出現したかを識別できるようになります。例えば、
-
foreign.go
とbreakout
マップ:foreign.go
で定義されたbreakout
マップは、HTML5のパーシング仕様における「外部コンテンツからのブレイクアウト」ルールを実装するためのものです。これは、SVGやMathMLの内部に特定のHTMLタグ(例:<body>
,<div>
,<table>
など)が出現した場合、それらのタグは外部コンテンツの一部ではなく、通常のHTMLコンテンツとして扱われるべきであるというルールです。このマップは、そのようなブレイクアウトを引き起こすタグを効率的に識別するために使用されます。
-
parse.go
における挿入モードの遷移ロジック:addElement
関数が親ノードの名前空間を継承するように変更されたことで、DOMツリー構築時に名前空間情報が正しく伝播されるようになりました。resetInsertionMode
関数は、パーサーがスタックを巻き戻す際に、現在のノードが名前空間を持つ(つまり外部コンテンツ内にある)場合に、自動的にinForeignContentIM
に切り替わるように修正されました。これは、外部コンテンツのパースルールが適用されるべき状況を正確に検出するために重要です。inBodyIM
におけるmath
およびsvg
タグの特殊なハンドリングは、HTMLコンテンツから外部コンテンツへの「入り口」を定義しています。これらのタグが検出されると、パーサーは名前空間を設定し、直ちにinForeignContentIM
に遷移します。これにより、以降のトークンはSVGやMathMLのパースルールに従って処理される準備が整います。- 新しく導入された
inForeignContentIM
は、外部コンテンツ内でのトークン処理を専門に行います。このモードでは、コメントは通常通り追加され、開始タグはbreakout
マップをチェックし、必要に応じて名前空間固有の調整(TODOコメントで示されている)が行われた上でノードが追加されます。このモードは、外部コンテンツの構文規則とセマンティクスを尊重しながらDOMツリーを構築するために不可欠です。
これらの変更は、Goのhtml
パッケージが、より複雑なウェブコンテンツ(特にHTML5で導入されたSVGやMathMLの埋め込み)を正確に解析し、標準に準拠したDOMツリーを生成するための重要な一歩となります。
関連リンク
- HTML5仕様 (W3C Recommendation): https://www.w3.org/TR/html5/
- 特に「12.2.5 The tree construction stage」および「12.2.5.5 The rules for parsing tokens in foreign content」セクションが関連します。
- SVG 1.1 (Second Edition) Specification: https://www.w3.org/TR/SVG11/
- MathML 3.0 (Second Edition) Specification: https://www.w3.org/TR/MathML3/
- Go言語の
html
パッケージのドキュメント: https://pkg.go.dev/golang.org/x/net/html (コミット当時のパスはsrc/pkg/html
ですが、現在はgolang.org/x/net/html
に移動しています)
参考にした情報源リンク
- W3C HTML5 Parsing Algorithm: https://html.spec.whatwg.org/multipage/parsing.html (WHATWG版のHTML Living Standardも参照)
- XML Namespaces: https://www.w3.org/TR/REC-xml-names/
- Go言語の公式リポジトリ: https://github.com/golang/go