[インデックス 13167] ファイルの概要
このコミットは、Go言語の実験的なHTMLパーサーである exp/html
パッケージにおいて、HTML5仕様に準拠するように外部コンテンツ(Foreign Content)のパースロジックを調整するものです。具体的には、冗長なチェックの削除、ヌルバイトの無視、<font>
タグの特殊な扱い、MathMLテキスト統合ポイントの考慮、そして自己終了タグの処理に関する修正が含まれています。
コミット
commit c23041efd99bc2cc7c6888ea6f6a83f5e13f8326
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Fri May 25 10:03:59 2012 +1000
exp/html: adjust parseForeignContent to match spec
Remove redundant checks for integration points.
Ignore null bytes in text.
Don't break out of foreign content for a <font> tag unless it
has a color, face, or size attribute.
Check for MathML text integration points when breaking out of
foreign content.
Pass two new tests.
R=nigeltao
CC=golang-dev
https://golang.org/cl/6256045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c23041efd99bc2cc7c6888ea6f6a83f5e13f8326
元コミット内容
このコミットの元の内容は以下の通りです。
exp/html: adjust parseForeignContent to match spec
- 統合ポイントに関する冗長なチェックを削除。
- テキスト内のヌルバイトを無視。
<font>
タグがcolor
,face
,size
のいずれかの属性を持たない限り、外部コンテンツから抜け出さないように変更。- 外部コンテンツから抜け出す際に、MathMLテキスト統合ポイントをチェックするように変更。
- 2つの新しいテストがパスするようになった。
変更の背景
この変更の背景には、ウェブブラウザがHTML5の仕様に厳密に従ってドキュメントをパースする必要があるという要件があります。特に、HTMLドキュメント内にSVG (Scalable Vector Graphics) やMathML (Mathematical Markup Language) のような「外部コンテンツ(Foreign Content)」が埋め込まれている場合、そのパースルールは通常のHTMLとは異なります。
HTML5のパース仕様は非常に複雑であり、ブラウザ間の互換性を保証するためには、その細部に至るまで正確に実装することが求められます。このコミットは、exp/html
パッケージがこれらの複雑なルール、特に外部コンテンツからの脱出条件や、特定の要素(例: <font>
タグ)の振る舞い、そしてテキストデータ内の特殊文字(例: ヌルバイト)の処理に関して、仕様との乖離を修正することを目的としています。
以前の実装では、外部コンテンツからの脱出条件が仕様と異なっていたり、ヌルバイトの処理が不適切であったり、<font>
タグの扱いが厳しすぎたりする問題があったと考えられます。これらの不一致は、特定のHTMLドキュメントがGoのパーサーとブラウザで異なるDOMツリーを生成する可能性があり、結果としてウェブアプリケーションの互換性問題を引き起こす原因となり得ます。このコミットは、これらの問題を解消し、パーサーの堅牢性と正確性を向上させるために行われました。
前提知識の解説
このコミットを理解するためには、以下のHTML5パースに関する前提知識が必要です。
-
HTML5パースアルゴリズム:
- HTML5のパースは、トークン化(Tokenization)とツリー構築(Tree Construction)の2つの主要なフェーズに分かれます。
- トークン化: 入力ストリーム(HTML文字列)を、開始タグ、終了タグ、テキスト、コメントなどの「トークン」に分解するプロセスです。
- ツリー構築: トークナイザーから受け取ったトークンに基づいて、DOM(Document Object Model)ツリーを構築するプロセスです。このフェーズでは、「挿入モード(Insertion Mode)」という概念が重要になります。挿入モードは、現在のパース状態に応じて、どのトークンをどのように処理するかを決定します。
- 要素スタック(List of active elements): 現在開いている要素(まだ対応する終了タグが来ていない要素)を追跡するためのスタック構造です。DOMツリーの階層構造を管理するために使用されます。
-
外部コンテンツ(Foreign Content):
- HTMLドキュメント内に埋め込まれた、HTMLとは異なるXML名前空間を持つコンテンツを指します。主にSVG (Scalable Vector Graphics) とMathML (Mathematical Markup Language) がこれに該当します。
- 外部コンテンツ内では、通常のHTMLのパースルールとは異なるXMLのパースルールが適用されます。例えば、HTMLでは大文字・小文字を区別しないタグ名が、SVGやMathMLでは区別されるなど、厳密なXMLの構文規則が適用されます。
-
統合ポイント(Integration Points):
- 外部コンテンツのパース中に、特定の条件が満たされた場合に、再びHTMLのパースルールに戻る(またはその逆)ための「境界」となるポイントです。
- HTML統合ポイント(HTML integration point): 外部コンテンツ内に特定のHTML要素(例:
<p>
,<a>
など)が出現した場合に、その要素とその子孫をHTMLとしてパースし直すためのポイントです。 - MathMLテキスト統合ポイント(MathML text integration point): MathMLコンテンツ内で、テキストとして扱われるべき特定の要素(例:
<mtext>
,<mn>
,<mo>
など)が出現した場合に、その内部のテキストをHTMLのテキストパースルールで処理するためのポイントです。これは、MathMLの要素がHTMLのテキストノードとして扱われるべき場合に特に重要です。
-
ヌルバイト(Null Byte,
\x00
)の扱い:- HTML5の仕様では、テキストデータ内にヌルバイトが出現した場合、それを無視するか、特定の文字に置き換えるように規定されています。これは、ヌルバイトが文字列の終端を示すために使われることがあり、セキュリティ上の問題(例: 文字列切り詰めによるパス検証の回避)や、予期せぬパースエラーを引き起こす可能性があるためです。HTML5では、ヌルバイトは通常、U+FFFD REPLACEMENT CHARACTERに置き換えられるか、単に無視されます。このコミットでは、単純に無視(削除)する実装が採用されています。
-
<font>
タグの特殊性:<font>
タグはHTML4以前の要素であり、HTML5では非推奨(deprecated)とされています。しかし、後方互換性のためにブラウザはこれをパースする必要があります。- 外部コンテンツ内での
<font>
タグの扱いは、HTML5仕様で特殊なルールが定められています。特に、color
,face
,size
といった属性を持つ<font>
タグは、外部コンテンツからHTMLコンテンツへの「脱出」を引き起こす可能性があります。これは、これらの属性がHTMLのレンダリングに直接影響を与えるため、外部コンテンツの文脈から切り離してHTMLとして処理する必要があるためです。
これらの概念を理解することで、parseForeignContent
関数がなぜ、どのように変更されたのか、そしてそれがHTMLパースの正確性にどのように寄与するのかを深く把握することができます。
技術的詳細
このコミットは、Goの exp/html
パッケージにおけるHTML5パースアルゴリズムの parseForeignContent
関数に焦点を当てています。この関数は、パーサーがSVGやMathMLなどの外部コンテンツを処理しているときに呼び出されます。
変更の技術的な詳細は以下の通りです。
-
ヌルバイトの無視 (
src/pkg/exp/html/parse.go
):TextToken
の処理において、以前はHTML統合ポイントのチェックを行っていましたが、これが削除され、代わりに以下の行が追加されました。p.tok.Data = strings.Replace(p.tok.Data, "\x00", "", -1)
- これは、入力されたテキストデータ (
p.tok.Data
) 内に含まれるすべてのヌルバイト (\x00
) を空文字列に置換することで、実質的にヌルバイトを無視(削除)する処理です。strings.Replace
の最後の引数-1
は、すべての出現箇所を置換することを意味します。これにより、HTML5仕様で規定されているヌルバイトの処理に準拠します。
-
<font>
タグの外部コンテンツからの脱出条件の変更 (src/pkg/exp/html/foreign.go
およびsrc/pkg/exp/html/parse.go
):src/pkg/exp/html/foreign.go
内のbreakout
マップから"font": true,
のエントリが削除されました。breakout
マップは、特定のタグが外部コンテンツ内で出現した場合に、無条件に外部コンテンツから脱出(HTMLパースモードに戻る)させるかどうかを定義していると考えられます。この変更により、<font>
タグは単独では外部コンテンツからの脱出を引き起こさなくなります。
src/pkg/exp/html/parse.go
のStartTagToken
処理において、<font>
タグに対する特別なロジックが追加されました。b := breakout[p.tok.Data] if p.tok.Data == "font" { loop: for _, attr := range p.tok.Attr { switch attr.Key { case "color", "face", "size": b = true break loop } } } if b { // ... 外部コンテンツからの脱出ロジック ... }
- まず、
breakout
マップに基づいてb
の初期値を設定します。 - もし現在のタグが
<font>
であれば、その属性をループでチェックします。 - 属性の中に
color
,face
,size
のいずれかが見つかった場合、b
をtrue
に設定し、ループを抜けます。 - この結果、
<font>
タグがこれらの属性のいずれかを持つ場合にのみ、b
がtrue
となり、その後のif b
ブロックで外部コンテンツからの脱出処理が実行されます。これにより、HTML5仕様の<font>
タグに関する特殊な脱出ルールが正確に実装されます。
- まず、
-
MathMLテキスト統合ポイントの考慮 (
src/pkg/exp/html/parse.go
):StartTagToken
処理における外部コンテンツからの脱出ロジック内で、要素スタック (p.oe
) を遡る際に、以下の条件が追加されました。if n.Namespace == "" || htmlIntegrationPoint(n) || mathMLTextIntegrationPoint(n) { // ... 外部コンテンツからの脱出ロジック ... }
- 以前は
htmlIntegrationPoint(p.oe[i])
のチェックのみでしたが、これにmathMLTextIntegrationPoint(n)
が追加されました。これは、要素スタックを遡る際に、現在の要素がMathMLテキスト統合ポイントである場合も、外部コンテンツからの脱出条件として考慮することを意味します。これにより、MathMLコンテンツ内の特定の要素がHTMLのテキストとして扱われるべき場合に、正しくパースモードが切り替わるようになります。
-
自己終了タグの処理の修正 (
src/pkg/exp/html/parse.go
):StartTagToken
処理の最後に、以下の新しいブロックが追加されました。if p.hasSelfClosingToken { p.oe.pop() p.acknowledgeSelfClosingTag() }
p.hasSelfClosingToken
は、現在のトークンが自己終了タグ(例:<br/>
,<img/>
など)であるかどうかを示すフラグです。- 外部コンテンツ内では、XMLのルールに従い、自己終了タグは要素スタックにプッシュされた後、すぐにポップされる必要があります。このコードは、自己終了タグが検出された場合に、要素スタックからその要素をポップし、自己終了タグが処理されたことをパーサーに通知する (
acknowledgeSelfClosingTag
) ことで、スタックの状態を正しく維持します。これにより、外部コンテンツ内での自己終了タグのパースが仕様に準拠するようになります。
これらの変更は、HTML5の複雑なパース仕様、特に外部コンテンツと統合ポイントに関するルールを正確に実装するために不可欠です。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主に src/pkg/exp/html/parse.go
と src/pkg/exp/html/foreign.go
の2つのファイルに集中しています。
src/pkg/exp/html/foreign.go
--- a/src/pkg/exp/html/foreign.go
+++ b/src/pkg/exp/html/foreign.go
@@ -82,7 +82,6 @@ var breakout = map[string]bool{\
"dt": true,
"em": true,
"embed": true,
- "font": true,
"h1": true,
"h2": true,
"h3": true,
breakout
マップから"font": true,
の行が削除されています。これにより、<font>
タグはデフォルトでは外部コンテンツからの脱出を引き起こさなくなります。
src/pkg/exp/html/parse.go
--- a/src/pkg/exp/html/parse.go
+++ b/src/pkg/exp/html/parse.go
@@ -1785,12 +1785,7 @@ func afterAfterFramesetIM(p *parser) bool {
func parseForeignContent(p *parser) bool {
switch p.tok.Type {
case TextToken:
- // TODO: HTML integration points.
- if p.top().Namespace == "" {
- inBodyIM(p)
- p.resetInsertionMode()
- return true
- }
+ p.tok.Data = strings.Replace(p.tok.Data, "\x00", "", -1)
if p.framesetOK {
p.framesetOK = strings.TrimLeft(p.tok.Data, whitespace) == ""
}
@@ -1801,15 +1796,21 @@ func parseForeignContent(p *parser) bool {
Data: p.tok.Data,
})
case StartTagToken:
- if htmlIntegrationPoint(p.top()) {
- inBodyIM(p)
- p.resetInsertionMode()
- return true
- }
- if breakout[p.tok.Data] {
+ b := breakout[p.tok.Data]
+ if p.tok.Data == "font" {
+ loop:
+ for _, attr := range p.tok.Attr {
+ switch attr.Key {
+ case "color", "face", "size":
+ b = true
+ break loop
+ }
+ }
+ }
+ if b {
for i := len(p.oe) - 1; i >= 0; i-- {
- // TODO: MathML integration points.
- if p.oe[i].Namespace == "" || htmlIntegrationPoint(p.oe[i]) {
+ n := p.oe[i]
+ if n.Namespace == "" || htmlIntegrationPoint(n) || mathMLTextIntegrationPoint(n) {
p.oe = p.oe[:i+1]
break
}
@@ -1833,6 +1834,10 @@ func parseForeignContent(p *parser) bool {
namespace := p.top().Namespace
p.addElement(p.tok.Data, p.tok.Attr)
p.top().Namespace = namespace
+ if p.hasSelfClosingToken {
+ p.oe.pop()
+ p.acknowledgeSelfClosingTag()
+ }
case EndTagToken:
for i := len(p.oe) - 1; i >= 0; i-- {
if p.oe[i].Namespace == "" {
TextToken
の処理で、ヌルバイトを削除するstrings.Replace
が追加され、冗長なHTML統合ポイントのチェックが削除されています。StartTagToken
の処理で、<font>
タグの属性 (color
,face
,size
) に応じて外部コンテンツからの脱出を制御するロジックが追加されています。- 要素スタックを遡る際に、
mathMLTextIntegrationPoint(n)
のチェックが追加され、MathMLテキスト統合ポイントも脱出条件として考慮されるようになっています。 - 自己終了タグ (
p.hasSelfClosingToken
) の処理が追加され、要素スタックの管理が修正されています。
src/pkg/exp/html/testlogs/plain-text-unsafe.dat.log
--- a/src/pkg/exp/html/testlogs/plain-text-unsafe.dat.log
+++ b/src/pkg/exp/html/testlogs/plain-text-unsafe.dat.log
@@ -16,8 +16,8 @@ FAIL "<svg>\x00filler\x00text"
FAIL "<svg>\x00<frameset>"
FAIL "<svg>\x00 <frameset>"
FAIL "<svg>\x00a<frameset>"
-FAIL "<svg>\x00</svg><frameset>"
-FAIL "<svg>\x00 </svg><frameset>"
+PASS "<svg>\x00</svg><frameset>"
+PASS "<svg>\x00 </svg><frameset>"
FAIL "<svg>\x00a</svg><frameset>"
PASS "<svg><path></path></svg><frameset>"
PASS "<svg><p><frameset>"
- ヌルバイトを含む2つのテストケースが
FAIL
からPASS
に変更されています。これは、ヌルバイトの無視処理が正しく機能していることを示しています。
コアとなるコードの解説
parseForeignContent
関数
この関数は、HTMLパーサーが外部コンテンツ(SVGやMathML)の内部をパースしているときに呼び出される主要な関数です。HTML5仕様では、外部コンテンツのパースには通常のHTMLとは異なるルールが適用されますが、特定の条件でHTMLパースモードに「切り替える」必要があります。この切り替えが「外部コンテンツからの脱出」です。
TextToken
の処理
case TextToken:
p.tok.Data = strings.Replace(p.tok.Data, "\x00", "", -1)
// ... (framesetOKのチェックなど、他のテキスト処理)
- 変更前: 以前は、テキストトークンがHTML統合ポイントであるかどうかをチェックし、もしそうであればHTMLパースモードに戻るというロジックがありました。これは冗長であったか、仕様の解釈が不正確であったため削除されました。
- 変更後: 最も重要な変更は、
strings.Replace(p.tok.Data, "\x00", "", -1)
の追加です。これは、テキストデータ内に含まれるすべてのヌルバイト (\x00
) を削除します。HTML5仕様では、ヌルバイトはテキストデータ内で特別な意味を持たず、通常は無視されるか、U+FFFD REPLACEMENT CHARACTERに置き換えられます。この修正により、パーサーはヌルバイトを含む不正な入力に対してより堅牢になり、仕様に準拠した振る舞いをします。
StartTagToken
の処理
このセクションは、開始タグが外部コンテンツ内で検出された場合の処理を定義しており、最も複雑な変更が含まれています。
case StartTagToken:
b := breakout[p.tok.Data]
if p.tok.Data == "font" {
loop:
for _, attr := range p.tok.Attr {
switch attr.Key {
case "color", "face", "size":
b = true
break loop
}
}
}
if b {
for i := len(p.oe) - 1; i >= 0; i-- {
n := p.oe[i]
if n.Namespace == "" || htmlIntegrationPoint(n) || mathMLTextIntegrationPoint(n) {
p.oe = p.oe[:i+1]
break
}
}
inBodyIM(p)
p.resetInsertionMode()
return true
}
// ... (要素の追加、名前空間の設定など)
if p.hasSelfClosingToken {
p.oe.pop()
p.acknowledgeSelfClosingTag()
}
-
<font>
タグの特殊処理:foreign.go
から"font"
がbreakout
マップから削除されたため、デフォルトでは<font>
タグは外部コンテンツからの脱出を引き起こしません。- しかし、このコードブロックでは、もしタグが
<font>
であり、かつcolor
,face
,size
のいずれかの属性を持つ場合、明示的にb
をtrue
に設定します。 b
がtrue
の場合、パーサーは外部コンテンツからの脱出処理(要素スタックを遡り、適切な統合ポイントを見つけてHTMLパースモードに戻る)を実行します。これにより、HTML5仕様で規定されている<font>
タグの特殊な脱出ルールが正確に実装されます。これは、これらの属性がHTMLのレンダリングに直接影響するため、外部コンテンツの文脈から切り離してHTMLとして処理する必要があるという仕様の意図を反映しています。
-
MathMLテキスト統合ポイントのチェック:
- 外部コンテンツから脱出する条件をチェックするループ内で、
mathMLTextIntegrationPoint(n)
が追加されました。 p.oe
は要素スタック(List of active elements)を表します。このループは、スタックを現在の要素からルートに向かって遡り、外部コンテンツからの脱出を引き起こす可能性のある統合ポイントを探します。n.Namespace == ""
はHTML名前空間の要素を意味し、htmlIntegrationPoint(n)
はHTML統合ポイントをチェックします。mathMLTextIntegrationPoint(n)
の追加により、MathMLコンテンツ内の特定の要素(例:<mtext>
,<mn>
,<mo>
など)がHTMLのテキストとして扱われるべき場合に、正しくパースモードが切り替わるようになります。これにより、MathMLとHTMLの間のセマンティックな境界が正確に処理されます。
- 外部コンテンツから脱出する条件をチェックするループ内で、
-
自己終了タグの処理:
if p.hasSelfClosingToken { p.oe.pop(); p.acknowledgeSelfClosingTag() }
のブロックが追加されました。- 外部コンテンツ内では、XMLのルールが適用されるため、自己終了タグ(例:
<path/>
in SVG)は、要素スタックにプッシュされた直後にポップされる必要があります。 - このコードは、トークンが自己終了タグであるとマークされている場合 (
p.hasSelfClosingToken
がtrue
)、要素スタックからその要素をポップし、パーサーに自己終了タグが処理されたことを通知します。これにより、要素スタックが常に正しい状態に保たれ、後続のパースが正確に行われるようになります。
これらの変更は、HTML5の複雑な外部コンテンツパースルールを正確に実装し、GoのHTMLパーサーがより多くの現実世界のHTMLドキュメントを堅牢かつ正確に処理できるようにするために不可欠です。
関連リンク
- HTML Standard - 13.2.5.6 The rules for parsing tokens in foreign content: https://html.spec.whatwg.org/multipage/parsing.html#parsing-html-fragments (このリンクは現在のHTML仕様の該当セクションですが、コミット当時の仕様とは異なる可能性があります。しかし、概念は共通しています。)
- HTML Standard - 13.2.5.1 The "in body" insertion mode: https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody
- Go
exp/html
package documentation (当時のもの): 現在はgolang.org/x/net/html
に統合されています。golang.org/x/net/html
package: https://pkg.go.dev/golang.org/x/net/html
参考にした情報源リンク
- HTML Standard (WHATWG): https://html.spec.whatwg.org/
- MDN Web Docs - HTML elements reference: https://developer.mozilla.org/en-US/docs/Web/HTML/Element
- MDN Web Docs - SVG: https://developer.mozilla.org/en-US/docs/Web/SVG
- MDN Web Docs - MathML: https://developer.mozilla.org/en-US/docs/Web/MathML
- Go Programming Language Documentation: https://go.dev/doc/
- Go
strings
package documentation: https://pkg.go.dev/strings - Gerrit Change-ID for this commit: https://golang.org/cl/6256045 (これはコミットメッセージに記載されているリンクであり、変更の詳細なレビュー履歴が含まれている可能性があります。)