[インデックス 10863] ファイルの概要
このコミットは、Go言語のhtml
パッケージにおけるHTMLパーサーの挙動を改善するものです。具体的には、HTMLドキュメント内でSVGやMathMLなどの「外部コンテンツ(Foreign Content)」が埋め込まれている場合に、その内部に存在するテキストノードのパース処理を正しくハンドリングするように修正しています。これにより、HTML5のパース仕様にさらに準拠し、より複雑なドキュメント構造を正確に解析できるようになります。
コミット
commit 18e844147693b0346dc813fbc05a8beb7a210f2f
Author: Nigel Tao <nigeltao@golang.org>
Date: Mon Dec 19 12:20:00 2011 +1100
html: handle text nodes in foreign content.
Passes tests10.dat, test 6:
<!DOCTYPE html><body><table><svg><g>foo</g></svg></table>
| <!DOCTYPE html>
| <html>
| <head>
| <body>
| <svg svg>
| <svg g>
| "foo"
| <table>
Also pass tests through test 12:
<!DOCTYPE html><body><table><caption><svg><g>foo</g><g>bar</g></svg><p>baz</caption></table>
R=andybalholm
CC=golang-dev
https://golang.org/cl/5495061
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/18e844147693b0346dc813fbc05a8beb7a210f2f
元コミット内容
html: handle text nodes in foreign content.
このコミットは、外部コンテンツ(Foreign Content)内のテキストノードを処理するようにHTMLパーサーを修正します。
テストケースtests10.dat
, test 6:
<!DOCTYPE html><body><table><svg><g>foo</g></svg></table>
上記のHTMLが以下のようにパースされることを確認します。
| <!DOCTYPE html>
| <html>
| <head>
| <body>
| <svg svg>
| <svg g>
| "foo"
| <table>
また、テスト12までの他のテストもパスすることを確認します。
<!DOCTYPE html><body><table><caption><svg><g>foo</g><g>bar</g></svg><p>baz</caption></table>
変更の背景
HTML5の仕様では、HTMLドキュメント内にSVG (Scalable Vector Graphics) や MathML (Mathematical Markup Language) といったXML名前空間の要素が埋め込まれることがあります。これらは「外部コンテンツ(Foreign Content)」と呼ばれ、通常のHTML要素とは異なるパースルールが適用されます。
このコミット以前のGoのhtml
パッケージのパーサーは、外部コンテンツ内のテキストノードの扱いが不完全でした。具体的には、SVG要素の内部にテキストデータが存在する場合、そのテキストが正しくDOMツリーに追加されない、あるいは誤ったコンテキストで処理される可能性がありました。
提示されたテストケース<!DOCTYPE html><body><table><svg><g>foo</g></svg></table>
では、<svg>
要素内に<g>
要素があり、その中にテキスト「foo」が含まれています。この「foo」はSVGの文脈におけるテキストノードとして扱われるべきですが、従来のパーサーではこのテキストが適切に処理されず、DOMツリーに反映されない、または予期せぬ場所に挿入されるといった問題が発生していました。
この問題は、HTML5の厳密なパース仕様に準拠し、ウェブブラウザと同様の正確なDOMツリーを構築するために解決される必要がありました。特に、SVGやMathMLを動的に操作するJavaScriptアプリケーションなどでは、パーサーが生成するDOMツリーの正確性が非常に重要になります。
前提知識の解説
HTML5パースアルゴリズム
HTML5のパースアルゴリズムは、非常に複雑で詳細な状態機械として定義されています。これは、エラー耐性(タグの閉じ忘れや不正なネストなどがあっても可能な限りDOMツリーを構築する)と、ブラウザ間の互換性(どのブラウザでも同じHTMLに対して同じDOMツリーを生成する)を保証するために設計されています。
パースは大きく分けて「トークン化(Tokenization)」と「ツリー構築(Tree Construction)」の2段階で行われます。
- トークン化: 入力されたHTML文字列を、タグ、属性、テキスト、コメントなどの「トークン」に分解します。
- ツリー構築: トークンストリームを読み込み、それに基づいてDOMツツリーを構築します。この段階で「挿入モード(Insertion Mode)」という概念が非常に重要になります。
挿入モード (Insertion Mode)
挿入モードは、現在のパーサーの状態と、次に処理すべきトークンに基づいて、DOMツリーにノードを挿入する方法を決定するものです。HTML5の仕様には多数の挿入モードが定義されており、例えばin body
モード、in head
モード、in table
モードなどがあります。パーサーは、特定のタグを検出したり、特定の条件が満たされたりすると、現在の挿入モードを切り替えます。
外部コンテンツ (Foreign Content)
HTMLドキュメント内に、HTML名前空間ではない要素が埋め込まれることがあります。最も一般的なのはSVG (Scalable Vector Graphics) と MathML (Mathematical Markup Language) です。これらの要素は「外部コンテンツ」と呼ばれ、通常のHTML要素とは異なるパースルールが適用されます。
外部コンテンツ内では、HTMLのパースルールの一部が無効になったり、異なる意味を持つようになったりします。例えば、HTMLでは自己終了タグ(<br/>
など)は特殊なケースですが、XMLベースのSVGやMathMLでは一般的な概念です。また、CDATAセクションの扱いなども異なります。
in foreign content
挿入モード
パーサーがSVGやMathMLの要素の開始タグを検出すると、挿入モードは「in foreign content
」に切り替わります。このモードでは、テキストノードの処理、特定のタグの扱い、属性のパースなどにおいて、通常のHTMLとは異なるルールが適用されます。
このコミットの文脈では、in foreign content
モードにおいてテキストノードが適切に処理されていなかったことが問題でした。HTML5の仕様では、外部コンテンツ内のテキストノードは、その外部コンテンツの名前空間に属するテキストノードとしてDOMツリーに追加されるべきです。
技術的詳細
このコミットの核心は、src/pkg/html/parse.go
ファイル内のinForeignContentIM
関数に、TextToken
(テキストノード)を処理するための新しいケースを追加したことです。
inForeignContentIM
関数は、パーサーが外部コンテンツ(SVGやMathMLなど)の内部にいるときに使用される挿入モードハンドラです。この関数は、次に現れるトークンの種類(p.tok.Type
)に基づいて異なる処理を行います。
変更前は、TextToken
がinForeignContentIM
モードで検出された場合、明示的な処理が定義されていませんでした。これは、テキストノードが無視されるか、あるいはデフォルトのフォールバックロジックによって誤って処理される可能性を意味していました。
追加されたコードは以下の通りです。
case TextToken:
// TODO: HTML integration points.
if p.top().Namespace == "" {
inBodyIM(p)
p.resetInsertionMode()
return true
}
if p.framesetOK {
p.framesetOK = strings.TrimLeft(p.tok.Data, whitespace) == ""
}
p.addText(p.tok.Data)
このコードブロックは、TextToken
が検出された際の処理を定義しています。
-
p.top().Namespace == ""
のチェック: これは「HTML統合ポイント(HTML integration points)」と呼ばれるHTML5の特殊なルールに関連しています。一部のSVG/MathML要素(例えば<foreignObject>
)は、その内部にHTMLコンテンツを埋め込むことを許可しており、その場合、一時的にHTMLのパースルールに戻る必要があります。p.top().Namespace == ""
は、現在の要素がHTML名前空間に属しているかどうかをチェックしています。もしそうであれば、inBodyIM(p)
を呼び出して挿入モードをin body
に切り替え、HTMLのパースルールでテキストを処理します。その後、p.resetInsertionMode()
で元の挿入モードに戻ります。 -
p.framesetOK
の更新:framesetOK
は、HTMLのframeset
要素に関連するフラグで、特定の状況下でframeset
要素が許可されるかどうかを追跡します。テキストノードが空白文字のみで構成されている場合、framesetOK
はtrue
のままですが、それ以外のテキストが含まれる場合はfalse
に設定されます。これは、HTML5のパース仕様におけるframeset
要素の制約を反映したものです。 -
p.addText(p.tok.Data)
: これがこのコミットの最も重要な部分です。現在のトークンがテキストノードであり、かつHTML統合ポイントの条件に合致しない場合(つまり、純粋な外部コンテンツ内のテキストである場合)、p.addText(p.tok.Data)
が呼び出されます。この関数は、トークンのデータ(テキストコンテンツ)を現在のノードの子としてDOMツリーに追加します。これにより、外部コンテンツ内のテキストが正しくパースされ、DOMツリーに反映されるようになります。
テストファイルsrc/pkg/html/parse_test.go
の変更は、tests10.dat
の期待されるテスト結果の行数を6
から13
に更新しています。これは、テキストノードが正しくパースされるようになったことで、生成されるDOMツリーの表現(おそらくはテキストノードの追加によるもの)が変化したことを示しています。
コアとなるコードの変更箇所
src/pkg/html/parse.go
--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -1585,6 +1585,17 @@ func afterAfterFramesetIM(p *parser) bool {
// Section 12.2.5.5.
func inForeignContentIM(p *parser) bool {
switch p.tok.Type {
+ case TextToken:
+ // TODO: HTML integration points.
+ if p.top().Namespace == "" {
+ inBodyIM(p)
+ p.resetInsertionMode()
+ return true
+ }
+ if p.framesetOK {
+ p.framesetOK = strings.TrimLeft(p.tok.Data, whitespace) == ""
+ }
+ p.addText(p.tok.Data)
case CommentToken:
p.addChild(&Node{
Type: CommentNode,
src/pkg/html/parse_test.go
--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -173,7 +173,7 @@ func TestParser(t *testing.T) {
{"tests4.dat", -1},
{"tests5.dat", -1},
{"tests6.dat", 36},
- {"tests10.dat", 6},
+ {"tests10.dat", 13},
}
for _, tf := range testFiles {
f, err := os.Open("testdata/webkit/" + tf.filename)
コアとなるコードの解説
src/pkg/html/parse.go
の変更
inForeignContentIM
関数は、HTML5パースアルゴリズムの「外部コンテンツ内(in foreign content)」挿入モードを実装しています。このモードは、SVGやMathMLなどのXML名前空間の要素がHTMLドキュメント内に埋め込まれている場合にアクティブになります。
追加されたcase TextToken:
ブロックは、パーサーがテキストトークン(つまり、実際のテキストコンテンツ)を検出したときの動作を定義します。
-
if p.top().Namespace == ""
: これは、現在の要素がHTML名前空間に属しているかどうかをチェックします。HTML5の仕様には「HTML統合ポイント(HTML integration points)」という概念があり、特定の外部コンテンツ要素(例:<svg:foreignObject>
)の内部では、一時的にHTMLのパースルールに戻る必要があります。- もし現在の要素がHTML名前空間に属している場合(
p.top().Namespace == ""
がtrue
)、それはHTML統合ポイントである可能性が高いため、パーサーはinBodyIM(p)
を呼び出して挿入モードを「in body」に切り替えます。これにより、テキストは通常のHTMLのテキストとして処理されます。 - テキスト処理後、
p.resetInsertionMode()
が呼び出され、パーサーは元の挿入モード(この場合はin foreign content
)に戻ります。 return true
は、このトークンの処理が完了し、次のトークンに進むべきであることを示します。
- もし現在の要素がHTML名前空間に属している場合(
-
if p.framesetOK { p.framesetOK = strings.TrimLeft(p.tok.Data, whitespace) == "" }
:framesetOK
は、HTMLの<frameset>
要素が許可されるかどうかを追跡する内部フラグです。HTML5のパース仕様では、特定の状況下で<frameset>
要素が許可されなくなります。この行は、テキストノードが空白文字のみで構成されている場合(strings.TrimLeft(p.tok.Data, whitespace) == ""
)、framesetOK
フラグはtrue
のままですが、それ以外の非空白文字が含まれるテキストが検出された場合、framesetOK
はfalse
に設定されます。これは、非空白文字のテキストが検出された場合、<frameset>
要素の挿入が許可されなくなるという仕様に準拠するためのものです。 -
p.addText(p.tok.Data)
: これがこのコミットの主要な修正点です。上記のHTML統合ポイントの条件に合致せず、かつテキストトークンが検出された場合、この行が実行されます。p.addText()
メソッドは、現在のトークンのデータ(つまり、テキストコンテンツ)を、現在開いている要素(外部コンテンツ要素)の子としてDOMツリーに追加します。これにより、SVGやMathMLなどの外部コンテンツ内に存在するテキストが、正しくDOMツードにテキストノードとして表現されるようになります。
src/pkg/html/parse_test.go
の変更
テストファイルでは、tests10.dat
に対する期待される結果の行数が6
から13
に変更されています。これは、tests10.dat
に含まれるHTMLスニペットが、このコミットによってテキストノードが正しくパースされるようになった結果、生成されるDOMツリーの文字列表現(おそらくはデバッグ出力や比較用の形式)が長くなったことを示しています。具体的には、テキストノードが以前は無視されていたか、不適切に扱われていたために出力に含まれていなかったものが、今回の修正によって正しく出力されるようになったため、行数が増加したと考えられます。
関連リンク
- HTML Standard - 13.2.6.4.1 The "in foreign content" insertion mode: https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inforeigncontent
- HTML Standard - 13.2.6.4.2 HTML integration points: https://html.spec.whatwg.org/multipage/parsing.html#html-integration-points
- Go
html
パッケージのドキュメント: https://pkg.go.dev/golang.org/x/net/html
参考にした情報源リンク
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQF5gdcRJR6I1AqvBIciiGtUw-PZDUN7RLgxb_1F0XJx1wpg96i2j9DOlzhuVh3Bf7JW48b9_Jm9fHRmZOJOLdhlB7vInFziaCifdJ2-4UPwRuu1gVsq43WKlMKCYZ2SluIah40JNzxZtIzIoqLshjhABtpqnA==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFq9YTHswcgN7BIQgdw1Y2T4KjfbARmv6tZ33FbumWOH7f5ieaq_5KeGser0if9wHhiwqvRUHhTS6eD3y40H9smGAp0Ks55M8Tg1IdxEprVLW2c2-ag-TUdGyp7TfE7nBuuO2YCGbE1tBuczbnehw==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHghRMA-fAcAmwSJF5fqGwvXojdIRGNikgauUS8yNz3iyDRS8VpWPe0HCsWiuNf3CESB4xzAAMXfJ7Iz13z5tSStNiLmAoOdgEelt-UHO9XvlvVWPhXklvqyfc=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGeLTJWxwKo60-Ee6W6Te4Pk4LsK3Zg1oEbXrMkzyEfTPoD3uD661lPS8Vz_DPAVYNrfpyoUGxUlgY3Kg2-FvNoiyWSc-bSeIh_QlYpfaRXmb2SDI5t9KtGiMNB8iQDzmF51rc=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFtOWRKFNqc9B-0NOpZCdIVq4D--RcYzjFq7IYzStyIWDyt-9eqfmIOMYJoJ5xZpeSt1fHYnG9to1n4nXGThr8RYWSMgTYOP0MAV3LOUeQpb0OBlphjuic2tJozyHS9-em-4webekZKQtFT3g8CeUEsvrre7dWc0sf7T53mgA5oUAyduW7Upw=