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

[インデックス 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仕様の「外部コンテンツ」の処理に関する最新の解釈に準拠するための複数の変更を含んでいます。

  1. resetInsertionMode 関数の変更:

    • 以前のresetInsertionMode関数は、現在の要素のスタックを遡り、名前空間が空でない(つまり外部コンテンツである)要素を見つけると、挿入モードをinForeignContentIMに設定していました。
    • このコミットでは、このロジックが削除されました。これは、外部コンテンツがもはや特定の挿入モードとして扱われるべきではないというHTML5仕様の変更を反映しています。代わりに、パーサーは現在の要素が外部コンテンツであるかどうかを、その名前空間に基づいて動的に判断するようになります。
  2. inBodyIM 関数の変更:

    • inBodyIM関数内で、addElementが呼び出された後にp.im = inForeignContentIMを設定していた行が削除されました。これは、上記と同様に、外部コンテンツが挿入モードではないという原則に沿った変更です。
  3. inForeignContentIM から parseForeignContent へのリネームとロジック変更:

    • 以前のinForeignContentIM関数は、外部コンテンツのパースロジックをカプセル化していましたが、その名前が「挿入モード」であることを示唆していました。この関数はparseForeignContentというより適切な名前にリネームされました。
    • 最も重要な変更は、parseForeignContent関数内での「ブレイクアウトタグ」の処理です。
      • StartTagTokenが検出され、そのタグがbreakoutマップに定義されているブレイクアウトタグである場合、パーサーは要素スタックを遡り、名前空間が空の(つまりHTML名前空間の)要素が見つかるまで外部コンテンツの要素をポップします。これにより、外部コンテンツのコンテキストが終了し、HTMLのパースモードに戻る準備が整います。
      • 以前のコードでは、この部分が// TODO.とコメントアウトされており、未実装でした。このコミットで、この重要なロジックが追加されました。
    • EndTagTokenの処理も変更され、外部コンテンツの要素が閉じられた際に、単にinBodyIM(p)を呼び出すのではなく、p.im(p)を返すようになりました。これは、現在の挿入モード(外部コンテンツから抜けた後のHTMLの挿入モード)に基づいて処理を継続することを意味します。また、p.resetInsertionMode()の呼び出しも削除されました。
  4. 新しいヘルパー関数 inForeignContent() の導入:

    • このコミットでは、parser構造体にinForeignContent()という新しいメソッドが追加されました。
    • この関数は、現在の要素スタックの最上位の要素が外部コンテンツ(名前空間が空でない)であるかどうかを効率的に判断します。これにより、パーサーのメインループが、現在のコンテキストが外部コンテンツであるかどうかを簡単にチェックできるようになります。
  5. parse 関数のメインループの変更:

    • parse関数のメインループ内で、トークンを処理する際に、まずp.inForeignContent()を呼び出して現在のコンテキストが外部コンテンツであるかどうかをチェックするようになりました。
    • もし外部コンテンツであれば、parseForeignContent(p)を呼び出して外部コンテンツのパースロジックを適用します。
    • そうでなければ、従来のp.im(p)(現在の挿入モードに応じた処理)を呼び出します。
    • この変更により、外部コンテンツの処理が挿入モードの概念から分離され、よりモジュール化された形で実装されました。

これらの変更により、GoのHTMLパーサーは、HTML5仕様の複雑な外部コンテンツとブレイクアウトタグの処理ルールをより正確に実装できるようになり、特にSVGやMathMLがHTMLドキュメントに埋め込まれた場合のパースの正確性が大幅に向上しました。

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

このコミットにおける主要なコード変更は、src/pkg/html/parse.go に集中しています。

  1. src/pkg/html/parse.goresetInsertionMode 関数:

    --- 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への切り替えロジックが削除されました。

  2. src/pkg/html/parse.goinBodyIM 関数:

    --- 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への直接的な挿入モード設定が削除されました。

  3. src/pkg/html/parse.goinForeignContentIM 関数が 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.
    

    inForeignContentIMparseForeignContentにリネームされ、ブレイクアウトタグの処理ロジックが追加されました。EndTagTokenの処理も変更されています。

  4. src/pkg/html/parse.goinForeignContent 関数が追加:

    --- 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
    

    現在の要素が外部コンテンツであるかを判定するヘルパー関数が追加されました。

  5. src/pkg/html/parse.goparse 関数内のメインループ:

    --- 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または現在の挿入モードの関数を呼び出すように変更されました。

  6. 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パーサーの標準準拠性を大幅に向上させました。

関連リンク

参考にした情報源リンク

[インデックス 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仕様の「外部コンテンツ」の処理に関する最新の解釈に準拠するための複数の変更を含んでいます。

  1. resetInsertionMode 関数の変更:

    • 以前のresetInsertionMode関数は、現在の要素のスタックを遡り、名前空間が空でない(つまり外部コンテンツである)要素を見つけると、挿入モードをinForeignContentIMに設定していました。
    • このコミットでは、このロジックが削除されました。これは、外部コンテンツがもはや特定の挿入モードとして扱われるべきではないというHTML5仕様の変更を反映しています。代わりに、パーサーは現在の要素が外部コンテンツであるかどうかを、その名前空間に基づいて動的に判断するようになります。
  2. inBodyIM 関数の変更:

    • inBodyIM関数内で、addElementが呼び出された後にp.im = inForeignContentIMを設定していた行が削除されました。これは、上記と同様に、外部コンテンツが挿入モードではないという原則に沿った変更です。
  3. inForeignContentIM から parseForeignContent へのリネームとロジック変更:

    • 以前のinForeignContentIM関数は、外部コンテンツのパースロジックをカプセル化していましたが、その名前が「挿入モード」であることを示唆していました。この関数はparseForeignContentというより適切な名前にリネームされました。
    • 最も重要な変更は、parseForeignContent関数内での「ブレイクアウトタグ」の処理です。
      • StartTagTokenが検出され、そのタグがbreakoutマップに定義されているブレイクアウトタグである場合、パーサーは要素スタックを遡り、名前空間が空の(つまりHTML名前空間の)要素が見つかるまで外部コンテンツの要素をポップします。これにより、外部コンテンツのコンテキストが終了し、HTMLのパースモードに戻る準備が整います。
      • 以前のコードでは、この部分が// TODO.とコメントアウトされており、未実装でした。このコミットで、この重要なロジックが追加されました。
    • EndTagTokenの処理も変更され、外部コンテンツの要素が閉じられた際に、単にinBodyIM(p)を呼び出すのではなく、p.im(p)を返すようになりました。これは、現在の挿入モード(外部コンテンツから抜けた後のHTMLの挿入モード)に基づいて処理を継続することを意味します。また、p.resetInsertionMode()の呼び出しも削除されました。
  4. 新しいヘルパー関数 inForeignContent() の導入:

    • このコミットでは、parser構造体にinForeignContent()という新しいメソッドが追加されました。
    • この関数は、現在の要素スタックの最上位の要素が外部コンテンツ(名前空間が空でない)であるかどうかを効率的に判断します。これにより、パーサーのメインループが、現在のコンテキストが外部コンテンツであるかどうかを簡単にチェックできるようになります。
  5. parse 関数のメインループの変更:

    • parse関数のメインループ内で、トークンを処理する際に、まずp.inForeignContent()を呼び出して現在のコンテキストが外部コンテンツであるかどうかをチェックするようになりました。
    • もし外部コンテンツであれば、parseForeignContent(p)を呼び出して外部コンテンツのパースロジックを適用します。
    • そうでなければ、従来のp.im(p)(現在の挿入モードに応じた処理)を呼び出します。
    • この変更により、外部コンテンツの処理が挿入モードの概念から分離され、よりモジュール化された形で実装されました。

これらの変更により、GoのHTMLパーサーは、HTML5仕様の複雑な外部コンテンツとブレイクアウトタグの処理ルールをより正確に実装できるようになり、特にSVGやMathMLがHTMLドキュメントに埋め込まれた場合のパースの正確性が大幅に向上しました。

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

このコミットにおける主要なコード変更は、src/pkg/html/parse.go に集中しています。

  1. src/pkg/html/parse.goresetInsertionMode 関数:

    --- 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への切り替えロジックが削除されました。

  2. src/pkg/html/parse.goinBodyIM 関数:

    --- 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への直接的な挿入モード設定が削除されました。

  3. src/pkg/html/parse.goinForeignContentIM 関数が 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.
    

    inForeignContentIMparseForeignContentにリネームされ、ブレイクアウトタグの処理ロジックが追加されました。EndTagTokenの処理も変更されています。

  4. src/pkg/html/parse.goinForeignContent 関数が追加:

    --- 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
    

    現在の要素が外部コンテンツであるかを判定するヘルパー関数が追加されました。

  5. src/pkg/html/parse.goparse 関数内のメインループ:

    --- 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または現在の挿入モードの関数を呼び出すように変更されました。

  6. 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パーサーの標準準拠性を大幅に向上させました。

関連リンク

参考にした情報源リンク