[インデックス 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文字列)を解析するための特殊なアルゴリズムです。このアルゴリズムの主な特徴は以下の通りです。
- コンテキスト要素 (Context Element): フラグメントがどのHTML要素の内部コンテンツとして扱われるかを指定します。例えば、
<title>
要素の内部に挿入されるフラグメントは、通常のHTMLとは異なるパースルール(RCDATA状態)が適用されます。このコンテキスト要素は、パーサーの初期状態や挿入モードに影響を与えます。 - DocumentFragmentの生成: パースされたノードは、通常、
DocumentFragment
と呼ばれる軽量なコンテナに格納されます。これは、DOMツリーの一部として扱われることなく、複数のノードを効率的に操作するためのものです。 - スクリプトの扱い: フラグメント内の
<script>
要素は、パース時にすぐに実行されるのではなく、already started
フラグがfalse
に設定され、parser document
がnull
に設定されます。これにより、フラグメントがライブDOMに挿入されたときに適切に処理されるようになります。 - エラー回復: 完全なドキュメントと同様に、フラグメントパースも一般的なHTMLエラーからの回復を考慮して設計されています。
挿入モード (Insertion Mode)
HTMLパーサーは、現在の状態に応じて「挿入モード」と呼ばれる様々なモードで動作します。例えば、initialIM
(初期モード)、inHeadIM
(head要素内モード)、inBodyIM
(body要素内モード) などがあります。これらのモードは、次にどのようなトークンが期待され、どのようにDOMツリーにノードが追加されるかを決定します。
resetInsertionMode
関数
resetInsertionMode
関数は、HTMLパーサーが特定の状況下で現在の挿入モードをリセットするために使用されます。これは、特にエラー回復や、HTMLフラグメントのパース時にコンテキスト要素に基づいて適切な挿入モードを確立するために重要です。この関数は、要素スタックを逆順に辿り、適切な挿入モードを見つけ出して設定します。
技術的詳細
このコミットは、Go言語の html
パッケージにおけるHTMLフラグメントパースのサポートを導入するために、主に以下の技術的変更を行っています。
-
parser
構造体へのcontext
フィールドの追加: HTMLフラグメントパースでは、フラグメントがどの要素のコンテキストでパースされるかが重要になります。このため、parser
構造体にcontext *Node
フィールドが追加されました。このフィールドは、ParseFragment
関数が呼び出された際に、引数として渡されたコンテキスト要素を保持します。 -
resetInsertionMode
の変更:resetInsertionMode
関数は、要素スタックを逆順に辿って適切な挿入モードを決定しますが、フラグメントパースの場合、スタックの最下層(インデックス0)がドキュメントノードではなく、コンテキスト要素になる可能性があります。この変更により、p.context != nil
の場合にスタックの最下層をp.context
に設定することで、フラグメントパースのコンテキストを正しく考慮するように修正されました。 -
Parse
関数の内部ロジックの分離: 既存のParse
関数は、完全なHTMLドキュメントをパースするためのものでした。このコミットでは、Parse
関数の主要なパースロジックが(p *parser) parse() error
という新しいプライベートメソッドに抽出されました。これにより、Parse
関数と新しく追加されるParseFragment
関数の両方で共通のパースロジックを再利用できるようになりました。 -
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()
メソッドを呼び出して実際のパースを実行します。 - パース結果から、コンテキスト要素の有無に応じて適切な親ノードから子ノードを抽出し、それらの親ポインタをクリアして、ノードのリストとして返します。
- 新しい
-
テストケースの更新 (
parse_test.go
):readParseTest
関数が、テストデータからコンテキスト要素を読み取るためにcontext string
を返すように変更されました。TestParser
関数内で、context
が空でない場合にParseFragment
を呼び出すロジックが追加されました。これにより、tests4.dat
のようなフラグメントテストケースを正しく処理できるようになりました。renderTestBlacklist
にcontext != ""
の条件が追加され、フラグメントテストケースではレンダリングと再パースのチェックをスキップするようにしました。これは、フラグメントパースの結果が完全なドキュメントのレンダリングとは異なる場合があるためです。
これらの変更により、Goの html
パッケージはHTML5仕様に準拠したHTMLフラグメントパース機能をサポートし、より多様なHTML処理シナリオに対応できるようになりました。
コアとなるコードの変更箇所
このコミットで変更されたファイルは以下の2つです。
-
src/pkg/html/parse.go
: HTMLパーサーの主要なロジックが含まれるファイル。parser
構造体にcontext
フィールドが追加。resetInsertionMode
関数がcontext
フィールドを考慮するように変更。Parse
関数の内部パースロジックが(p *parser) parse() error
メソッドとして分離。ParseFragment
関数が新規追加。
-
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処理が可能になりました。
関連リンク
- Go CL (Code Review) へのリンク: https://golang.org/cl/5447055
参考にした情報源リンク
- HTML Living Standard (HTML5) - HTML fragment parsing algorithm:
- Mozilla Developer Network (MDN) - DocumentFragment:
- W3C HTML5 (旧仕様) - HTML fragment parsing algorithm:
- Web search results for "HTML fragment parsing algorithm" (provided by the tool):
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHIgf7GYWxdtrAIdZJ4EdtMJEsv7NzjuIIgWeJb5bhJcrszbl9a-1XgrKyupEcOD68up1wprChplsodmxeK__KNQNd-ou7RMJTnkOgIaiRENZu3RS9xnntDk3AZAngOH30NAdZmKxDE7le8N4GaPaiQR0_y8x0U4rsE7lP9mZpOc6R6uJrKnSXmjV8=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEN2rVhkkWYhiiVW5zN53kQv5wKJVEbfYTcLuSK7aTkImBQLwRK4bgm41GJiouaG06LmOR4BxH12xf_GlmiDGBcujnVqXKIsnNVcjiCZ9duxRJ1Rl3oFp1435qrZIh5iZqzNJhS8Bkr4_hnn_n77szweeiMu-4yUOVjqj8joUfb
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFpXvhwT8omO72GGWYDCDCYxpHy0IV8P4MKmGk444oybhJQ2E7cd4kHP_r9WzTRAajDO9kzsDScJuTOx-5U0jXf92R0Y49p0stgwGoBPqddPV8HMFPh0-Upjykj3CqE9pA13pvpaVHUoonETefEuLJuhh4Y2nun5cZsvQcacgELTc65HuQa0-E=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGCIz7jFY1SDJwS_6QUMkANUOt9YL8YUtnvCoBBsdn0uPleaRUhvB3VxEHNqk6jaktKkDpP5pCkUHO7h3WJWc9QXMOBKDbSClL46DgVxEEqZDaNFHHgyq9PGv0WABhj6wFj2g5Hh7my7uo_5zXWCA==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEj49o0yuBZ66BGVKAs7A9FWjderyblecRKSEhFmR7tyzh_CkkwJvO-_4MrXjBhUtudK3im2yJ25FvRKdUzlvZIpDrsqqqPSZYD15Ptv3v1tcIQD4Oa3Pl6gNPYP9LuVHzWiuap73c=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHmG8LgDjwsG8jw_IZROgP_IdUIgxxi1Wm56ibCGwoGlfsMe6BfVBkZzJ_btW4rQQCVNl_753JHHFR8jQPWphtq9zpgihtMeBjI0im5eTjNqdEApYu7YG9PtsscjCPevyuHJR4i6Ljz0Vq3A-9mD_2MVGsykvXzKhXlFUsr0Q==