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

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

このコミットは、Go言語のhtmlパッケージにおけるHTMLパーサーの機能拡張に関するものです。具体的には、HTMLドキュメント内に埋め込まれたMathML (Mathematical Markup Language) およびSVG (Scalable Vector Graphics) といった「外部コンテンツ(foreign content)」の初期的なパース(解析)サポートを導入しています。これにより、パーサーはこれらのXMLベースのコンテンツを適切に識別し、HTMLとは異なる名前空間(namespace)を持つノードとして扱うことができるようになります。

コミット

commit b9064fb13287c49ba978715af6da797428dcb77d
Author: Nigel Tao <nigeltao@golang.org>
Date:   Tue Dec 13 13:52:47 2011 +1100

    html: a first step at parsing foreign content (MathML, SVG).
    
    Nodes now have a Namespace field.
    
    Pass adoption01.dat, test 12:
    <a><svg><tr><input></a>
    
    | <html>
    |   <head>
    |   <body>
    |     <a>
    |       <svg svg>
    |         <svg tr>
    |           <svg input>
    
    The other adoption01.dat tests already passed.
    
    R=andybalholm
    CC=golang-dev
    https://golang.org/cl/5467075

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

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

元コミット内容

このコミットは、HTMLパーサーがMathMLやSVGといった外部コンテンツを解析するための第一歩を踏み出すものです。主な変更点として、ノード構造にNamespaceフィールドが追加されました。これにより、パーサーはHTML要素と外部コンテンツの要素を名前空間に基づいて区別できるようになります。

adoption01.datテストスイートのテスト12(<a><svg><tr><input></a>)がこの変更によってパスするようになりました。このテストケースは、<a>タグ内に<svg>タグがネストされ、さらにその中に<tr><input>といったHTML要素がネストされている場合の挙動を検証しています。変更後のパース結果は、<html> -> <body> -> <a> -> <svg svg> -> <svg tr> -> <svg input>のように、SVG要素が適切な名前空間(svg)を持つノードとして表現されることを示しています。

変更の背景

HTML5の仕様では、HTMLドキュメント内にSVGやMathMLといったXMLベースのコンテンツを直接埋め込むことが可能です。これらのコンテンツは、HTMLとは異なる独自の要素セットとセマンティクスを持ちます。従来のHTMLパーサーは、これらの外部コンテンツを単なる未知のHTML要素として扱ってしまうか、あるいは正しくパースできない可能性がありました。

このコミットの背景には、Go言語のhtmlパッケージがHTML5の仕様に準拠し、より堅牢で汎用的なHTMLパーサーを提供することを目指すという目的があります。特に、SVGやMathMLのような外部コンテンツを正しく解析し、DOMツリー内で適切な名前空間情報を持つノードとして表現することは、これらのコンテンツをJavaScriptなどで操作したり、レンダリングエンジンで正しく表示したりするために不可欠です。

この変更は、HTML5のパースアルゴリズムにおける「外部コンテンツ(Foreign Content)」の取り扱いに関するセクション(例えば、W3CのHTML仕様の12.2.5.5節など)に準拠するための初期実装と言えます。

前提知識の解説

HTML5パーシングアルゴリズム

HTML5の仕様は、ウェブブラウザがHTMLドキュメントをどのように解析し、DOM(Document Object Model)ツリーを構築するかについて非常に詳細なアルゴリズムを定義しています。このアルゴリズムは、トークン化(Tokenization)とツリー構築(Tree Construction)の2つの主要なフェーズに分かれます。

  • トークン化: 入力ストリーム(HTMLソースコード)を読み込み、個々のトークン(開始タグ、終了タグ、テキスト、コメントなど)に分解します。
  • ツリー構築: トークン化フェーズで生成されたトークンを消費し、DOMツリーを構築します。このフェーズは、現在のパーサーの状態を示す「挿入モード(Insertion Mode)」に基づいて動作します。挿入モードは、次にどの種類のトークンが期待され、どのようにDOMツリーにノードを追加すべきかを決定します。

名前空間 (Namespace)

XMLの名前空間は、XMLドキュメント内の要素名や属性名の衝突を避けるためのメカニズムです。異なるXML語彙(例えば、HTML、SVG、MathML)が同じ要素名(例: <title>)を持つ場合、名前空間を使用することで、どの語彙の要素であるかを明確に区別できます。名前空間はURI(Uniform Resource Identifier)によって識別され、通常は要素名のプレフィックスとして関連付けられます(例: <svg:svg>)。HTML5では、SVGやMathML要素がHTMLドキュメント内に埋め込まれる際、これらの要素はそれぞれSVG名前空間(http://www.w3.org/2000/svg)やMathML名前空間(http://www.w3.org/1998/Math/MathML)に属するものとして扱われます。

外部コンテンツ (Foreign Content)

HTML5のパーシングアルゴリズムにおいて、「外部コンテンツ」とは、HTML名前空間に属さない要素、具体的にはSVG要素やMathML要素を指します。パーサーが外部コンテンツの開始タグを検出すると、その要素はHTML名前空間ではなく、対応する名前空間(SVGまたはMathML)に属するものとしてDOMツリーに挿入されます。外部コンテンツ内では、HTMLのパースルールとは異なるXMLのパースルールが適用される場合があります。

挿入モード (Insertion Mode)

HTML5のツリー構築アルゴリズムの中心的な概念です。パーサーは常に特定の挿入モードにあり、このモードが次のトークンをどのように処理するかを決定します。例えば、inBodyIM<body>タグ内での処理を、inForeignContentIMは外部コンテンツ内での処理を定義します。モードは、特定のタグの開始や終了、あるいは特定の条件に基づいて切り替わります。

アクティブフォーマット要素 (Active Formatting Elements)

HTML5パーシングアルゴリズムにおけるもう一つの重要な概念です。これは、<b><i><a>などのフォーマット要素の開始タグが検出されたときに、それらの要素がDOMツリーに挿入されるだけでなく、特別なリスト(アクティブフォーマット要素リスト)にも追加されることを意味します。このリストは、ネストされたフォーマット要素の正しい構造を維持し、特定の状況で要素を「再構築」するために使用されます。

技術的詳細

このコミットは、Go言語のhtmlパッケージがHTML5の仕様に準拠し、外部コンテンツ(MathML、SVG)を適切にパースするための基盤を構築しています。

  1. Node構造体へのNamespaceフィールド追加:

    • src/pkg/html/node.goにおいて、Node構造体にNamespace stringフィールドが追加されました。これは、DOMツリー内の各要素がどの名前空間に属するか(HTML、SVG、MathMLなど)を識別するための最も基本的な変更です。これにより、パーサーは同じタグ名を持つ要素でも、その名前空間に基づいて異なるセースマンティクスで処理できるようになります。
  2. foreign.goの導入とbreakoutマップ:

    • 新しくsrc/pkg/html/foreign.goファイルが追加され、breakoutというmap[string]bool型の変数が定義されました。このマップには、外部コンテンツ(SVGやMathML)の内部で出現した場合に、パーサーを外部コンテンツモードから通常のHTMLモードに「ブレイクアウト」させるHTMLタグ名がリストされています。これはHTML5仕様の「Parsing HTML fragments」や「The rules for parsing tokens in foreign content」セクションで定義されている挙動を実装するためのものです。例えば、SVG要素の内部に<body>タグが出現した場合、それはSVGの一部ではなく、HTMLのボディ要素として扱われるべきです。
  3. パーサーの挿入モードの変更とinForeignContentIMの導入:

    • src/pkg/html/parse.goにおいて、パーサーの挙動が大幅に修正されました。
    • addElement関数は、新しく追加されるノードのNamespaceを、現在のパーサーのスタックの最上位ノードのNamespaceから継承するように変更されました。
    • resetInsertionMode関数は、現在のノードが名前空間を持つ場合(つまり、外部コンテンツ内にある場合)に、挿入モードをinForeignContentIM(外部コンテンツ内挿入モード)に切り替えるロジックが追加されました。これは、外部コンテンツのパースルールが適用されるべき状況を正確に検出するために重要です。
    • inBodyIM関数(<body>タグ内の処理を司る挿入モード)では、mathまたはsvgの開始タグが検出された際に、以下の処理が行われます。
      • reconstructActiveFormattingElements()が呼び出され、アクティブフォーマット要素リストが再構築されます。
      • 検出されたタグに応じて、ノードのNamespace"mathml"または"svg"に設定されます。
      • その後、挿入モードがinForeignContentIMに切り替わります。これにより、以降のトークンは外部コンテンツのルールに従ってパースされるようになります。
    • inForeignContentIM関数が新しく追加(または大幅に拡張)されました。このモードでは、コメントトークン、開始タグトークン、終了タグトークンが処理されます。特に、開始タグがbreakoutマップに含まれる場合、特別な処理(TODOコメントで示されているように、将来的にはブレイクアウト処理が実装される)が行われます。また、現在の名前空間(MathMLまたはSVG)に基づいて、属性の調整やタグ名の調整が必要になることがTODOコメントで示されています。
  4. テストの更新:

    • src/pkg/html/parse_test.goでは、dumpLevel関数が変更され、ElementNodeのダンプ時にNamespace情報も出力されるようになりました(例: <svg svg>)。これにより、パース結果のDOMツリーが名前空間情報を正しく保持しているかを確認できるようになります。
    • TestParser関数にadoption01.datテストスイートが追加され、外部コンテンツのパースに関するテストが実行されるようになりました。

これらの変更により、GoのHTMLパーサーは、HTML5の複雑な仕様の一部である外部コンテンツの取り扱いに関して、より正確で堅牢な挙動を示すようになりました。ただし、TODOコメントが示すように、MathMLやSVG固有の属性調整、タグ名の調整など、さらなる実装が必要な部分も残されています。

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

src/pkg/html/node.go

type Node struct {
	Parent    *Node
	Child     []*Node
	Type      NodeType
	Data      string
	Namespace string // 新しく追加されたフィールド
	Attr      []Attribute
}

src/pkg/html/foreign.go (新規ファイル)

// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package html

// Section 12.2.5.5.
var breakout = map[string]bool{
	"b":          true,
	"big":        true,
	"blockquote": true,
	"body":       true,
	"br":         true,
	"center":     true,
	"code":       true,
	"dd":         true,
	"div":        true,
	"dl":         true,
	"dt":         true,
	"em":         true,
	"embed":      true,
	"font":       true,
	"h1":         true,
	"h2":         true,
	"h3":         true,
	"h4":         true,
	"h5":         true,
	"h6":         true,
	"head":       true,
	"hr":         true,
	"i":          true,
	"img":        true,
	"li":         true,
	"listing":    true,
	"menu":       true,
	"meta":       true,
	"nobr":       true,
	"ol":         true,
	"p":          true,
	"pre":        true,
	"ruby":       true,
	"s":          true,
	"small":      true,
	"span":       true,
	"strong":     true,
	"strike":     true,
	"sub":        true,
	"sup":        true,
	"table":      true,
	"tt":         true,
	"u":          true,
	"ul":         true,
	"var":        true,
}

// TODO: add look-up tables for MathML and SVG adjustments.

src/pkg/html/parse.go

addElement関数の変更:

 func (p *parser) addElement(tag string, attr []Attribute) {
 	p.addChild(&Node{
-		Type: ElementNode,
-		Data: tag,
-		Attr: attr,
+		Type:      ElementNode,
+		Data:      tag,
+		Namespace: p.top().Namespace, // 親ノードの名前空間を継承
+		Attr:      attr,
 	})
 }

resetInsertionMode関数の変更:

 	// ...
 	for i := len(p.stack) - 1; i >= 0; i-- {
 		switch p.stack[i].Data {
 		case "html":
 			p.im = beforeHeadIM
 		default:
-			continue
+			if p.top().Namespace == "" { // 名前空間がない場合(HTML)はスキップ
+				continue
+			}
+			p.im = inForeignContentIM // 名前空間がある場合は外部コンテンツモードへ
 		}
 		return
 	}

inBodyIM関数の変更(math, svgタグのハンドリング追加):

 		case "math", "svg":
 			p.reconstructActiveFormattingElements()
 			namespace := ""
 			if p.tok.Data == "math" {
 				// TODO: adjust MathML attributes.
 				namespace = "mathml"
 			} else {
 				// TODO: adjust SVG attributes.
 				namespace = "svg"
 			}
 			// TODO: adjust foreign attributes.
 			p.addElement(p.tok.Data, p.tok.Attr)
 			p.top().Namespace = namespace // 新しいノードに名前空間を設定
 			p.im = inForeignContentIM     // 外部コンテンツモードへ移行
 			return true

inForeignContentIM関数の追加:

// TODO: fix up the other IM's section numbers to match the latest spec.

// Section 12.2.5.5.
func inForeignContentIM(p *parser) bool {
	switch p.tok.Type {
	case CommentToken:
		p.addChild(&Node{
			Type: CommentNode,
			Data: p.tok.Data,
		})
	case StartTagToken:
		if breakout[p.tok.Data] {
			// TODO.
		}
		switch p.top().Namespace {
		case "mathml":
			// TODO: adjust MathML attributes.
		case "svg":
			// TODO: adjust SVG tag names.
			// TODO: adjust SVG attributes.
		default:
			panic("html: bad parser state: unexpected namespace")
		}
		// TODO: adjust foreign attributes.
		p.addElement(p.tok.Data, p.tok.Attr)
	case EndTagToken:
		// TODO.
	default:
		// Ignore the token.
	}
	return true
}

src/pkg/html/parse_test.go

dumpLevel関数の変更(名前空間の表示追加):

 	case ElementNode:
-		fmt.Fprintf(w, "<%s>", n.Data)
+		if n.Namespace != "" {
+			fmt.Fprintf(w, "<%s %s>", n.Namespace, n.Data) // 名前空間があれば表示
+		} else {
+			fmt.Fprintf(w, "<%s>", n.Data)
+		}

TestParser関数の変更(adoption01.datの追加):

 	}{
 		// TODO(nigeltao): Process all the test cases from all the .dat files.
 		{"adoption01.dat", -1}, // 追加
 		{"doctype01.dat", -1},
 		{"tests1.dat", -1},
 		{"tests2.dat", -1},
 	}

コアとなるコードの解説

このコミットの核となる変更は、HTMLパーサーがHTML、MathML、SVGといった異なるマークアップ言語の要素を、それぞれの「名前空間」に基づいて区別し、適切にDOMツリーに組み込む能力を獲得した点にあります。

  1. Node構造体のNamespaceフィールド:

    • これは、パースされた各要素ノードがどの名前空間に属するかを明示的に保持するための最も重要な変更です。これにより、パーサーは単にタグ名だけでなく、そのタグがHTML、SVG、MathMLのいずれの文脈で出現したかを識別できるようになります。例えば、<title>タグはHTMLの<title>とSVGの<title>で意味が異なるため、このNamespaceフィールドがその区別を可能にします。
  2. foreign.gobreakoutマップ:

    • foreign.goで定義されたbreakoutマップは、HTML5のパーシング仕様における「外部コンテンツからのブレイクアウト」ルールを実装するためのものです。これは、SVGやMathMLの内部に特定のHTMLタグ(例: <body>, <div>, <table>など)が出現した場合、それらのタグは外部コンテンツの一部ではなく、通常のHTMLコンテンツとして扱われるべきであるというルールです。このマップは、そのようなブレイクアウトを引き起こすタグを効率的に識別するために使用されます。
  3. parse.goにおける挿入モードの遷移ロジック:

    • addElement関数が親ノードの名前空間を継承するように変更されたことで、DOMツリー構築時に名前空間情報が正しく伝播されるようになりました。
    • resetInsertionMode関数は、パーサーがスタックを巻き戻す際に、現在のノードが名前空間を持つ(つまり外部コンテンツ内にある)場合に、自動的にinForeignContentIMに切り替わるように修正されました。これは、外部コンテンツのパースルールが適用されるべき状況を正確に検出するために重要です。
    • inBodyIMにおけるmathおよびsvgタグの特殊なハンドリングは、HTMLコンテンツから外部コンテンツへの「入り口」を定義しています。これらのタグが検出されると、パーサーは名前空間を設定し、直ちにinForeignContentIMに遷移します。これにより、以降のトークンはSVGやMathMLのパースルールに従って処理される準備が整います。
    • 新しく導入されたinForeignContentIMは、外部コンテンツ内でのトークン処理を専門に行います。このモードでは、コメントは通常通り追加され、開始タグはbreakoutマップをチェックし、必要に応じて名前空間固有の調整(TODOコメントで示されている)が行われた上でノードが追加されます。このモードは、外部コンテンツの構文規則とセマンティクスを尊重しながらDOMツリーを構築するために不可欠です。

これらの変更は、Goのhtmlパッケージが、より複雑なウェブコンテンツ(特にHTML5で導入されたSVGやMathMLの埋め込み)を正確に解析し、標準に準拠したDOMツリーを生成するための重要な一歩となります。

関連リンク

参考にした情報源リンク