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

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

このコミットは、Go言語の標準ライブラリ html パッケージにおけるHTMLパーサーの挙動を修正するものです。具体的には、<body> タグの内部に <frameset> タグが出現した場合のパース処理を改善し、関連するテストケースを通過するように変更しています。

変更されたファイルは以下の通りです。

  • src/pkg/html/parse.go: HTMLパーサーの主要なロジックが含まれるファイル。inBodyIM (in body insertion mode) 関数に frameset タグを処理するための新しいロジックが追加されました。
  • src/pkg/html/parse_test.go: HTMLパーサーのテストファイル。tests6.dat のテストケースの期待値が更新されました。

コミット

commit 99fed2be279934f0e4d806833f810a3ac78f0e60
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Wed Jan 4 09:51:15 2012 +1100

    html: parse <frameset> inside body
    
    Pass tests6.dat, test 47:
    <param><frameset></frameset>
    
    Also pass remaining tests in tests6.dat.
    
    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/5489136

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

https://github.com/golang/go/commit/99fed2be279934f0e4d806833f810a3ac78f0e60

元コミット内容

html: parse <frameset> inside body

このコミットは、html パッケージが <body> タグの内部にある <frameset> タグを正しくパースできるようにするものです。

具体的には、tests6.dat のテスト47(<param><frameset></frameset>)および tests6.dat 内の残りのテストケースを通過するように修正されました。

変更の背景

HTMLの仕様において、<frameset> 要素は <html> 要素の直下、または <body> 要素の代わりに配置されるべきであり、<body> 要素の内部に直接配置されることは通常ありません。しかし、ウェブ上には非標準的なマークアップや、古いHTMLの慣習に従ったページが存在するため、堅牢なHTMLパーサーはこのような「不正な」構造も適切に処理できる必要があります。

このコミット以前のGo言語の html パッケージのパーサーは、<body> 要素の内部に <frameset> 要素が出現した場合に、HTML5のパースアルゴリズムの特定のルールに従って正しく処理できていませんでした。その結果、tests6.dat のテスト47を含むいくつかのテストケースが失敗していました。

この変更の目的は、HTML5のパースアルゴリズムに厳密に従い、<body> 内の <frameset> を検出した際に、既存の <body> 要素をDOMツリーから削除し、代わりに <frameset> を適切な位置に挿入することで、より正確なパース結果を提供することにあります。これにより、パーサーの堅牢性と互換性が向上します。

前提知識の解説

HTML5 パースアルゴリズム

HTML5の仕様は、ウェブブラウザがHTMLドキュメントをどのようにパースし、DOMツリーを構築するかについて非常に詳細なアルゴリズムを定義しています。このアルゴリズムは、トークナイゼーション(文字ストリームをトークンに変換)とツリー構築(トークンをDOMノードに変換し、ツリーに配置)の2つの主要なフェーズに分かれます。

ツリー構築フェーズでは、「挿入モード (Insertion Mode)」という概念が中心となります。これは、パーサーが現在処理しているHTMLのコンテキストに基づいて、次にどのトークンをどのように処理するかを決定する状態機械です。例えば、<head> タグの中では「in head」モード、<body> タグの中では「in body」モードなどがあります。

<body> 要素と <frameset> 要素

  • <body> 要素: HTMLドキュメントの可視コンテンツ(テキスト、画像、リンクなど)を格納する主要なコンテナです。通常、<html> 要素の直下、<head> 要素の後に配置されます。
  • <frameset> 要素: HTML4以前で、ブラウザウィンドウを複数のフレームに分割するために使用された要素です。各フレームは独立したHTMLドキュメントを表示できます。HTML5では非推奨となり、代わりに <iframe> やCSS、JavaScriptを用いたレイアウトが推奨されています。

HTML5のパースアルゴリズムでは、<body> 要素が既に開いている状態で <frameset> 要素の開始タグが検出された場合、特別な処理が定義されています。これは、<body><frameset> が相互に排他的なルートレベルのコンテンツコンテナであるためです。このシナリオでは、既存の <body> 要素をDOMツリーから削除し、代わりに <frameset> 要素を <html> 要素の直下に挿入し、挿入モードを「in frameset」に切り替える必要があります。

Go言語の html パッケージ

Go言語の html パッケージは、HTML5の仕様に準拠したHTMLパーサーを提供します。このパッケージは、ウェブスクレイピング、HTMLテンプレート処理、HTMLコンテンツのサニタイズなど、様々な用途で利用されます。内部的には、HTML5パースアルゴリズムの挿入モードを実装しており、inBodyIM のような関数は、特定の挿入モードにおけるトークン処理ロジックをカプセル化しています。

技術的詳細

このコミットの核心は、src/pkg/html/parse.go 内の inBodyIM 関数における <frameset> タグの処理ロジックの追加です。

inBodyIM は、パーサーが「in body insertion mode」にあるときに呼び出される関数です。このモードでは、通常、<body> 要素のコンテンツがパースされます。

変更前は、<body> 内で <frameset> が検出された場合、HTML5の仕様で定義されている特定の回復メカニズムが適切に適用されていませんでした。HTML5の仕様では、<body> 要素が既に開いている状態で <frameset> 開始タグが検出された場合、以下のステップが推奨されます。

  1. <body> 要素のクローズ: 現在開いている <body> 要素を閉じます。
  2. <body> 要素の削除: DOMツリーから <body> 要素を削除します。これは、<body><frameset> が同時に存在できないためです。
  3. <frameset> の挿入: 新しい <frameset> 要素を <html> 要素の直下に挿入します。
  4. 挿入モードの変更: パーサーの挿入モードを「in frameset insertion mode」に切り替えます。

このコミットは、上記の仕様に沿って inBodyIM 関数にロジックを追加することで、この特定のケースを正確に処理するようにします。

コード内の p.framesetOK は、パーサーが <frameset> を受け入れる準備ができているかどうかを示すフラグであると推測されます。また、p.oe は「open elements」スタック(現在開いている要素のスタック)を表し、p.oe[1] は通常 <body> 要素を指します(p.oe[0]<html>)。

新しいロジックは、以下の条件をチェックします。

  • !p.framesetOK: <frameset> の挿入が許可されていない場合。
  • len(p.oe) < 2: オープン要素スタックに <html><body> が存在しない場合。
  • p.oe[1].Data != "body": スタックの2番目の要素が <body> でない場合。

これらの条件のいずれかが真であれば、トークンは無視されます。これは、<frameset> が不正なコンテキストにあるか、既に処理済みであることを意味する可能性があります。

条件が満たされない場合(つまり、<body> 内に <frameset> が出現し、処理すべき有効なケースである場合)、以下の処理が行われます。

  1. body := p.oe[1]: オープン要素スタックから <body> 要素を取得します。
  2. if body.Parent != nil { body.Parent.Remove(body) }: <body> 要素が親を持つ場合、DOMツリーから <body> 要素を削除します。
  3. p.oe = p.oe[:1]: オープン要素スタックから <body> 要素をポップし、<html> 要素のみを残します。
  4. p.addElement(p.tok.Data, p.tok.Attr): 新しい <frameset> 要素をDOMツリーに追加します。この際、<html> が親となります。
  5. p.im = inFramesetIM: パーサーの挿入モードを inFramesetIM (in frameset insertion mode) に切り替えます。

これにより、HTML5の仕様に準拠した正確なDOMツリーが構築され、tests6.dat のテストケースが正しくパースされるようになります。

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

src/pkg/html/parse.go

--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -749,6 +749,19 @@ func inBodyIM(p *parser) bool {
 				copyAttributes(body, p.tok)
 			}
 		}
+		case "frameset":
+			if !p.framesetOK || len(p.oe) < 2 || p.oe[1].Data != "body" {
+				// Ignore the token.
+				return true
+			}
+			body := p.oe[1]
+			if body.Parent != nil {
+				body.Parent.Remove(body)
+			}
+			p.oe = p.oe[:1]
+			p.addElement(p.tok.Data, p.tok.Attr)
+			p.im = inFramesetIM
+			return true
 		case "base", "basefont", "bgsound", "command", "link", "meta", "noframes", "script", "style", "title":
 			return inHeadIM(p)
 		case "image":

src/pkg/html/parse_test.go

--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -183,7 +183,7 @@ func TestParser(t *testing.T) {
 		{"tests3.dat", -1},
 		{"tests4.dat", -1},
 		{"tests5.dat", -1},
-		{"tests6.dat", 47},
+		{"tests6.dat", -1},
 		{"tests10.dat", 30},
 	}
 	for _, tf := range testFiles {

コアとなるコードの解説

src/pkg/html/parse.go の変更

inBodyIM 関数内の switch ステートメントに、新しい case "frameset": ブロックが追加されました。

  1. 条件チェック:

    if !p.framesetOK || len(p.oe) < 2 || p.oe[1].Data != "body" {
        // Ignore the token.
        return true
    }
    

    この条件は、<frameset> タグが処理されるべきではない状況を特定します。

    • !p.framesetOK: パーサーが <frameset> を受け入れる状態にない場合。
    • len(p.oe) < 2: オープン要素スタックに <html><body> の両方が存在しない場合(つまり、<body> が開いていないか、DOMツリーの構造が想定と異なる場合)。
    • p.oe[1].Data != "body": オープン要素スタックの2番目の要素が <body> でない場合。 これらのいずれかの条件が真であれば、現在の <frameset> トークンは無視され、関数は true を返して次のトークンの処理に進みます。
  2. <body> 要素の処理:

    body := p.oe[1]
    if body.Parent != nil {
        body.Parent.Remove(body)
    }
    

    条件チェックを通過した場合、これは <body> 要素が現在開いており、その内部に <frameset> が出現した有効なケースであることを意味します。

    • body := p.oe[1]: オープン要素スタックの2番目の要素(通常は <body>)を取得します。
    • if body.Parent != nil { body.Parent.Remove(body) }: 取得した <body> 要素がDOMツリー内で親を持つ場合、その親から <body> 要素を削除します。これにより、<body> 要素はDOMツリーから切り離されます。
  3. オープン要素スタックの調整:

    p.oe = p.oe[:1]
    

    オープン要素スタック p.oe を、最初の要素(通常は <html>)のみを含むように切り詰めます。これにより、<body> 要素がスタックから削除されます。

  4. <frameset> 要素の追加:

    p.addElement(p.tok.Data, p.tok.Attr)
    

    現在のトークン(<frameset>)をDOMツリーに追加します。この時点でオープン要素スタックのトップは <html> であるため、<frameset><html> の子として追加されます。

  5. 挿入モードの切り替え:

    p.im = inFramesetIM
    

    パーサーの挿入モードを inFramesetIM (in frameset insertion mode) に変更します。これにより、以降のトークンは <frameset> のコンテキストで処理されるようになります。

src/pkg/html/parse_test.go の変更

--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -183,7 +183,7 @@ func TestParser(t *testing.T) {
 		{"tests3.dat", -1},
 		{"tests4.dat", -1},
 		{"tests5.dat", -1},
-		{"tests6.dat", 47},
+		{"tests6.dat", -1},
 		{"tests10.dat", 30},
 	}
 	for _, tf := range testFiles {

tests6.dat のテストケースの期待値が 47 から -1 に変更されました。これは、以前はテスト47で特定の失敗が期待されていたが、今回の修正によりそのテストケースが完全に通過するようになったため、特定の失敗を期待する設定が不要になったことを意味します。-1 は、そのテストファイル内のすべてのテストが成功することを期待するという意味合いで使われることが多いです。

関連リンク

参考にした情報源リンク