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

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

コミット

commit aa033c20b356b608e2fcc51e284cf711f952309b
Author: Nigel Tao <nigeltao@golang.org>
Date:   Wed Jan 11 10:15:40 2012 +1100

    html: propagate foreign namespaces only when adding foreign content.
    
    Pass tests10.dat, test 31:
    <div><svg><path><foreignObject><p></div>a
    
    | <html>
    |   <head>
    |   <body>
    |     <div>
    |       <svg svg>
    |         <svg path>
    |           <svg foreignObject>
    |             <p>
    |               "a"
    
    Also pass test 32:
    <!DOCTYPE html><svg><desc><div><svg><ul>a
    
    R=andybalholm
    CC=golang-dev
    https://golang.org/cl/5527064

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

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

元コミット内容

このコミットは、Go言語のhtmlパッケージにおけるHTMLパーサーの挙動を修正するものです。具体的には、外部(foreign)名前空間(例:SVGやMathML)の要素をHTMLコンテンツに追加する際に、名前空間の伝播(propagate)が正しく行われるように変更しています。これにより、tests10.datのテスト31およびテスト32がパスするようになります。

テスト31の例: <div><svg><path><foreignObject><p></div>a この入力に対して、パーサーは以下のような構造を生成することが期待されます。

<html>
  <head>
  <body>
    <div>
      <svg svg>
        <svg path>
          <svg foreignObject>
            <p>
              "a"

テスト32の例: <!DOCTYPE html><svg><desc><div><svg><ul>a

変更の背景

HTML5のパース仕様では、HTMLコンテンツ内にSVGやMathMLのようなXML名前空間を持つ要素(これらを「外部コンテンツ」または「foreign content」と呼びます)が埋め込まれる場合、その要素とその子孫要素は適切な名前空間に属する必要があります。従来のパーサーの実装では、この名前空間の伝播が常に適切に行われていなかった可能性があります。

特に、foreignObject要素のように、外部名前空間内にHTML要素を埋め込むことができる特殊なケースでは、名前空間の切り替えと伝播のロジックが複雑になります。このコミットは、このようなシナリオにおいて、要素が追加される際に親要素の名前空間を無条件に継承するのではなく、外部コンテンツの追加時のみに名前空間を適切に設定し直すことで、正しいDOMツリーが構築されるように問題を解決しています。

これにより、HTML5のパース仕様に準拠し、より堅牢で正確なHTMLパーサーを提供することが目的です。

前提知識の解説

HTMLパーシングとDOMツリー

HTMLパーシングとは、HTMLドキュメントのテキストを読み込み、それをブラウザが理解できる構造化されたデータ(DOMツリー)に変換するプロセスです。DOM(Document Object Model)ツリーは、HTMLドキュメントの論理的な構造を表現するツリー構造であり、各ノードはHTML要素、属性、テキストなどを表します。

XML名前空間 (Namespaces)

XML名前空間は、XMLドキュメント内で要素名や属性名の衝突を避けるためのメカニズムです。異なるXML語彙(例:HTML、SVG、MathML)からの要素が同じドキュメント内で使用される場合、名前空間はどの語彙に属するかを識別します。

  • HTML名前空間: 通常、HTML要素は名前空間を持ちませんが、内部的には「HTML名前空間」に属すると見なされます。
  • SVG名前空間: Scalable Vector Graphics (SVG) 要素は、http://www.w3.org/2000/svgという名前空間に属します。
  • MathML名前空間: Mathematical Markup Language (MathML) 要素は、http://www.w3.org/1998/Math/MathMLという名前空間に属します。

HTML5のパースアルゴリズムと外部コンテンツ (Foreign Content)

HTML5のパースアルゴリズムは非常に複雑で、特定のルールに基づいて要素の名前空間を決定します。特に重要な概念が「外部コンテンツ(Foreign Content)」です。

  • 外部コンテンツ: HTMLドキュメント内に埋め込まれたSVGやMathMLの要素を指します。これらの要素はHTML名前空間ではなく、それぞれのXML名前空間に属します。
  • 名前空間の切り替え: HTMLパーサーは、<svg>タグや<math>タグを検出すると、現在の名前空間をHTML名前空間から対応するSVG名前空間やMathML名前空間に切り替えます。
  • foreignObject要素: SVGの名前空間に属する特殊な要素で、その内部にHTML名前空間のコンテンツを埋め込むことができます。この要素の存在は、パーサーが名前空間を一時的にHTML名前空間に戻す必要があることを意味します。

このコミットは、特にforeignObjectのような要素が絡む複雑な名前空間の切り替えと伝播のシナリオにおいて、パーサーが正しく動作するようにするためのものです。

技術的詳細

このコミットの技術的な核心は、HTMLパーサーが要素をDOMツリーに追加する際の「名前空間の伝播」のロジックを修正することにあります。

以前の実装では、addElement関数が新しい要素を追加する際に、無条件に現在のパーサーの状態(p.top())から名前空間を継承していました。これは、ほとんどのHTML要素では問題ありませんが、外部コンテンツ(SVGやMathML)の内部で、さらにHTMLコンテンツが埋め込まれるような特殊なケース(例: <svg><foreignObject><div>...</div></foreignObject></svg>)では問題を引き起こす可能性がありました。

具体的には、<foreignObject>要素の内部に入った場合、パーサーは一時的にHTML名前空間に戻る必要があります。しかし、addElementが常に親の名前空間を継承してしまうと、<foreignObject>内の<div>のようなHTML要素が誤ってSVG名前空間に属してしまう、といった不整合が発生する可能性がありました。

このコミットでは、以下の2つの変更によってこの問題を解決しています。

  1. addElementからの名前空間継承の削除: addElement関数から、新しい要素のNamespaceフィールドをp.top().Namespaceから初期化する行が削除されました。これにより、addElementは要素の名前空間を自動的に設定しなくなります。
  2. parseForeignContentでの明示的な名前空間設定: parseForeignContent関数内で、外部コンテンツの要素(例: <foreignObject>)が追加される直前に現在の名前空間を一時変数namespaceに保存し、p.addElementの呼び出し後に、その保存しておいた名前空間を明示的にp.top().Namespaceに設定し直しています。

この変更により、foreignObjectのような要素がパースされる際に、その要素自体は正しい外部名前空間に属しつつ、その子要素(特にHTMLコンテンツ)がパースされる際には、パーサーの状態が一時的にHTML名前空間に切り替わり、その後、親の外部名前空間に正しく戻されるという、より正確な名前空間の管理が可能になります。

これにより、HTML5の仕様に厳密に準拠したDOMツリーが構築され、ブラウザのレンダリングやJavaScriptによるDOM操作において予期せぬ挙動が発生するのを防ぎます。

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

diff --git a/src/pkg/html/parse.go b/src/pkg/html/parse.go
index 7077612e7a..43c04727ab 100644
--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -243,10 +243,9 @@ func (p *parser) addText(text string) {
 // addElement calls addChild with an element node.
 func (p *parser) addElement(tag string, attr []Attribute) {
 	p.addChild(&Node{
-		Type:      ElementNode,
-		Data:      tag,
-		Namespace: p.top().Namespace,
-		Attr:      attr,
+		Type: ElementNode,
+		Data: tag,
+		Attr: attr,
 	})
 }
 
@@ -1736,7 +1735,9 @@ func parseForeignContent(p *parser) bool {
 			panic("html: bad parser state: unexpected namespace")
 		}\n 		adjustForeignAttributes(p.tok.Attr)\n+\t\tnamespace := p.top().Namespace
 	\tp.addElement(p.tok.Data, p.tok.Attr)\n+\t\tp.top().Namespace = namespace
 	case EndTagToken:\n 	\tfor i := len(p.oe) - 1; i >= 0; i-- {\n 	\t\tif p.oe[i].Namespace == "" {\ndiff --git a/src/pkg/html/parse_test.go b/src/pkg/html/parse_test.go
index 91c8388b3a..c929c25772 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", 31},
+		{"tests10.dat", 33},
 	}
 	for _, tf := range testFiles {
 		f, err := os.Open("testdata/webkit/" + tf.filename)

コアとなるコードの解説

src/pkg/html/parse.go

  1. func (p *parser) addElement(tag string, attr []Attribute)の変更:

    • 変更前:
      p.addChild(&Node{
          Type:      ElementNode,
          Data:      tag,
          Namespace: p.top().Namespace, // ここで親の名前空間を継承していた
          Attr:      attr,
      })
      
    • 変更後:
      p.addChild(&Node{
          Type: ElementNode,
          Data: tag,
          Attr: attr,
      })
      
    • この変更により、addElement関数は新しい要素を作成する際に、自動的に親要素の名前空間を継承しなくなりました。名前空間の設定は、より上位のパースロジック(特に外部コンテンツを扱う部分)で明示的に制御されるようになります。
  2. func parseForeignContent(p *parser) boolの変更:

    • この関数は、SVGやMathMLなどの外部コンテンツをパースする際に呼び出されます。
    • 変更前は、p.addElementが呼び出されると、その中で名前空間が設定されていました。
    • 変更後:
      		adjustForeignAttributes(p.tok.Attr)
      		namespace := p.top().Namespace // 現在の名前空間を一時的に保存
      		p.addElement(p.tok.Data, p.tok.Attr)
      		p.top().Namespace = namespace // addElement後に保存した名前空間を再設定
      
    • p.addElementを呼び出す前に、現在のパーサーのトップ要素の名前空間(つまり、外部コンテンツの名前空間)をnamespace変数に保存しています。
    • p.addElementが呼び出された後(この時点では、新しい要素は名前空間を持たない状態で追加されています)、保存しておいたnamespaceの値を、新しく追加された要素(p.top())の名前空間として明示的に設定し直しています。
    • この修正により、addElementが名前空間を自動継承しないようになったことと合わせて、外部コンテンツの要素が追加される際に、その要素が正しい名前空間に属することが保証されます。特に、foreignObjectのような要素の内部でHTMLコンテンツがパースされる際に、名前空間の切り替えがより正確に行われるようになります。

src/pkg/html/parse_test.go

  • {"tests10.dat", 31}{"tests10.dat", 33}に変更されています。これは、tests10.datファイル内のテストケースのインデックスが変更されたことを示唆しています。コミットメッセージでテスト31と32がパスすると述べられていることから、この変更はテストスイートの更新または再編成によるものと考えられます。

関連リンク

参考にした情報源リンク