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

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

このコミットは、Go言語の標準ライブラリである html パッケージにおけるHTMLパーサーの改善に関するものです。特に、HTML5の仕様に準拠した frameset 要素のパース処理を正確に行うための変更が含まれています。これにより、特定のHTML構造(特に framesetnoframes を含むもの)が正しくDOMツリーとして構築されるようになり、関連するテストケースがパスするようになりました。

コミット

commit e9e874b7fcc722e2e9af942761b8fc2cd8e2c240
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Thu Nov 10 23:56:13 2011 +1100

    html: parse framesets
    
    Pass tests1.dat, test 106:
    <frameset><frame><frameset><frame></frameset><noframes></noframes></frameset>
    
    | <html>
    |   <head>
    |   <frameset>
    |     <frame>
    |     <frameset>
    |       <frame>
    |     <noframes>
    
    Also pass test 107:
    <h1><table><td><h3></table><h3></h1>
    
    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/5373050

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

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

元コミット内容

このコミットは、Go言語の html パッケージにおいて、frameset 要素のパース処理を実装し、関連するテストケース(tests1.dat のテスト106およびテスト107)をパスするように修正するものです。

具体的には、以下のHTML構造が正しくパースされることを目的としています。

  • テスト106: <frameset><frame><frameset><frame></frameset><noframes></noframes></frameset>
    • この構造が、期待されるDOMツリー(<html><head><frameset><frame><frameset><frame><noframes>)として表現されることを確認します。
  • テスト107: <h1><table><td><h3></table><h3></h1>
    • このテストは、frameset とは直接関係ありませんが、HTMLパーサーの堅牢性を確認するためのものです。

変更の背景

HTMLのパースは、ウェブブラウザがウェブページを表示するために不可欠なプロセスです。特にHTML5の仕様では、エラーを含む不完全なHTMLであっても、一貫した方法でパースするための詳細なアルゴリズムが定義されています。Go言語の html パッケージは、このHTML5パースアルゴリズムに準拠することを目指しています。

このコミットが行われた当時、frameset 要素のパース処理が不完全であったため、特定のHTML構造が正しくDOMツリーとして表現されない問題がありました。frameset は、複数のHTMLドキュメントを一つのウィンドウ内に表示するための古いHTML要素ですが、HTML5でも互換性のためにそのパースルールが定義されています。

このコミットの目的は、frameset 要素とその関連要素(frame, noframes)がHTML5のパースアルゴリズムに従って正確に処理されるように、パーサーの「挿入モード(insertion mode)」ロジックを拡張することでした。これにより、Goの html パッケージがより堅牢で標準準拠のHTMLパーサーとなることが期待されました。

前提知識の解説

HTML5パースアルゴリズム

HTML5のパースアルゴリズムは、ウェブブラウザがHTMLドキュメントを解析し、DOM(Document Object Model)ツリーを構築するための詳細な手順を定めたものです。このアルゴリズムは、非常に堅牢であり、構文エラーを含むHTMLでも一貫した結果を生成するように設計されています。

主要な概念として、以下のものがあります。

  • トークナイゼーション(Tokenization): 入力されたHTML文字列を、タグ、属性、テキストなどの「トークン」に分解するプロセスです。
  • ツリー構築(Tree Construction): トークナイザーから受け取ったトークンを基に、DOMツリーを構築するプロセスです。
  • 挿入モード(Insertion Mode): ツリー構築アルゴリズムの中心的な概念です。パーサーは常に特定の「挿入モード」にあり、このモードによって、次に受け取るトークンがどのように処理されるかが決定されます。例えば、<head> タグ内では「in head」モード、<body> タグ内では「in body」モードなどがあります。各モードには、特定のタグが来た場合の処理(要素の挿入、スタックからのポップ、エラー処理など)が詳細に定義されています。

frameset, frame, noframes 要素

これらの要素は、HTML4以前でウェブページを分割するために使用されていました。HTML5では非推奨とされていますが、後方互換性のためにパースルールが定義されています。

  • <frameset>: 複数のフレームを定義するためのコンテナ要素です。<body> 要素の代わりに使用され、ウィンドウを複数の領域に分割します。
  • <frame>: frameset 内で、個々のフレーム(別のHTMLドキュメントを表示する領域)を定義します。
  • <noframes>: frameset をサポートしないブラウザ向けに、代替コンテンツを提供するための要素です。frameset 内に配置されます。

これらの要素は、通常のHTML要素とは異なるパースルールを持つため、パーサーはこれらの要素を検出した際に、適切な挿入モードに切り替える必要があります。

技術的詳細

このコミットの技術的な核心は、Go言語の html パッケージにおけるHTML5パースアルゴリズムの実装に、frameset 関連の挿入モードとそれらの遷移ルールを追加した点にあります。

HTML5パースアルゴリズムのセクション11.2.5.4には、様々な挿入モードが定義されており、このコミットでは特に以下のモードが追加または修正されています。

  • inFramesetIM (In frameset insertion mode): frameset 要素がオープンされているときにパーサーが遷移するモードです。このモードでは、frame やネストされた frameset、あるいは noframes などの要素が特別に処理されます。
  • afterFramesetIM (After frameset insertion mode): frameset 要素が閉じられた後にパーサーが遷移するモードです。このモードでは、htmlnoframes などの特定の要素が処理されます。
  • afterAfterFramesetIM (After after frameset insertion mode): afterFramesetIM からさらに遷移する可能性のあるモードで、ドキュメントの終わりに近い状態での frameset 関連の処理を扱います。

コミットの変更点を見ると、parse.go 内の resetInsertionMode 関数が frameset 要素を検出した際に inFramesetIM に遷移するように修正されています。また、afterHeadIM 関数も frameset タグを検出した場合に inFramesetIM に遷移し、要素を追加するロジックが追加されています。

新しい挿入モード関数 (inFramesetIM, afterFramesetIM, afterAfterFramesetIM) は、それぞれ以下のルールに従ってトークンを処理します。

  • コメントトークン: コメントノードとして子に追加されます。
  • 開始タグトークン:
    • html: inBodyIM または inHeadIM のルールを適用して処理されます。
    • frameset: 新しい frameset 要素が追加されます。
    • frame: frame 要素が追加され、すぐにポップされます(自己終了タグとして扱われるため)。
    • noframes: inHeadIM のルールを適用して処理されます。
  • 終了タグトークン:
    • frameset: 現在の要素スタックのトップが frameset であればポップされ、必要に応じて afterFramesetIM に遷移します。
    • html: afterAfterFramesetIM に遷移します。
  • その他のトークン: 基本的に無視されます。

これらの変更により、frameset を含む複雑なHTML構造が、HTML5の仕様に厳密に従ってDOMツリーとして構築されるようになります。

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

--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -321,7 +321,7 @@ func (p *parser) resetInsertionMode() insertionMode {
 		case "body":
 			return inBodyIM
 		case "frameset":
-			// TODO: return inFramesetIM
+			return inFramesetIM
 		case "html":
 			return beforeHeadIM
 		}
@@ -517,7 +517,8 @@ func afterHeadIM(p *parser) (insertionMode, bool) {
 			attr = p.tok.Attr
 			framesetOK = false
 		case "frameset":
-			// TODO.
+			p.addElement(p.tok.Data, p.tok.Attr)
+			return inFramesetIM, true
 		case "base", "basefont", "bgsound", "link", "meta", "noframes", "script", "style", "title":
 			p.oe = append(p.oe, p.head)
 			defer p.oe.pop()
@@ -646,7 +647,7 @@ func inBodyIM(p *parser) (insertionMode, bool) {
 				break
 			}
 			p.popUntil(buttonScopeStopTags, "p")
-			p.addElement("li", p.tok.Attr)
+			p.addElement(p.tok.Data, p.tok.Attr)
 		case "optgroup", "option":
 			if p.top().Data == "option" {
 				p.oe.pop()
@@ -1169,6 +1170,69 @@ func afterBodyIM(p *parser) (insertionMode, bool) {
 	return afterBodyIM, true
 }
 
+// Section 11.2.5.4.19.
+func inFramesetIM(p *parser) (insertionMode, bool) {
+	switch p.tok.Type {
+	case CommentToken:
+		p.addChild(&Node{
+			Type: CommentNode,
+			Data: p.tok.Data,
+		})
+	case StartTagToken:
+		switch p.tok.Data {
+		case "html":
+			return useTheRulesFor(p, inFramesetIM, inBodyIM)
+		case "frameset":
+			p.addElement(p.tok.Data, p.tok.Attr)
+		case "frame":
+			p.addElement(p.tok.Data, p.tok.Attr)
+			p.oe.pop()
+			p.acknowledgeSelfClosingTag()
+		case "noframes":
+			return useTheRulesFor(p, inFramesetIM, inHeadIM)
+		}
+	case EndTagToken:
+		switch p.tok.Data {
+		case "frameset":
+			if p.oe.top().Data != "html" {
+				p.oe.pop()
+				if p.oe.top().Data != "frameset" {
+					return afterFramesetIM, true
+				}
+			}
+		}
+	default:
+		// Ignore the token.
+	}
+	return inFramesetIM, true
+}
+
+// Section 11.2.5.4.20.
+func afterFramesetIM(p *parser) (insertionMode, bool) {
+	switch p.tok.Type {
+	case CommentToken:
+		p.addChild(&Node{
+			Type: CommentNode,
+			Data: p.tok.Data,
+		})
+	case StartTagToken:
+		switch p.tok.Data {
+		case "html":
+			return useTheRulesFor(p, inFramesetIM, inBodyIM)
+		case "noframes":
+			return useTheRulesFor(p, inFramesetIM, inHeadIM)
+		}
+	case EndTagToken:
+		switch p.tok.Data {
+		case "html":
+			return afterAfterFramesetIM, true
+		}
+	default:
+		// Ignore the token.
+	}
+	return afterFramesetIM, true
+}
+
 // Section 11.2.5.4.21.
 func afterAfterBodyIM(p *parser) (insertionMode, bool) {
 	switch p.tok.Type {
@@ -1191,6 +1255,27 @@ func afterAfterBodyIM(p *parser) (insertionMode, bool) {
 	return inBodyIM, false
 }
 
+// Section 11.2.5.4.22.
+func afterAfterFramesetIM(p *parser) (insertionMode, bool) {
+	switch p.tok.Type {
+	case CommentToken:
+		p.addChild(&Node{
+			Type: CommentNode,
+			Data: p.tok.Data,
+		})
+	case StartTagToken:
+		switch p.tok.Data {
+		case "html":
+			return useTheRulesFor(p, afterAfterFramesetIM, inBodyIM)
+		case "noframes":
+			return useTheRulesFor(p, afterAfterFramesetIM, inHeadIM)
+		}
+	default:
+		// Ignore the token.
+	}
+	return afterAfterFramesetIM, true
+}
+
 // Parse returns the parse tree for the HTML from the given Reader.
 // The input is assumed to be UTF-8 encoded.
 func Parse(r io.Reader) (*Node, error) {
diff --git a/src/pkg/html/parse_test.go b/src/pkg/html/parse_test.go
index 8cef0fa8e3..0e93a9de84 100644
--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -133,7 +133,7 @@ func TestParser(t *testing.T) {
 		\tn int
 		}{
 		// TODO(nigeltao): Process all the test cases from all the .dat files.
-		{"tests1.dat", 106},
+		{"tests1.dat", 108},
 		{"tests2.dat", 0},
 		{"tests3.dat", 0},
 	}

コアとなるコードの解説

src/pkg/html/parse.go

  1. resetInsertionMode 関数の修正:

    • 以前は frameset タグが検出された際に // TODO: return inFramesetIM とコメントアウトされていましたが、このコミットで return inFramesetIM が有効化されました。これにより、パーサーが frameset 要素の内部にいることを正しく認識し、適切な挿入モードに切り替わるようになります。
  2. afterHeadIM 関数の修正:

    • frameset タグが afterHeadIM モード(<head> タグの直後)で検出された場合、以前は // TODO. となっていましたが、このコミットで p.addElement(p.tok.Data, p.tok.Attr) を呼び出して frameset 要素をDOMツリーに追加し、その後 inFramesetIM に遷移するように変更されました。これは、HTML5の仕様で framesethead の後に続く場合の処理を反映しています。
  3. inBodyIM 関数の修正:

    • li 要素の追加ロジックが p.addElement("li", p.tok.Attr) から p.addElement(p.tok.Data, p.tok.Attr) に変更されています。これは、li だけでなく、現在のトークンのデータ(タグ名)を汎用的に使用するように修正されたもので、より柔軟な要素追加を可能にします。この変更は frameset と直接関係ありませんが、パーサーの一般的な改善の一部です。
  4. 新しい挿入モード関数の追加:

    • inFramesetIM: HTML5仕様のセクション11.2.5.4.19「In frameset insertion mode」に対応する関数です。
      • コメントトークンは子ノードとして追加されます。
      • 開始タグ htmlinBodyIM のルールで処理されます。
      • 開始タグ frameset は新しい frameset 要素として追加されます。
      • 開始タグ frameframe 要素として追加され、すぐにスタックからポップされます(HTML5では frame は自己終了要素として扱われるため)。また、p.acknowledgeSelfClosingTag() が呼び出され、自己終了タグとして認識されます。
      • 開始タグ noframesinHeadIM のルールで処理されます。
      • 終了タグ frameset は、要素スタックのトップが html でない限り、スタックからポップされます。もしポップされた要素が frameset でなければ、afterFramesetIM に遷移します。
      • その他のトークンは無視されます。
    • afterFramesetIM: HTML5仕様のセクション11.2.5.4.20「After frameset insertion mode」に対応する関数です。
      • コメントトークンは子ノードとして追加されます。
      • 開始タグ htmlinBodyIM のルールで処理されます。
      • 開始タグ noframesinHeadIM のルールで処理されます。
      • 終了タグ htmlafterAfterFramesetIM に遷移します。
      • その他のトークンは無視されます。
    • afterAfterFramesetIM: HTML5仕様のセクション11.2.5.4.22「After after frameset insertion mode」に対応する関数です。
      • コメントトークンは子ノードとして追加されます。
      • 開始タグ htmlinBodyIM のルールで処理されます。
      • 開始タグ noframesinHeadIM のルールで処理されます。
      • その他のトークンは無視されます。

これらの新しい挿入モードと既存のモードからの遷移ルールの追加により、frameset を含むHTMLドキュメントがHTML5の仕様に厳密に従ってパースされ、正しいDOMツリーが構築されるようになります。

src/pkg/html/parse_test.go

  1. テストケース番号の更新:
    • TestParser 関数内の tests1.dat のテストケース数が 106 から 108 に変更されています。これは、tests1.dat ファイル内のテストケースの総数が増加したか、またはこのコミットで追加されたテストケースが tests1.dat の末尾に追加されたことを示唆しています。この変更自体はパースロジックの変更ではなく、テストスイートの更新です。

関連リンク

参考にした情報源リンク