[インデックス 10916] ファイルの概要
このコミットは、Go言語のHTMLパーサー(src/pkg/html
)における重要な修正を含んでいます。特に、HTML5仕様の最新版に準拠するため、外部コンテンツ(Foreign Content、例えばSVGやMathML)のパース処理を改善し、外部コンテンツ内に出現する特定の「ブレイクアウトタグ」が正しく処理されるように変更されました。これにより、外部コンテンツから通常のHTMLパースモードへの切り替えがより正確に行われるようになり、複雑なHTMLドキュメントの解析精度が向上しています。
コミット
このコミットは、HTMLパーサーが外部コンテンツ(SVGやMathMLなど)内のブレイクアウトタグを適切に処理するように修正します。また、最新のHTML5仕様において、外部コンテンツがもはや「挿入モード」ではなく、独立した概念として扱われるようになったことを認識し、それに対応する変更が加えられています。これにより、tests10.dat
のテスト13およびテスト15までのすべてのテストがパスするようになりました。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fe28d1aacf108cb7b7a4ec573a019e193d07c696
元コミット内容
commit fe28d1aacf108cb7b7a4ec573a019e193d07c696
Author: Nigel Tao <nigeltao@golang.org>
Date: Wed Dec 21 10:00:41 2011 +1100
html: handle breakout tags in foreign content.
Also recognize that, in the latest version of the HTML5 spec,
foreign content is not an insertion mode, but a separate concern.
Pass tests10.dat, test 13:
<!DOCTYPE html><body><table><caption><svg><g>foo</g><g>bar</g><p>baz</table><p>quux
| <!DOCTYPE html>
| <html>
| <head>
| <body>
| <table>
| <caption>
| <svg svg>
| <svg g>
| "foo"
| <svg g>
| "bar"
| <p>
| "baz"
| <p>
| "quux"
Also pass tests through test 15:
<!DOCTYPE html><body><table><colgroup><svg><g>foo</g><g>bar</g><p>baz</table><p>quux
R=andybalholm
CC=golang-dev
https://golang.org/cl/5494078
変更の背景
この変更の背景には、HTML5仕様の進化と、それに伴うHTMLパーシングアルゴリズムの厳密な解釈があります。
初期のHTML5仕様のドラフトでは、SVGやMathMLといった「外部コンテンツ(Foreign Content)」をパースする際に、パーサーが特定の「挿入モード(Insertion Mode)」に切り替わるという概念がありました。しかし、仕様の改訂が進むにつれて、外部コンテンツの処理は独立したメカニズムとして定義されるようになりました。つまり、外部コンテンツは特定の挿入モードの一部としてではなく、パーサーが現在処理している要素の「名前空間(Namespace)」に基づいて、その要素が外部コンテンツであるかどうかを判断し、それに応じたパースルールを適用するという形に変わったのです。
このコミット以前のGoのHTMLパーサーは、古い仕様の解釈に基づいて外部コンテンツをinForeignContentIM
という挿入モードとして扱っていました。しかし、このアプローチでは、外部コンテンツ内に特定のHTMLタグ(「ブレイクアウトタグ」と呼ばれる)が出現した場合に、パーサーが外部コンテンツのパースを終了し、通常のHTMLパースモードに戻るというHTML5の重要なルールを正確に実装することが困難でした。
具体的には、<table><caption><svg><g>foo</g><g>bar</g><p>baz</table><p>quux
のようなマークアップにおいて、<svg>
要素内で<p>
タグが出現した場合、HTML5仕様では<p>
タグがブレイクアウトタグとして機能し、SVGのパースモードを終了してHTMLのパースモードに戻るべきだと定めています。古い実装ではこれが正しく処理されず、テストケースが失敗していました。
このコミットは、HTML5仕様の最新の解釈に準拠し、外部コンテンツの処理を挿入モードから独立させ、ブレイクアウトタグの検出とそれによるパースモードの切り替えを正確に行うことで、パーサーの堅牢性と互換性を向上させることを目的としています。
前提知識の解説
このコミットの理解を深めるために、以下の概念について解説します。
- HTMLパーサー: HTMLドキュメントを読み込み、その構造を解析して、ブラウザがレンダリングできるような内部表現(通常はDOMツリー)を構築するソフトウェアコンポーネントです。HTMLは非常に寛容な言語であるため、パーサーはエラーのあるマークアップも適切に処理し、可能な限りDOMツリーを構築する必要があります。
- HTML5仕様: World Wide Web Consortium (W3C) によって策定されたHTMLの最新の標準仕様です。HTML5は、新しい要素、API、そして厳密なパースアルゴリズムを導入し、ウェブの相互運用性を高めることを目指しています。
- 挿入モード (Insertion Mode): HTML5のパースアルゴリズムの中心的な概念の一つです。HTMLパーサーは、入力ストリームからトークンを読み込む際に、現在の「挿入モード」に基づいてそのトークンをどのように処理するかを決定します。例えば、
inBodyIM
(body要素内)、inTableIM
(table要素内)、inHeadIM
(head要素内)など、様々な挿入モードが存在し、それぞれ異なるトークン処理ルールを持ちます。パーサーは、特定のトークンや要素の出現に応じて、現在の挿入モードを切り替えます。 - 外部コンテンツ (Foreign Content): HTMLドキュメント内に埋め込まれた、HTML名前空間に属さないコンテンツを指します。最も一般的な例は、Scalable Vector Graphics (SVG) と Mathematical Markup Language (MathML) です。これらのコンテンツは独自のXML名前空間を持ち、HTMLとは異なるパースルールが適用されます。ブラウザは、これらのコンテンツをHTML DOMとは異なる方法で処理し、レンダリングします。
- ブレイクアウトタグ (Breakout Tags): 外部コンテンツ(SVGやMathML)のコンテキスト内で出現した際に、その外部コンテンツのパースモードを終了させ、通常のHTMLパースモードに戻す特定のHTMLタグを指します。例えば、SVG要素の内部に
<p>
タグのようなHTML要素が出現した場合、HTML5仕様では<p>
タグがブレイクアウトタグとして機能し、パーサーはSVGのパースを中断してHTMLのパースを再開する必要があります。これにより、誤って外部コンテンツ内にHTML要素がネストされても、ドキュメントの残りの部分が正しくパースされるようになります。 - DOM (Document Object Model): HTMLやXMLドキュメントの論理構造を表現し、その内容、構造、スタイルをプログラム的にアクセスおよび変更するためのAPIです。パーサーは、入力されたHTMLからDOMツリーを構築します。
- 名前空間 (Namespace): XMLベースの言語(SVGやMathMLなど)で使用される概念で、要素や属性の名前の衝突を避けるために使用されます。各名前空間はURIによって識別され、要素がどの言語の仕様に属するかを示します。
技術的詳細
このコミットは、GoのHTMLパーサーがHTML5仕様の「外部コンテンツ」の処理に関する最新の解釈に準拠するための複数の変更を含んでいます。
-
resetInsertionMode
関数の変更:- 以前の
resetInsertionMode
関数は、現在の要素のスタックを遡り、名前空間が空でない(つまり外部コンテンツである)要素を見つけると、挿入モードをinForeignContentIM
に設定していました。 - このコミットでは、このロジックが削除されました。これは、外部コンテンツがもはや特定の挿入モードとして扱われるべきではないというHTML5仕様の変更を反映しています。代わりに、パーサーは現在の要素が外部コンテンツであるかどうかを、その名前空間に基づいて動的に判断するようになります。
- 以前の
-
inBodyIM
関数の変更:inBodyIM
関数内で、addElement
が呼び出された後にp.im = inForeignContentIM
を設定していた行が削除されました。これは、上記と同様に、外部コンテンツが挿入モードではないという原則に沿った変更です。
-
inForeignContentIM
からparseForeignContent
へのリネームとロジック変更:- 以前の
inForeignContentIM
関数は、外部コンテンツのパースロジックをカプセル化していましたが、その名前が「挿入モード」であることを示唆していました。この関数はparseForeignContent
というより適切な名前にリネームされました。 - 最も重要な変更は、
parseForeignContent
関数内での「ブレイクアウトタグ」の処理です。StartTagToken
が検出され、そのタグがbreakout
マップに定義されているブレイクアウトタグである場合、パーサーは要素スタックを遡り、名前空間が空の(つまりHTML名前空間の)要素が見つかるまで外部コンテンツの要素をポップします。これにより、外部コンテンツのコンテキストが終了し、HTMLのパースモードに戻る準備が整います。- 以前のコードでは、この部分が
// TODO.
とコメントアウトされており、未実装でした。このコミットで、この重要なロジックが追加されました。
EndTagToken
の処理も変更され、外部コンテンツの要素が閉じられた際に、単にinBodyIM(p)
を呼び出すのではなく、p.im(p)
を返すようになりました。これは、現在の挿入モード(外部コンテンツから抜けた後のHTMLの挿入モード)に基づいて処理を継続することを意味します。また、p.resetInsertionMode()
の呼び出しも削除されました。
- 以前の
-
新しいヘルパー関数
inForeignContent()
の導入:- このコミットでは、
parser
構造体にinForeignContent()
という新しいメソッドが追加されました。 - この関数は、現在の要素スタックの最上位の要素が外部コンテンツ(名前空間が空でない)であるかどうかを効率的に判断します。これにより、パーサーのメインループが、現在のコンテキストが外部コンテンツであるかどうかを簡単にチェックできるようになります。
- このコミットでは、
-
parse
関数のメインループの変更:parse
関数のメインループ内で、トークンを処理する際に、まずp.inForeignContent()
を呼び出して現在のコンテキストが外部コンテンツであるかどうかをチェックするようになりました。- もし外部コンテンツであれば、
parseForeignContent(p)
を呼び出して外部コンテンツのパースロジックを適用します。 - そうでなければ、従来の
p.im(p)
(現在の挿入モードに応じた処理)を呼び出します。 - この変更により、外部コンテンツの処理が挿入モードの概念から分離され、よりモジュール化された形で実装されました。
これらの変更により、GoのHTMLパーサーは、HTML5仕様の複雑な外部コンテンツとブレイクアウトタグの処理ルールをより正確に実装できるようになり、特にSVGやMathMLがHTMLドキュメントに埋め込まれた場合のパースの正確性が大幅に向上しました。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、src/pkg/html/parse.go
に集中しています。
-
src/pkg/html/parse.go
のresetInsertionMode
関数:--- a/src/pkg/html/parse.go +++ b/src/pkg/html/parse.go @@ -319,10 +319,7 @@ func (p *parser) resetInsertionMode() { case "html": \tp.im = beforeHeadIM default: -\t\t\tif p.top().Namespace == "" { -\t\t\t\tcontinue -\t\t\t} -\t\t\tp.im = inForeignContentIM +\t\t\tcontinue } return }
inForeignContentIM
への切り替えロジックが削除されました。 -
src/pkg/html/parse.go
のinBodyIM
関数:--- a/src/pkg/html/parse.go +++ b/src/pkg/html/parse.go @@ -814,7 +811,6 @@ func inBodyIM(p *parser) bool { \t// TODO: adjust foreign attributes. \tp.addElement(p.tok.Data, p.tok.Attr) \tp.top().Namespace = namespace -\t\t\tp.im = inForeignContentIM \treturn true case "caption", "col", "colgroup", "frame", "head", "tbody", "td", "tfoot", "th", "thead", "tr": \t// Ignore the token.
inForeignContentIM
への直接的な挿入モード設定が削除されました。 -
src/pkg/html/parse.go
のinForeignContentIM
関数がparseForeignContent
にリネームされ、ブレイクアウトタグ処理が追加:--- a/src/pkg/html/parse.go +++ b/src/pkg/html/parse.go @@ -1590,7 +1586,7 @@ func afterAfterFramesetIM(p *parser) bool { } // Section 12.2.5.5. -func inForeignContentIM(p *parser) bool { +func parseForeignContent(p *parser) bool { switch p.tok.Type { case TextToken: \t// TODO: HTML integration points. @@ -1610,7 +1606,14 @@ func inForeignContentIM(p *parser) bool { \t})\n \tcase StartTagToken:\n \t\tif breakout[p.tok.Data] {\n-\t\t\t// TODO.\n+\t\t\tfor i := len(p.oe) - 1; i >= 0; i-- {\n+\t\t\t\t// TODO: HTML, MathML integration points.\n+\t\t\t\tif p.oe[i].Namespace == "" {\n+\t\t\t\t\tp.oe = p.oe[:i+1]\n+\t\t\t\t\tbreak\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\treturn false\n }\n switch p.top().Namespace { case "mathml": @@ -1626,15 +1629,13 @@ func inForeignContentIM(p *parser) bool { case EndTagToken:\n \t\tfor i := len(p.oe) - 1; i >= 0; i-- {\n \t\t\tif p.oe[i].Namespace == "" {\n-\t\t\t\tinBodyIM(p)\n-\t\t\t\tbreak +\t\t\t\treturn p.im(p) \t\t\t}\n \t\t\tif strings.EqualFold(p.oe[i].Data, p.tok.Data) {\n \t\t\t\tp.oe = p.oe[:i]\n \t\t\t\tbreak \t\t\t}\n \t}\n-\t\tp.resetInsertionMode()\n \treturn true \tdefault: \t\t// Ignore the token.
inForeignContentIM
がparseForeignContent
にリネームされ、ブレイクアウトタグの処理ロジックが追加されました。EndTagToken
の処理も変更されています。 -
src/pkg/html/parse.go
にinForeignContent
関数が追加:--- a/src/pkg/html/parse.go +++ b/src/pkg/html/parse.go @@ -1642,6 +1643,20 @@ func inForeignContentIM(p *parser) bool { return true } +// Section 12.2.5. +func (p *parser) inForeignContent() bool { +\tif len(p.oe) == 0 { +\t\treturn false +\t} +\tn := p.oe[len(p.oe)-1] +\tif n.Namespace == "" { +\t\treturn false +\t} +\t// TODO: MathML, HTML integration points. +\t// TODO: MathML's annotation-xml combining with SVG's svg. +\treturn true +} + func (p *parser) parse() error { // Iterate until EOF. Any other error will cause an early return. consumed := true
現在の要素が外部コンテンツであるかを判定するヘルパー関数が追加されました。
-
src/pkg/html/parse.go
のparse
関数内のメインループ:--- a/src/pkg/html/parse.go +++ b/src/pkg/html/parse.go @@ -1654,7 +1669,11 @@ func (p *parser) parse() error { \t\t\t\treturn err \t\t\t} \t\t}\n-\t\tconsumed = p.im(p) +\t\t\tif p.inForeignContent() { +\t\t\t\tconsumed = parseForeignContent(p) +\t\t\t} else { +\t\t\t\tconsumed = p.im(p) +\t\t\t} }\n \t// Loop until the final token (the ErrorToken signifying EOF) is consumed. \tfor {
inForeignContent()
のチェックに基づいて、parseForeignContent
または現在の挿入モードの関数を呼び出すように変更されました。 -
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},\n \t\t{"tests5.dat", -1},\n \t\t{"tests6.dat", 45},\n-\t\t{"tests10.dat\", 13},\n+\t\t{"tests10.dat\", 16},\n }\n \tfor _, tf := range testFiles { \t\tf, err := os.Open(\"testdata/webkit/\" + tf.filename)\n ``` `tests10.dat`の期待されるテスト結果が13から16に更新されました。これは、変更によってより多くのテストがパスするようになったことを示しています。
コアとなるコードの解説
-
resetInsertionMode
およびinBodyIM
からのinForeignContentIM
参照の削除: これらの変更は、HTML5仕様の最新の解釈に厳密に準拠するためのものです。以前は、外部コンテンツのパースは特定の「挿入モード」として扱われていましたが、新しい仕様では、外部コンテンツは要素の「名前空間」に基づいて識別される独立した概念となりました。この削除により、パーサーは外部コンテンツを挿入モードの切り替えによってではなく、要素の名前空間を直接チェックすることで処理するようになります。これにより、パーサーのロジックが仕様により忠実になり、柔軟性が向上します。 -
inForeignContentIM
からparseForeignContent
へのリネームとブレイクアウトタグ処理の実装: 関数のリネームは、その役割が「挿入モード」ではなく「外部コンテンツのパース処理」であることを明確にするためのものです。最も重要なのは、StartTagToken
がブレイクアウトタグである場合の処理の実装です。for i := len(p.oe) - 1; i >= 0; i-- { // TODO: HTML, MathML integration points. if p.oe[i].Namespace == "" { p.oe = p.oe[:i+1] break } } return false
このコードは、ブレイクアウトタグ(例:
<p>
タグ)が外部コンテンツ(例:<svg>
)内で検出された場合に実行されます。p.oe
は「open elements」(開いている要素)のスタックを表します。このループはスタックを逆順に(最も最近開かれた要素から)走査し、名前空間が空の要素(つまりHTML名前空間の要素)が見つかるまで、外部コンテンツの要素をスタックからポップします。p.oe = p.oe[:i+1]
は、スタックをそのHTML要素の直前まで切り詰めることを意味します。これにより、パーサーは外部コンテンツのコンテキストを終了し、HTMLのパースモードに戻る準備ができます。return false
は、現在のトークンが消費されず、次のパースサイクルで現在の挿入モード(外部コンテンツから抜けた後のHTMLの挿入モード)によって再処理されることを示唆しています。EndTagToken
の処理におけるreturn p.im(p)
への変更も同様に、外部コンテンツの終了時に、パーサーが現在の挿入モード(外部コンテンツから抜けた後のHTMLの挿入モード)に基づいて処理を継続することを保証します。 -
inForeignContent()
ヘルパー関数の追加:func (p *parser) inForeignContent() bool { if len(p.oe) == 0 { return false } n := p.oe[len(p.oe)-1] if n.Namespace == "" { return false } // TODO: MathML, HTML integration points. // TODO: MathML's annotation-xml combining with SVG's svg. return true }
この関数は、現在の要素スタックの最上位の要素が外部コンテンツであるかどうかを簡潔にチェックするためのものです。これにより、パーサーのメインループが、現在のパースコンテキストが外部コンテンツであるかどうかを効率的に判断し、適切なパースロジック(
parseForeignContent
または通常の挿入モードの関数)を呼び出すことができるようになります。これは、コードの可読性と保守性を向上させます。 -
parse
関数内のメインループの変更:if p.inForeignContent() { consumed = parseForeignContent(p) } else { consumed = p.im(p) }
この変更は、外部コンテンツの処理を挿入モードの概念から完全に分離する、このコミットの核心部分です。パーサーは、まず
inForeignContent()
を呼び出して現在のコンテキストが外部コンテンツであるかを判断します。もしそうであれば、parseForeignContent
関数を呼び出して外部コンテンツ固有のルールでトークンを処理します。そうでなければ、従来のp.im(p)
(現在のHTML挿入モードに応じた処理)を呼び出します。これにより、HTML5仕様の「外部コンテンツは挿入モードではない」という原則がコードレベルで明確に反映され、より正確で堅牢なパース動作が実現されます。
これらの変更は、HTML5の複雑なパースルール、特に名前空間とブレイクアウトタグの挙動を正確に実装するために不可欠であり、GoのHTMLパーサーの標準準拠性を大幅に向上させました。
関連リンク
- Go Code Review 5494078: https://golang.org/cl/5494078
参考にした情報源リンク
- HTML Standard - 12.2.5 The parsing model: https://html.spec.whatwg.org/multipage/parsing.html#the-parsing-model
- HTML Standard - 12.2.5.5 The rules for parsing tokens in foreign content: https://html.spec.whatwg.org/multipage/parsing.html#parsing-html-fragments (Note: The section numbers might have shifted in later versions of the spec, but the content on foreign content parsing rules remains relevant.)
- HTML Standard - 12.2.5.4 The rules for parsing tokens in HTML content: https://html.spec.whatwg.org/multipage/parsing.html#parsing-html-fragments
- Mozilla Developer Network (MDN) - HTML parsing: https://developer.mozilla.org/en-US/docs/Glossary/HTML_parsing
- Mozilla Developer Network (MDN) - Namespaces in XML: https://developer.mozilla.org/en-US/docs/Web/XML/Namespaces
- SVG (Scalable Vector Graphics) - W3C Recommendation: https://www.w3.org/TR/SVG/
- MathML (Mathematical Markup Language) - W3C Recommendation: https://www.w3.org/TR/MathML/
[インデックス 10916] ファイルの概要
このコミットは、Go言語のHTMLパーサー(src/pkg/html
)における重要な修正を含んでいます。特に、HTML5仕様の最新版に準拠するため、外部コンテンツ(Foreign Content、例えばSVGやMathML)のパース処理を改善し、外部コンテンツ内に出現する特定の「ブレイクアウトタグ」が正しく処理されるように変更されました。これにより、外部コンテンツから通常のHTMLパースモードへの切り替えがより正確に行われるようになり、複雑なHTMLドキュメントの解析精度が向上しています。
コミット
このコミットは、HTMLパーサーが外部コンテンツ(SVGやMathMLなど)内のブレイクアウトタグを適切に処理するように修正します。また、最新のHTML5仕様において、外部コンテンツがもはや「挿入モード」ではなく、独立した概念として扱われるようになったことを認識し、それに対応する変更が加えられています。これにより、tests10.dat
のテスト13およびテスト15までのすべてのテストがパスするようになりました。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fe28d1aacf108cb7b7a4ec573a019e193d07c696
元コミット内容
commit fe28d1aacf108cb7b7a4ec573a019e193d07c696
Author: Nigel Tao <nigeltao@golang.org>
Date: Wed Dec 21 10:00:41 2011 +1100
html: handle breakout tags in foreign content.
Also recognize that, in the latest version of the HTML5 spec,
foreign content is not an insertion mode, but a separate concern.
Pass tests10.dat, test 13:
<!DOCTYPE html><body><table><caption><svg><g>foo</g><g>bar</g><p>baz</table><p>quux
| <!DOCTYPE html>
| <html>
| <head>
| <body>
| <table>
| <caption>
| <svg svg>
| <svg g>
| "foo"
| <svg g>
| "bar"
| <p>
| "baz"
| <p>
| "quux"
Also pass tests through test 15:
<!DOCTYPE html><body><table><colgroup><svg><g>foo</g><g>bar</g><p>baz</table><p>quux
R=andybalholm
CC=golang-dev
https://golang.org/cl/5494078
変更の背景
この変更の背景には、HTML5仕様の進化と、それに伴うHTMLパーシングアルゴリズムの厳密な解釈があります。
初期のHTML5仕様のドラフトでは、SVGやMathMLといった「外部コンテンツ(Foreign Content)」をパースする際に、パーサーが特定の「挿入モード(Insertion Mode)」に切り替わるという概念がありました。しかし、仕様の改訂が進むにつれて、外部コンテンツの処理は独立したメカニズムとして定義されるようになりました。つまり、外部コンテンツは特定の挿入モードの一部としてではなく、パーサーが現在処理している要素の「名前空間(Namespace)」に基づいて、その要素が外部コンテンツであるかどうかを判断し、それに応じたパースルールを適用するという形に変わったのです。
このコミット以前のGoのHTMLパーサーは、古い仕様の解釈に基づいて外部コンテンツをinForeignContentIM
という挿入モードとして扱っていました。しかし、このアプローチでは、外部コンテンツ内に特定のHTMLタグ(「ブレイクアウトタグ」と呼ばれる)が出現した場合に、パーサーが外部コンテンツのパースを終了し、通常のHTMLパースモードに戻るというHTML5の重要なルールを正確に実装することが困難でした。
具体的には、<table><caption><svg><g>foo</g><g>bar</g><p>baz</table><p>quux
のようなマークアップにおいて、<svg>
要素内で<p>
タグが出現した場合、HTML5仕様では<p>
タグがブレイクアウトタグとして機能し、SVGのパースモードを終了してHTMLのパースモードに戻るべきだと定めています。古い実装ではこれが正しく処理されず、テストケースが失敗していました。
このコミットは、HTML5仕様の最新の解釈に準拠し、外部コンテンツの処理を挿入モードから独立させ、ブレイクアウトタグの検出とそれによるパースモードの切り替えを正確に行うことで、パーサーの堅牢性と互換性を向上させることを目的としています。
前提知識の解説
このコミットの理解を深めるために、以下の概念について解説します。
- HTMLパーサー: HTMLドキュメントを読み込み、その構造を解析して、ブラウザがレンダリングできるような内部表現(通常はDOMツリー)を構築するソフトウェアコンポーネントです。HTMLは非常に寛容な言語であるため、パーサーはエラーのあるマークアップも適切に処理し、可能な限りDOMツリーを構築する必要があります。
- HTML5仕様: World Wide Web Consortium (W3C) によって策定されたHTMLの最新の標準仕様です。HTML5は、新しい要素、API、そして厳密なパースアルゴリズムを導入し、ウェブの相互運用性を高めることを目指しています。
- 挿入モード (Insertion Mode): HTML5のパースアルゴリズムの中心的な概念の一つです。HTMLパーサーは、入力ストリームからトークンを読み込む際に、現在の「挿入モード」に基づいてそのトークンをどのように処理するかを決定します。例えば、
inBodyIM
(body要素内)、inTableIM
(table要素内)、inHeadIM
(head要素内)など、様々な挿入モードが存在し、それぞれ異なるトークン処理ルールを持ちます。パーサーは、特定のトークンや要素の出現に応じて、現在の挿入モードを切り替えます。 - 外部コンテンツ (Foreign Content): HTMLドキュメント内に埋め込まれた、HTML名前空間に属さないコンテンツを指します。最も一般的な例は、Scalable Vector Graphics (SVG) と Mathematical Markup Language (MathML) です。これらのコンテンツは独自のXML名前空間を持ち、HTMLとは異なるパースルールが適用されます。ブラウザは、これらのコンテンツをHTML DOMとは異なる方法で処理し、レンダリングします。
- ブレイクアウトタグ (Breakout Tags): 外部コンテンツ(SVGやMathML)のコンテキスト内で出現した際に、その外部コンテンツのパースモードを終了させ、通常のHTMLパースモードに戻す特定のHTMLタグを指します。例えば、SVG要素の内部に
<p>
タグのようなHTML要素が出現した場合、HTML5仕様では<p>
タグがブレイクアウトタグとして機能し、パーサーはSVGのパースを中断してHTMLのパースを再開する必要があります。これにより、誤って外部コンテンツ内にHTML要素がネストされても、ドキュメントの残りの部分が正しくパースされるようになります。 - DOM (Document Object Model): HTMLやXMLドキュメントの論理構造を表現し、その内容、構造、スタイルをプログラム的にアクセスおよび変更するためのAPIです。パーサーは、入力されたHTMLからDOMツリーを構築します。
- 名前空間 (Namespace): XMLベースの言語(SVGやMathMLなど)で使用される概念で、要素や属性の名前の衝突を避けるために使用されます。各名前空間はURIによって識別され、要素がどの言語の仕様に属するかを示します。
技術的詳細
このコミットは、GoのHTMLパーサーがHTML5仕様の「外部コンテンツ」の処理に関する最新の解釈に準拠するための複数の変更を含んでいます。
-
resetInsertionMode
関数の変更:- 以前の
resetInsertionMode
関数は、現在の要素のスタックを遡り、名前空間が空でない(つまり外部コンテンツである)要素を見つけると、挿入モードをinForeignContentIM
に設定していました。 - このコミットでは、このロジックが削除されました。これは、外部コンテンツがもはや特定の挿入モードとして扱われるべきではないというHTML5仕様の変更を反映しています。代わりに、パーサーは現在の要素が外部コンテンツであるかどうかを、その名前空間に基づいて動的に判断するようになります。
- 以前の
-
inBodyIM
関数の変更:inBodyIM
関数内で、addElement
が呼び出された後にp.im = inForeignContentIM
を設定していた行が削除されました。これは、上記と同様に、外部コンテンツが挿入モードではないという原則に沿った変更です。
-
inForeignContentIM
からparseForeignContent
へのリネームとロジック変更:- 以前の
inForeignContentIM
関数は、外部コンテンツのパースロジックをカプセル化していましたが、その名前が「挿入モード」であることを示唆していました。この関数はparseForeignContent
というより適切な名前にリネームされました。 - 最も重要な変更は、
parseForeignContent
関数内での「ブレイクアウトタグ」の処理です。StartTagToken
が検出され、そのタグがbreakout
マップに定義されているブレイクアウトタグである場合、パーサーは要素スタックを遡り、名前空間が空の(つまりHTML名前空間の)要素が見つかるまで外部コンテンツの要素をポップします。これにより、外部コンテンツのコンテキストが終了し、HTMLのパースモードに戻る準備が整います。- 以前のコードでは、この部分が
// TODO.
とコメントアウトされており、未実装でした。このコミットで、この重要なロジックが追加されました。
EndTagToken
の処理も変更され、外部コンテンツの要素が閉じられた際に、単にinBodyIM(p)
を呼び出すのではなく、p.im(p)
を返すようになりました。これは、現在の挿入モード(外部コンテンツから抜けた後のHTMLの挿入モード)に基づいて処理を継続することを意味します。また、p.resetInsertionMode()
の呼び出しも削除されました。
- 以前の
-
新しいヘルパー関数
inForeignContent()
の導入:- このコミットでは、
parser
構造体にinForeignContent()
という新しいメソッドが追加されました。 - この関数は、現在の要素スタックの最上位の要素が外部コンテンツ(名前空間が空でない)であるかどうかを効率的に判断します。これにより、パーサーのメインループが、現在のコンテキストが外部コンテンツであるかどうかを簡単にチェックできるようになります。
- このコミットでは、
-
parse
関数のメインループの変更:parse
関数のメインループ内で、トークンを処理する際に、まずp.inForeignContent()
を呼び出して現在のコンテキストが外部コンテンツであるかどうかをチェックするようになりました。- もし外部コンテンツであれば、
parseForeignContent(p)
を呼び出して外部コンテンツのパースロジックを適用します。 - そうでなければ、従来の
p.im(p)
(現在の挿入モードに応じた処理)を呼び出します。 - この変更により、外部コンテンツの処理が挿入モードの概念から分離され、よりモジュール化された形で実装されました。
これらの変更により、GoのHTMLパーサーは、HTML5仕様の複雑な外部コンテンツとブレイクアウトタグの処理ルールをより正確に実装できるようになり、特にSVGやMathMLがHTMLドキュメントに埋め込まれた場合のパースの正確性が大幅に向上しました。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、src/pkg/html/parse.go
に集中しています。
-
src/pkg/html/parse.go
のresetInsertionMode
関数:--- a/src/pkg/html/parse.go +++ b/src/pkg/html/parse.go @@ -319,10 +319,7 @@ func (p *parser) resetInsertionMode() { case "html": \tp.im = beforeHeadIM default: -\t\t\tif p.top().Namespace == "" { -\t\t\t\tcontinue -\t\t\t} -\t\t\tp.im = inForeignContentIM +\t\t\tcontinue } return }
inForeignContentIM
への切り替えロジックが削除されました。 -
src/pkg/html/parse.go
のinBodyIM
関数:--- a/src/pkg/html/parse.go +++ b/src/pkg/html/parse.go @@ -814,7 +811,6 @@ func inBodyIM(p *parser) bool { \t// TODO: adjust foreign attributes. \tp.addElement(p.tok.Data, p.tok.Attr) \tp.top().Namespace = namespace -\t\t\tp.im = inForeignContentIM \treturn true case "caption", "col", "colgroup", "frame", "head", "tbody", "td", "tfoot", "th", "thead", "tr": \t// Ignore the token.
inForeignContentIM
への直接的な挿入モード設定が削除されました。 -
src/pkg/html/parse.go
のinForeignContentIM
関数がparseForeignContent
にリネームされ、ブレイクアウトタグ処理が追加:--- a/src/pkg/html/parse.go +++ b/src/pkg/html/parse.go @@ -1590,7 +1586,7 @@ func afterAfterFramesetIM(p *parser) bool { } // Section 12.2.5.5. -func inForeignContentIM(p *parser) bool { +func parseForeignContent(p *parser) bool { switch p.tok.Type { case TextToken: \t// TODO: HTML integration points. @@ -1610,7 +1606,14 @@ func inForeignContentIM(p *parser) bool { \t})\n \tcase StartTagToken:\n \t\tif breakout[p.tok.Data] {\n-\t\t\t// TODO.\n+\t\t\tfor i := len(p.oe) - 1; i >= 0; i-- {\n+\t\t\t\t// TODO: HTML, MathML integration points.\n+\t\t\t\tif p.oe[i].Namespace == "" {\n+\t\t\t\t\tp.oe = p.oe[:i+1]\n+\t\t\t\t\tbreak\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\treturn false\n }\n switch p.top().Namespace { case "mathml": @@ -1626,15 +1629,13 @@ func inForeignContentIM(p *parser) bool { case EndTagToken:\n \t\tfor i := len(p.oe) - 1; i >= 0; i-- {\n \t\t\tif p.oe[i].Namespace == "" {\n-\t\t\t\tinBodyIM(p)\n-\t\t\t\tbreak +\t\t\t\treturn p.im(p) \t\t\t}\n \t\t\tif strings.EqualFold(p.oe[i].Data, p.tok.Data) {\n \t\t\t\tp.oe = p.oe[:i]\n \t\t\t\tbreak \t\t\t}\n \t}\n-\t\tp.resetInsertionMode()\n \treturn true \tdefault: \t\t// Ignore the token.
inForeignContentIM
がparseForeignContent
にリネームされ、ブレイクアウトタグの処理ロジックが追加されました。EndTagToken
の処理も変更されています。 -
src/pkg/html/parse.go
にinForeignContent
関数が追加:--- a/src/pkg/html/parse.go +++ b/src/pkg/html/parse.go @@ -1642,6 +1643,20 @@ func inForeignContentIM(p *parser) bool { return true } +// Section 12.2.5. +func (p *parser) inForeignContent() bool { +\tif len(p.oe) == 0 { +\t\treturn false +\t} +\tn := p.oe[len(p.oe)-1] +\tif n.Namespace == "" { +\t\treturn false +\t} +\t// TODO: MathML, HTML integration points. +\t// TODO: MathML's annotation-xml combining with SVG's svg. +\treturn true +} + func (p *parser) parse() error { // Iterate until EOF. Any other error will cause an early return. consumed := true
現在の要素が外部コンテンツであるかを判定するヘルパー関数が追加されました。
-
src/pkg/html/parse.go
のparse
関数内のメインループ:--- a/src/pkg/html/parse.go +++ b/src/pkg/html/parse.go @@ -1654,7 +1669,11 @@ func (p *parser) parse() error { \t\t\t\treturn err \t\t\t} \t\t}\n-\t\tconsumed = p.im(p) +\t\t\tif p.inForeignContent() { +\t\t\t\tconsumed = parseForeignContent(p) +\t\t\t} else { +\t\t\t\tconsumed = p.im(p) +\t\t\t} }\n \t// Loop until the final token (the ErrorToken signifying EOF) is consumed. \tfor {
inForeignContent()
のチェックに基づいて、parseForeignContent
または現在の挿入モードの関数を呼び出すように変更されました。 -
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},\n \t\t{"tests5.dat", -1},\n \t\t{"tests6.dat", 45},\n-\t\t{"tests10.dat\", 13},\n+\t\t{"tests10.dat\", 16},\n }\n \tfor _, tf := range testFiles { \t\tf, err := os.Open(\"testdata/webkit/\" + tf.filename)\n ``` `tests10.dat`の期待されるテスト結果が13から16に更新されました。これは、変更によってより多くのテストがパスするようになったことを示しています。
コアとなるコードの解説
-
resetInsertionMode
およびinBodyIM
からのinForeignContentIM
参照の削除: これらの変更は、HTML5仕様の最新の解釈に厳密に準拠するためのものです。以前は、外部コンテンツのパースは特定の「挿入モード」として扱われていましたが、新しい仕様では、外部コンテンツは要素の「名前空間」に基づいて識別される独立した概念となりました。この削除により、パーサーは外部コンテンツを挿入モードの切り替えによってではなく、要素の名前空間を直接チェックすることで処理するようになります。これにより、パーサーのロジックが仕様により忠実になり、柔軟性が向上します。 -
inForeignContentIM
からparseForeignContent
へのリネームとブレイクアウトタグ処理の実装: 関数のリネームは、その役割が「挿入モード」ではなく「外部コンテンツのパース処理」であることを明確にするためのものです。最も重要なのは、StartTagToken
がブレイクアウトタグである場合の処理の実装です。for i := len(p.oe) - 1; i >= 0; i-- { // TODO: HTML, MathML integration points. if p.oe[i].Namespace == "" { p.oe = p.oe[:i+1] break } } return false
このコードは、ブレイクアウトタグ(例:
<p>
タグ)が外部コンテンツ(例:<svg>
)内で検出された場合に実行されます。p.oe
は「open elements」(開いている要素)のスタックを表します。このループはスタックを逆順に(最も最近開かれた要素から)走査し、名前空間が空の要素(つまりHTML名前空間の要素)が見つかるまで、外部コンテンツの要素をスタックからポップします。p.oe = p.oe[:i+1]
は、スタックをそのHTML要素の直前まで切り詰めることを意味します。これにより、パーサーは外部コンテンツのコンテキストを終了し、HTMLのパースモードに戻る準備ができます。return false
は、現在のトークンが消費されず、次のパースサイクルで現在の挿入モード(外部コンテンツから抜けた後のHTMLの挿入モード)によって再処理されることを示唆しています。EndTagToken
の処理におけるreturn p.im(p)
への変更も同様に、外部コンテンツの終了時に、パーサーが現在の挿入モード(外部コンテンツから抜けた後のHTMLの挿入モード)に基づいて処理を継続することを保証します。 -
inForeignContent()
ヘルパー関数の追加:func (p *parser) inForeignContent() bool { if len(p.oe) == 0 { return false } n := p.oe[len(p.oe)-1] if n.Namespace == "" { return false } // TODO: MathML, HTML integration points. // TODO: MathML's annotation-xml combining with SVG's svg. return true }
この関数は、現在の要素スタックの最上位の要素が外部コンテンツであるかどうかを簡潔にチェックするためのものです。これにより、パーサーのメインループが、現在のパースコンテキストが外部コンテンツであるかどうかを効率的に判断し、適切なパースロジック(
parseForeignContent
または通常の挿入モードの関数)を呼び出すことができるようになります。これは、コードの可読性と保守性を向上させます。 -
parse
関数内のメインループの変更:if p.inForeignContent() { consumed = parseForeignContent(p) } else { consumed = p.im(p) }
この変更は、外部コンテンツの処理を挿入モードの概念から完全に分離する、このコミットの核心部分です。パーサーは、まず
inForeignContent()
を呼び出して現在のコンテキストが外部コンテンツであるかを判断します。もしそうであれば、parseForeignContent
関数を呼び出して外部コンテンツ固有のルールでトークンを処理します。そうでなければ、従来のp.im(p)
(現在のHTML挿入モードに応じた処理)を呼び出します。これにより、HTML5仕様の「外部コンテンツは挿入モードではない」という原則がコードレベルで明確に反映され、より正確で堅牢なパース動作が実現されます。
これらの変更は、HTML5の複雑なパースルール、特に名前空間とブレイクアウトタグの挙動を正確に実装するために不可欠であり、GoのHTMLパーサーの標準準拠性を大幅に向上させました。
関連リンク
- Go Code Review 5494078: https://golang.org/cl/5494078
参考にした情報源リンク
- HTML Standard - 12.2.5 The parsing model: https://html.spec.whatwg.org/multipage/parsing.html#the-parsing-model
- HTML Standard - 12.2.5.5 The rules for parsing tokens in foreign content: https://html.spec.whatwg.org/multipage/parsing.html#parsing-html-fragments (Note: The section numbers might have shifted in later versions of the spec, but the content on foreign content parsing rules remains relevant.)
- HTML Standard - 12.2.5.4 The rules for parsing tokens in HTML content: https://html.spec.whatwg.org/multipage/parsing.html#parsing-html-fragments
- Mozilla Developer Network (MDN) - HTML parsing: https://developer.mozilla.org/en-US/docs/Glossary/HTML_parsing
- Mozilla Developer Network (MDN) - Namespaces in XML: https://developer.mozilla.org/en-US/docs/Web/XML/Namespaces
- SVG (Scalable Vector Graphics) - W3C Recommendation: https://www.w3.org/TR/SVG/
- MathML (Mathematical Markup Language) - W3C Recommendation: https://www.w3.org/TR/MathML/