Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 11258] ファイルの概要

このコミットは、Go言語のHTMLパーサー(src/pkg/htmlパッケージ)における、外部コンテンツ(Foreign Content)のパース処理に関するバグ修正です。具体的には、SVGやMathMLといった外部コンテンツ内でHTMLの統合点(Integration Point)を持つ要素が適切に処理されず、誤って外部コンテンツモードから抜け出してしまう問題を解決します。これにより、HTML5のパース仕様に準拠し、特定の不正なマークアップが正しく解釈されるようになります。

コミット

commit b4829c1de6ffd8581c40932da7a57dcfdd0610fb
Author: Nigel Tao <nigeltao@golang.org>
Date:   Thu Jan 19 17:41:10 2012 +1100

    html: in foreign content, check for HTML integration points in breakout
    elements.
    
    Pass tests10.dat, test 33:
    <!DOCTYPE html><svg><desc><svg><ul>a
    
    | <!DOCTYPE html>
    | <html>
    |   <head>
    |   <body>
    |     <svg svg>
    |       <svg desc>
    |         <svg svg>
    |         <ul>
    |           "a"
    
    Also pass test 34:
    <!DOCTYPE html><p><svg><desc><p>
    
    R=andybalholm, dsymonds
    CC=golang-dev
    https://golang.org/cl/5536048

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/b4829c1de6ffd8581c40932da7a57dcfdd0610fb

元コミット内容

html: in foreign content, check for HTML integration points in breakout
elements.

Pass tests10.dat, test 33:
<!DOCTYPE html><svg><desc><svg><ul>a

| <!DOCTYPE html>
| <html>
|   <head>
|   <body>
|     <svg svg>
|       <svg desc>
|         <svg svg>
|         <ul>
|           "a"

Also pass test 34:
<!DOCTYPE html><p><svg><desc><p>

R=andybalholm, dsymonds
CC=golang-dev
https://golang.org/cl/5536048

変更の背景

このコミットは、HTML5のパース仕様における「外部コンテンツ(Foreign Content)」の取り扱いに関するバグを修正するために導入されました。HTMLドキュメント内にSVG(Scalable Vector Graphics)やMathML(Mathematical Markup Language)のようなXMLベースのコンテンツが埋め込まれる場合、HTMLパーサーは通常のHTMLパースモードから「外部コンテンツモード」に切り替わります。このモードでは、XMLの厳密なルール(大文字・小文字の区別、閉じタグの必須など)が適用されます。

問題は、外部コンテンツ内で特定のHTML要素(例えば <ul><p>)が出現した場合に、パーサーが外部コンテンツモードからHTMLモードに「ブレイクアウト(抜け出し)」する際の挙動にありました。HTML5の仕様では、外部コンテンツ内に埋め込まれたHTML要素が「HTML統合点(HTML Integration Point)」を持つ場合、その要素は外部コンテンツの一部としてではなく、HTMLとして適切にパースされるべきです。しかし、GoのHTMLパーサーは、この統合点のチェックが不十分であったため、<ul><p> のような要素がSVGやMathMLの内部に現れた際に、意図せず外部コンテンツモードから抜け出してしまい、結果としてDOMツリーが不正に構築されるというバグがありました。

コミットメッセージに記載されている tests10.dat, test 33test 34 は、この問題を再現する具体的なテストケースです。

  • <!DOCTYPE html><svg><desc><svg><ul>a このケースでは、SVG要素の内部に <ul> 要素がネストされています。本来であれば、<ul> はHTML統合点を持つ要素として、SVGの外部コンテンツモード内でHTMLとしてパースされ、DOMツリーに正しく組み込まれるべきです。しかし、バグのあるパーサーでは、<ul> が出現した時点でSVGのコンテキストから抜け出してしまい、意図しないDOM構造が生成されていました。

  • <!DOCTYPE html><p><svg><desc><p> 同様に、SVG要素の内部に <p> 要素がネストされているケースです。これも <ul> と同様に、HTML統合点を持つ要素として適切に処理される必要があります。

このコミットは、これらのテストケースをパスするようにパーサーのロジックを修正し、HTML5の仕様に準拠した外部コンテンツのパースを実現することを目的としています。

前提知識の解説

外部コンテンツ (Foreign Content)

HTML5において「外部コンテンツ」とは、HTMLの仕様自体には含まれないが、HTMLドキュメント内に埋め込むことができるXML名前空間を持つ要素のことを指します。最も一般的な外部コンテンツは以下の2つです。

  • SVG (Scalable Vector Graphics): ベクターグラフィックスを記述するためのXMLベースの言語です。HTML内に直接 <svg> タグを使って埋め込むことができます。
  • MathML (Mathematical Markup Language): 数学的な表記を記述するためのXMLベースの言語です。HTML内に直接 <math> タグを使って埋め込むことができます。

これらの外部コンテンツは、HTMLとは異なるパースルールとDOM構造を持ちます。HTMLパーサーは、<svg><math> タグを検出すると、通常のHTMLパースモードから「外部コンテンツモード」に切り替わり、より厳密なXMLのパース規則に従って内部のコンテンツを処理します。

HTML統合点 (HTML Integration Point)

HTML統合点とは、外部コンテンツ(SVGやMathML)の内部に、特定のHTML要素を埋め込むことを許可し、かつその要素をHTMLとしてパースすべき場所を示す概念です。HTML5の仕様では、一部のSVG要素(例: <foreignObject>, <desc>, <title>) やMathML要素(例: <annotation-xml>) は、その子要素としてHTMLコンテンツを受け入れることができます。これらの要素がHTML統合点として機能することで、外部コンテンツとHTMLコンテンツがシームレスに混在できるようになります。

例えば、SVGの <desc> 要素は、SVGグラフィックの説明をHTML形式で記述するために使用できます。この場合、<desc> の内部に <ul><p> といったHTML要素を記述しても、それらはSVGの一部としてではなく、通常のHTML要素としてパースされ、DOMツリーに組み込まれることが期待されます。

ブレイクアウト要素 (Breakout Elements) とパースモードの切り替え

HTMLパーサーは、外部コンテンツモードでパース中に特定の条件を満たすと、通常のHTMLパースモードに「ブレイクアウト(抜け出し)」します。これは、外部コンテンツが終了した場合や、外部コンテンツの内部にHTML統合点を持たないHTML要素が誤って出現した場合などに発生します。

しかし、HTML統合点を持つ要素(例: SVGの <desc> の内部に現れる <ul>)の場合、パーサーは外部コンテンツモードを維持しつつ、そのHTML統合点の子要素をHTMLとしてパースする必要があります。もし、この統合点のチェックが不十分だと、パーサーは誤って外部コンテンツモードから抜け出してしまい、結果としてDOMツリーが不正になる可能性があります。

このコミットは、まさにこの「外部コンテンツ内でHTML統合点を持つ要素が出現した際のブレイクアウト挙動」を修正し、HTML5の仕様に準拠させることを目的としています。

技術的詳細

HTML5のパースアルゴリズムは、非常に複雑で状態遷移に基づいています。パーサーは、現在の要素のコンテキスト(HTML、SVG、MathMLなど)に応じて、異なるパースルールを適用します。

Go言語のHTMLパーサー(src/pkg/html/parse.go)では、parseForeignContent 関数が外部コンテンツのパースを担当しています。この関数は、外部コンテンツモード中にトークンを処理し、DOMツリーを構築します。

外部コンテンツモードからのブレイクアウトは、通常、以下のいずれかの条件で発生します。

  1. 外部コンテンツの終了タグ: 例えば </svg></math> が現れた場合。
  2. 特定のHTML要素の出現: 外部コンテンツの内部に、HTML統合点を持たないHTML要素(例: <div> がSVGのルート直下に現れるなど)が出現した場合。この場合、パーサーは外部コンテンツモードを終了し、HTMLモードに戻ってその要素をパースしようとします。
  3. パースエラー: 外部コンテンツがXMLとして整形式でない場合など。

このコミットが修正したのは、2番目のケース、特に「HTML統合点を持つ要素」の扱いです。修正前のコードでは、外部コンテンツモード中に breakout[p.tok.Data]true となる要素(つまり、HTMLモードへのブレイクアウトを引き起こす可能性のある要素)が検出された際、その要素がHTML統合点を持つかどうかを適切にチェックしていませんでした。

具体的には、parseForeignContent 関数内で、ブレイクアウト要素が検出された際に、現在のオープン要素スタック(p.oe)を遡り、HTML名前空間の要素(p.oe[i].Namespace == "")が見つかった場合にブレイクアウトしていました。しかし、HTML統合点を持つ外部コンテンツ要素(例: SVGの <desc>)は、それ自体はHTML名前空間ではありませんが、その内部にHTMLコンテンツを許容します。したがって、p.oe[i].Namespace == "" という条件だけでは不十分でした。

このコミットでは、htmlIntegrationPoint(p.oe[i]) という新しい条件が追加されました。これは、現在のオープン要素がHTML統合点であるかどうかをチェックするものです。これにより、外部コンテンツモード中にブレイクアウト要素が検出されても、それがHTML統合点を持つ要素の内部にある場合は、外部コンテンツモードを維持し、HTML統合点の子要素としてHTMLを正しくパースし続けることができるようになりました。

この修正により、HTML5の仕様に厳密に準拠したパースが可能となり、特にSVGやMathML内に埋め込まれたHTMLコンテンツが意図通りにDOMツリーに反映されるようになります。

コアとなるコードの変更箇所

src/pkg/html/parse.gosrc/pkg/html/parse_test.go の2つのファイルが変更されています。

diff --git a/src/pkg/html/parse.go b/src/pkg/html/parse.go
index 43c04727ab..04f4ae7533 100644
--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -1713,8 +1713,8 @@ func parseForeignContent(p *parser) bool {
 		}
 		if breakout[p.tok.Data] {
 			for i := len(p.oe) - 1; i >= 0; i-- {
-				// TODO: HTML, MathML integration points.
-				if p.oe[i].Namespace == "" {
+				// TODO: MathML integration points.
+				if p.oe[i].Namespace == "" || htmlIntegrationPoint(p.oe[i]) {
 					p.oe = p.oe[:i+1]
 					break
 				}
diff --git a/src/pkg/html/parse_test.go b/src/pkg/html/parse_test.go
index c929c25772..1528dffaaf 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", 33},
+		{"tests10.dat", 35},
 	}
 	for _, tf := range testFiles {
 		f, err := os.Open("testdata/webkit/" + tf.filename)

コアとなるコードの解説

src/pkg/html/parse.go の変更

変更の中心は parseForeignContent 関数内の以下の行です。

-				if p.oe[i].Namespace == "" {
+				if p.oe[i].Namespace == "" || htmlIntegrationPoint(p.oe[i]) {
  • parseForeignContent 関数: この関数は、HTMLパーサーがSVGやMathMLなどの外部コンテンツをパースしている際に呼び出されます。外部コンテンツモードでのトークン処理とDOMツリー構築のロジックが含まれています。
  • breakout[p.tok.Data]: これは、現在のトークン(p.tok)のデータ(要素名)が、外部コンテンツモードからHTMLモードへのブレイクアウトを引き起こす可能性のある要素であるかどうかを示すブール値のマップまたはセットです。例えば、SVG内で <html><body> のようなHTMLのルート要素が出現した場合などが該当します。
  • for i := len(p.oe) - 1; i >= 0; i--: このループは、現在のオープン要素スタック(p.oe)を逆順に(最も最近開かれた要素から)走査しています。p.oe は、現在開いている要素のスタックであり、DOMツリーの階層構造を反映しています。
  • p.oe[i].Namespace == "": 修正前の条件です。これは、スタック上の要素 p.oe[i] がHTML名前空間に属しているかどうかをチェックしています。もしHTML名前空間の要素が見つかれば、そこまでスタックを巻き戻し(p.oe = p.oe[:i+1])、外部コンテンツモードから抜け出す(break)というロジックでした。
  • htmlIntegrationPoint(p.oe[i]): 修正で追加された新しい条件です。これは、スタック上の要素 p.oe[i] がHTML統合点であるかどうかをチェックする関数呼び出しです。
    • htmlIntegrationPoint 関数(このdiffには含まれていませんが、同じパッケージ内に定義されていると推測されます)は、与えられた要素がSVGの <desc>, <title>, <foreignObject> やMathMLの <annotation-xml> など、HTMLコンテンツを子として受け入れることができる要素であるかを判定します。

変更の意図: 修正前のコードでは、外部コンテンツモード中にブレイクアウト要素(例: <ul>)が検出された場合、パーサーはオープン要素スタックを遡り、最初にHTML名前空間の要素が見つかった時点で外部コンテンツモードを終了していました。しかし、SVGの <desc> のようなHTML統合点を持つ要素の内部に <ul> がある場合、<desc> 自体はHTML名前空間ではありません。そのため、p.oe[i].Namespace == "" の条件では <desc> をHTML統合点として認識できず、誤ってその親のHTML要素まで遡ってしまい、SVGコンテキストから不適切に抜け出してしまっていました。

新しい条件 || htmlIntegrationPoint(p.oe[i]) を追加することで、パーサーはオープン要素スタックを遡る際に、HTML名前空間の要素だけでなく、HTML統合点を持つ外部コンテンツ要素もチェックするようになりました。これにより、ブレイクアウト要素がHTML統合点を持つ要素の内部にある場合でも、パーサーはHTML統合点の直前までスタックを巻き戻し、外部コンテンツモードを維持したまま、HTML統合点の子要素としてHTMLを正しくパースできるようになります。

src/pkg/html/parse_test.go の変更

-		{"tests10.dat", 33},
+		{"tests10.dat", 35},

この変更は、テストファイルの期待値の更新です。tests10.dat はWebkitのテストスイートの一部であり、HTML5のパース挙動を検証するためのものです。

  • 33 から 35 への変更は、tests10.dat ファイル内のテストケースの総数が、この修正によって増えたか、あるいは特定のテストケースのインデックスが変更されたことを示唆しています。コミットメッセージに「Pass tests10.dat, test 33」とあることから、元々テスト33が失敗していたが、修正によってパスするようになり、さらにテストケースが追加されたか、テストスイートの構成が変更された可能性が高いです。

このテストの更新は、コードの変更が意図した通りに動作し、関連するテストケースを正しく処理できるようになったことを確認するためのものです。

関連リンク

  • Go CL 5536048: https://golang.org/cl/5536048 (Web検索では直接的な情報が見つかりませんでしたが、コミットメッセージに記載されているため含めます。)

参考にした情報源リンク