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

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

このコミットは、Go言語の標準ライブラリ html パッケージにおいて、HTMLフラグメント(断片)のパースアルゴリズムを実装し、関連するテストを追加・修正するものです。これにより、完全なHTMLドキュメントではなく、HTMLの一部を正しく解析できるようになります。

コミット

  • コミットハッシュ: ce27b00f48bf3b90445bb4bcd28f6115c129d75b
  • Author: Andrew Balholm andybalholm@gmail.com
  • Date: Thu Dec 1 12:47:57 2011 +1100

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

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

元コミット内容

html: implement fragment parsing algorithm

Pass the tests in tests4.dat.

R=nigeltao
CC=golang-dev
https://golang.org/cl/5447055

変更の背景

HTMLのパースは、ウェブブラウザやHTML処理ツールにとって非常に重要な機能です。しかし、常に完全なHTMLドキュメントを扱うわけではありません。例えば、JavaScriptの innerHTML プロパティを使って既存の要素にHTMLコンテンツを動的に挿入する場合や、特定の要素の内部コンテンツのみを解析したい場合など、HTMLの「断片」(フラグメント)をパースする必要が生じます。

従来のHTMLパーサーは、通常、<html>, <head>, <body> といったルート要素を持つ完全なドキュメントを前提として設計されています。しかし、HTMLフラグメントはこれらの要素を持たず、任意のHTML要素の内部コンテンツとして扱われるため、通常のドキュメントパースとは異なるルール(特に挿入モードのリセットやコンテキスト要素の考慮)が必要です。

このコミットは、HTML5仕様で定義されているHTMLフラグメントパースアルゴリズムをGoの html パッケージに導入することで、このような断片的なHTMLコンテンツを正確に解析できるようにすることを目的としています。これにより、Go言語でより堅牢なHTML処理アプリケーションを構築できるようになります。特に、tests4.dat に含まれるフラグメント関連のテストケースをパスすることが、この変更の直接的な動機となっています。

前提知識の解説

HTMLパースアルゴリズム

HTMLパースアルゴリズムは、HTML文字列を解析してDOMツリー(Document Object Model)を構築する一連の規則です。HTML5仕様では、このアルゴリズムが詳細に定義されており、エラー回復メカニズムや、特定のタグが検出された際の特殊な処理などが含まれます。パーサーは、入力ストリームからトークンを読み込み、それらのトークンに基づいてDOMツリーを構築します。このプロセスは「挿入モード」と呼ばれる状態機械によって制御され、現在の挿入モードに応じて次のトークンの処理方法が決定されます。

HTMLフラグメントパースアルゴリズム

HTMLフラグメントパースアルゴリズムは、完全なHTMLドキュメントではなく、HTMLの断片(例: <div><p>Hello</p></div> のような部分的なHTML文字列)を解析するための特殊なアルゴリズムです。このアルゴリズムの主な特徴は以下の通りです。

  1. コンテキスト要素 (Context Element): フラグメントがどのHTML要素の内部コンテンツとして扱われるかを指定します。例えば、<title> 要素の内部に挿入されるフラグメントは、通常のHTMLとは異なるパースルール(RCDATA状態)が適用されます。このコンテキスト要素は、パーサーの初期状態や挿入モードに影響を与えます。
  2. DocumentFragmentの生成: パースされたノードは、通常、DocumentFragment と呼ばれる軽量なコンテナに格納されます。これは、DOMツリーの一部として扱われることなく、複数のノードを効率的に操作するためのものです。
  3. スクリプトの扱い: フラグメント内の <script> 要素は、パース時にすぐに実行されるのではなく、already started フラグが false に設定され、parser documentnull に設定されます。これにより、フラグメントがライブDOMに挿入されたときに適切に処理されるようになります。
  4. エラー回復: 完全なドキュメントと同様に、フラグメントパースも一般的なHTMLエラーからの回復を考慮して設計されています。

挿入モード (Insertion Mode)

HTMLパーサーは、現在の状態に応じて「挿入モード」と呼ばれる様々なモードで動作します。例えば、initialIM (初期モード)、inHeadIM (head要素内モード)、inBodyIM (body要素内モード) などがあります。これらのモードは、次にどのようなトークンが期待され、どのようにDOMツリーにノードが追加されるかを決定します。

resetInsertionMode 関数

resetInsertionMode 関数は、HTMLパーサーが特定の状況下で現在の挿入モードをリセットするために使用されます。これは、特にエラー回復や、HTMLフラグメントのパース時にコンテキスト要素に基づいて適切な挿入モードを確立するために重要です。この関数は、要素スタックを逆順に辿り、適切な挿入モードを見つけ出して設定します。

技術的詳細

このコミットは、Go言語の html パッケージにおけるHTMLフラグメントパースのサポートを導入するために、主に以下の技術的変更を行っています。

  1. parser 構造体への context フィールドの追加: HTMLフラグメントパースでは、フラグメントがどの要素のコンテキストでパースされるかが重要になります。このため、parser 構造体に context *Node フィールドが追加されました。このフィールドは、ParseFragment 関数が呼び出された際に、引数として渡されたコンテキスト要素を保持します。

  2. resetInsertionMode の変更: resetInsertionMode 関数は、要素スタックを逆順に辿って適切な挿入モードを決定しますが、フラグメントパースの場合、スタックの最下層(インデックス0)がドキュメントノードではなく、コンテキスト要素になる可能性があります。この変更により、p.context != nil の場合にスタックの最下層を p.context に設定することで、フラグメントパースのコンテキストを正しく考慮するように修正されました。

  3. Parse 関数の内部ロジックの分離: 既存の Parse 関数は、完全なHTMLドキュメントをパースするためのものでした。このコミットでは、Parse 関数の主要なパースロジックが (p *parser) parse() error という新しいプライベートメソッドに抽出されました。これにより、Parse 関数と新しく追加される ParseFragment 関数の両方で共通のパースロジックを再利用できるようになりました。

  4. ParseFragment 関数の新規追加: ParseFragment(r io.Reader, context *Node) ([]*Node, error) 関数が追加されました。この関数は、HTMLフラグメントをパースするための主要なエントリポイントです。

    • 新しい parser インスタンスを作成し、context フィールドに渡されたコンテキスト要素を設定します。
    • 特定のコンテキスト要素(例: iframe, noembed, noscript, plaintext, script, style, title, textarea, xmp)の場合、トークナイザーの rawTag を設定し、その要素の特殊なコンテンツモデル(RCDATAなど)を考慮するようにします。
    • フラグメントパースでは、ルート要素として <html> 要素が仮定され、要素スタックにプッシュされます。
    • コンテキスト要素からフォーム要素を探索し、パーサーの form フィールドに設定します。これは、フォーム関連の要素がフラグメント内に存在する場合に重要です。
    • 抽出された p.parse() メソッドを呼び出して実際のパースを実行します。
    • パース結果から、コンテキスト要素の有無に応じて適切な親ノードから子ノードを抽出し、それらの親ポインタをクリアして、ノードのリストとして返します。
  5. テストケースの更新 (parse_test.go):

    • readParseTest 関数が、テストデータからコンテキスト要素を読み取るために context string を返すように変更されました。
    • TestParser 関数内で、context が空でない場合に ParseFragment を呼び出すロジックが追加されました。これにより、tests4.dat のようなフラグメントテストケースを正しく処理できるようになりました。
    • renderTestBlacklistcontext != "" の条件が追加され、フラグメントテストケースではレンダリングと再パースのチェックをスキップするようにしました。これは、フラグメントパースの結果が完全なドキュメントのレンダリングとは異なる場合があるためです。

これらの変更により、Goの html パッケージはHTML5仕様に準拠したHTMLフラグメントパース機能をサポートし、より多様なHTML処理シナリオに対応できるようになりました。

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

このコミットで変更されたファイルは以下の2つです。

  1. src/pkg/html/parse.go: HTMLパーサーの主要なロジックが含まれるファイル。

    • parser 構造体に context フィールドが追加。
    • resetInsertionMode 関数が context フィールドを考慮するように変更。
    • Parse 関数の内部パースロジックが (p *parser) parse() error メソッドとして分離。
    • ParseFragment 関数が新規追加。
  2. src/pkg/html/parse_test.go: HTMLパーサーのテストケースが含まれるファイル。

    • readParseTest 関数が context 文字列を返すように変更。
    • TestParser 関数内で ParseFragment を使用してフラグメントテストケースを処理するロジックが追加。

変更行数: 127行の追加、30行の削除。

コアとなるコードの解説

src/pkg/html/parse.go

// parser struct に context フィールドを追加
type parser struct {
	// ... 既存のフィールド ...
	fosterParenting bool
	// quirks is whether the parser is operating in "quirks mode."
	quirks bool
	// context is the context element when parsing an HTML fragment
	// (section 11.4).
	context *Node // ★ 新規追加
}

// resetInsertionMode の変更
func (p *parser) resetInsertionMode() {
	for i := len(p.oe) - 1; i >= 0; i-- {
		n := p.oe[i]
		// HTMLフラグメントパースの場合、スタックの最下層が context 要素になる
		if i == 0 && p.context != nil { // ★ 変更箇所
			n = p.context
		}
		// ... 既存のロジック ...
	}
}

// Parse 関数の内部ロジックを parse() メソッドに分離
// 変更前: Parse 関数内に直接パースループがあった
// 変更後:
func (p *parser) parse() error { // ★ 新規追加
	// Iterate until EOF. Any other error will cause an early return.
	consumed := true
	for {
		// ... 既存のパースループロジック ...
	}
	return nil
}

// Parse 関数の変更 (parse() メソッドの呼び出し)
func Parse(r io.Reader) (*Node, error) {
	p := &parser{
		tokenizer: NewTokenizer(r),
		doc: &Node{
			Type: DocumentNode,
		},
		scripting:  true,
		framesetOK: true,
		im:         initialIM,
	}
	err := p.parse() // ★ parse() メソッドを呼び出す
	if err != nil {
		return nil, err
	}
	return p.doc, nil
}

// ParseFragment 関数の新規追加
func ParseFragment(r io.Reader, context *Node) ([]*Node, error) { // ★ 新規追加
	p := &parser{
		tokenizer: NewTokenizer(r),
		doc: &Node{
			Type: DocumentNode,
		},
		scripting: true,
		context:   context, // ★ context フィールドを設定
	}

	// 特定のコンテキスト要素に対する rawTag の設定
	if context != nil {
		switch context.Data {
		case "iframe", "noembed", "noframes", "noscript", "plaintext", "script", "style", "title", "textarea", "xmp":
			p.tokenizer.rawTag = context.Data
		}
	}

	// フラグメントパースのための仮のルート要素 (html) を設定
	root := &Node{
		Type: ElementNode,
		Data: "html",
	}
	p.doc.Add(root)
	p.oe = nodeStack{root}
	p.resetInsertionMode() // ★ 挿入モードのリセット

	// コンテキスト要素からフォーム要素を探索
	for n := context; n != nil; n = n.Parent {
		if n.Type == ElementNode && n.Data == "form" {
			p.form = n
			break
		}
	}

	err := p.parse() // ★ 実際のパースを実行
	if err != nil {
		return nil, err
	}

	// パース結果からノードを抽出し、親ポインタをクリアして返す
	parent := p.doc
	if context != nil {
		parent = root
	}
	result := parent.Child
	parent.Child = nil
	for _, n := range result {
		n.Parent = nil
	}
	return result, nil
}

src/pkg/html/parse_test.go

// readParseTest 関数のシグネチャ変更 (context を追加)
func readParseTest(r *bufio.Reader) (text, want, context string, err error) { // ★ 変更箇所
	// ... 既存のロジック ...
	// #document-fragment セクションの読み込みを追加
	if string(line) == "#document-fragment\\n" { // ★ 新規追加
		line, err = r.ReadSlice('\n')
		if err != nil {
			return "", "", "", err
		}
		context = strings.TrimSpace(string(line))
		line, err = r.ReadSlice('\n')
		if err != nil {
			return "", "", "", err
		}
	}
	// ... 既存のロジック ...
	return text, string(b), context, nil // ★ context を返す
}

// TestParser 関数の変更 (ParseFragment の使用)
func TestParser(t *testing.T) {
	// ... 既存のロジック ...
	for _, tf := range testFiles {
		// ... 既存のファイル読み込みロジック ...
		for i := 0; i != tf.n; i++ {
			text, want, context, err := readParseTest(r) // ★ context を受け取る
			// ... エラーハンドリング ...

			var doc *Node
			if context == "" { // ★ context がない場合は通常の Parse を使用
				doc, err = Parse(strings.NewReader(text))
				if err != nil {
					t.Fatal(err)
				}
			} else { // ★ context がある場合は ParseFragment を使用
				contextNode := &Node{
					Type: ElementNode,
					Data: context,
				}
				nodes, err := ParseFragment(strings.NewReader(text), contextNode) // ★ ParseFragment を呼び出す
				if err != nil {
					t.Fatal(err)
				}
				doc = &Node{
					Type: DocumentNode,
				}
				for _, n := range nodes { // ★ ParseFragment の結果を DocumentNode に追加
					doc.Add(n)
				}
			}
			// ... 既存の比較ロジック ...
			if renderTestBlacklist[text] || context != "" { // ★ context がある場合はレンダリングテストをスキップ
				continue
			}
			// ... 既存のレンダリングと再パースのチェック ...
		}
	}
}

これらの変更により、html パッケージはHTMLフラグメントのパースをサポートし、innerHTML のような動的なHTMLコンテンツの処理や、特定の要素の内部構造の解析など、より高度なHTML処理が可能になりました。

関連リンク

参考にした情報源リンク