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

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

このコミットは、Go言語のHTMLパーサーにおけるframeset要素の処理に関するバグ修正と改善を目的としています。具体的には、frameset要素内またはその直後で、空白文字(スペース、タブ、改行、フォームフィード、キャリッジリターン)が誤って無視されていた問題を解決し、HTMLの仕様に準拠したパース動作を実現しています。

コミット

commit 0c5443a0a61182276f755c1c728d4990cf0983e9
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Mon Dec 12 13:18:01 2011 +1100

    html: don't ignore whitespace in or after framesets
    
    Pass tests6.dat, test 7:
    <frameset></frameset>
    foo
    
    | <html>
    |   <head>
    |   <frameset>
    |   "
    "
    
    Also pass tests through test 12:
    <form><form>
    
    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/5480061

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

https://github.com/golang/go/commit/0c5443a0a61182276f755c1c728d4990cf0983e9

元コミット内容

html: don't ignore whitespace in or after framesets

このコミットは、frameset要素内またはその直後で空白文字を無視しないようにするものです。 これにより、tests6.datのテスト7(<frameset></frameset>fooのようなケース)およびテスト12までの他のテストがパスするようになります。

変更の背景

HTMLのパースにおいて、特定の要素(特にframeset)のコンテキストでは、空白文字の扱いが重要になります。従来のGo言語のHTMLパーサーでは、frameset要素の内部、またはframeset要素が閉じられた直後のコンテキストにおいて、空白文字が不適切に無視されていました。これは、HTML5のパースアルゴリズムや既存のブラウザの挙動と異なる可能性があり、予期せぬDOM構造の生成やレンダリングの問題を引き起こす可能性がありました。

コミットメッセージに記載されているtests6.dat, test 7の例は、この問題を示す典型的なケースです。

<frameset></frameset>
foo

このHTMLスニペットでは、<frameset>タグの後に改行とfooというテキストが続いています。正しいHTMLパースでは、このfooというテキストはDOMツリーに適切に追加されるべきですが、空白文字の無視により、そのテキストが失われる可能性がありました。

この変更は、HTML5のパース仕様に準拠し、より堅牢で正確なHTMLパーサーを提供するために行われました。また、golang.org/cl/5480061というGo Change List (CL) に関連しており、これはGoプロジェクトにおけるコードレビューと変更管理のプロセスの一部です。

前提知識の解説

HTMLパースアルゴリズム

HTMLのパースは、非常に複雑なプロセスです。XMLのような厳格な構文規則とは異なり、HTMLはブラウザがエラーのあるマークアップでも寛容に処理し、DOMツリーを構築できるように設計されています。HTML5の仕様では、この寛容なエラー処理とDOM構築のための詳細なパースアルゴリズムが定義されています。

パースアルゴリズムは、入力ストリームをトークン化し、それらのトークンに基づいてDOMツリーを構築するステートマシンとして機能します。このステートマシンは、現在の「挿入モード (insertion mode)」に基づいて、受け取ったトークンをどのように処理するかを決定します。挿入モードは、現在パース中のHTML要素のコンテキストによって変化します。

挿入モード (Insertion Mode)

HTML5のパースアルゴリズムには、多数の挿入モードが存在します。それぞれのモードは、特定のHTML要素の内部や特定の状況下でのトークンの処理方法を定義します。このコミットで関連するのは以下のモードです。

  • "in frameset" 挿入モード: <frameset>要素の開始タグがパースされた後に遷移するモードです。このモードでは、<frame><noframes>などの要素が期待されます。
  • "after frameset" 挿入モード: frameset要素の終了タグがパースされた後に遷移するモードです。このモードでは、通常、<body>要素や他のトップレベルの要素が期待されます。

これらのモードでは、通常、テキストコンテンツ(特に空白文字以外のテキスト)は許可されません。しかし、空白文字はHTMLのレイアウトに影響を与えるため、ブラウザはこれらの空白文字をDOMツリーにテキストノードとして追加することがあります。この挙動は、HTMLの互換性要件の一部です。

トークン化 (Tokenization)

HTMLパーサーの最初の段階は、入力ストリームを意味のある単位(トークン)に分割するトークン化です。HTMLのトークンには、開始タグ、終了タグ、テキスト、コメント、DOCTYPEなどがあります。このコミットでは、TextToken(テキストトークン)の処理が焦点となっています。

frameset要素

<frameset>要素は、HTML4以前でフレームベースのウェブページを作成するために使用されました。これは、ブラウザウィンドウを複数のフレームに分割し、それぞれのフレームに異なるHTMLドキュメントを表示することを可能にしました。HTML5では非推奨とされていますが、既存のウェブコンテンツとの互換性のために、パーサーは引き続きこれを適切に処理する必要があります。

技術的詳細

このコミットの技術的な核心は、Go言語のHTMLパーサーにおけるinFramesetIM("in frameset" 挿入モード)とafterFramesetIM("after frameset" 挿入モード)の処理ロジックに、TextToken(テキストトークン)のハンドリングを追加した点です。

変更前は、これらの挿入モードにおいてTextTokenが来た場合、特定の処理が行われず、結果として空白文字が無視される挙動になっていました。変更後は、以下のロジックが追加されています。

  1. TextTokenの検出: パーサーがTextTokenを受け取った場合、この新しいロジックが適用されます。
  2. 空白文字のフィルタリング: strings.Map関数を使用して、p.tok.Data(現在のトークンのデータ、つまりテキストコンテンツ)から空白文字(スペース ' '、タブ '\t'、改行 '\n'、フォームフィード '\f'、キャリッジリターン '\r')のみを抽出します。空白文字以外の文字は-1にマップされ、結果の文字列から除外されます。
  3. テキストノードの追加: フィルタリングの結果、抽出された空白文字の文字列sが空でなければ(つまり、トークンに1つ以上の空白文字が含まれていた場合)、p.addText(s)を呼び出して、その空白文字をDOMツリーにテキストノードとして追加します。

この変更により、framesetコンテキストにおいても、HTML5のパース仕様で要求されるように、空白文字が適切にDOMツリーに反映されるようになりました。これにより、ブラウザの挙動との一貫性が向上し、予期せぬレイアウトの崩れやコンテンツの欠落を防ぐことができます。

また、parse_test.goの変更は、この修正が正しく機能することを確認するためのテストケースの更新です。tests6.datのテストケースの期待値が7から13に変更されています。これは、修正によってパース結果が変わったため、その新しい正しい結果に合わせてテストの期待値を更新したことを意味します。

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

変更は主にsrc/pkg/html/parse.gosrc/pkg/html/parse_test.goの2つのファイルにわたります。

src/pkg/html/parse.go

inFramesetIM関数とafterFramesetIM関数に、TextTokenを処理するcase文が追加されました。

--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -1432,6 +1432,18 @@ func inFramesetIM(p *parser) bool {
 			Type: CommentNode,
 			Data: p.tok.Data,
 		})
+	case TextToken:
+		// Ignore all text but whitespace.
+		s := strings.Map(func(c rune) rune {
+			switch c {
+			case ' ', '\t', '\n', '\f', '\r':
+				return c
+			}
+			return -1
+		}, p.tok.Data)
+		if s != "" {
+			p.addText(s)
+		}
 	case StartTagToken:
 		switch p.tok.Data {
 		case "html":
@@ -1470,6 +1482,18 @@ func afterFramesetIM(p *parser) bool {
 			Type: CommentNode,
 			Data: p.tok.Data,
 		})
+	case TextToken:
+		// Ignore all text but whitespace.
+		s := strings.Map(func(c rune) rune {
+			switch c {
+			case ' ', '\t', '\n', '\f', '\r':
+				return c
+			}
+			return -1
+		}, p.tok.Data)
+		if s != "" {
+			p.addText(s)
+		}
 	case StartTagToken:
 		switch p.tok.Data {
 		case "html":

src/pkg/html/parse_test.go

TestParser関数内のテストデータ定義で、tests6.datの期待値が変更されました。

--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -167,7 +167,7 @@ func TestParser(t *testing.T) {
 		{"tests3.dat", -1},
 		{"tests4.dat", -1},
 		{"tests5.dat", -1},
-		{"tests6.dat", 7},
+		{"tests6.dat", 13},
 	}
 	for _, tf := range testFiles {
 		f, err := os.Open("testdata/webkit/" + tf.filename)

コアとなるコードの解説

inFramesetIMafterFramesetIMは、HTMLパーサーのステートマシンにおける特定の挿入モードを処理する関数です。これらの関数は、パーサーが現在処理しているトークンの種類(p.tok.Type)に基づいて異なるロジックを実行します。

追加されたcase TextToken:ブロックは、パーサーがテキストトークンを検出したときに実行されます。

case TextToken:
    // Ignore all text but whitespace.
    s := strings.Map(func(c rune) rune {
        switch c {
        case ' ', '\t', '\n', '\f', '\r':
            return c
        }
        return -1
    }, p.tok.Data)
    if s != "" {
        p.addText(s)
    }
  • strings.Map(func(c rune) rune { ... }, p.tok.Data): この行が、テキストトークンから空白文字のみを抽出する核心部分です。
    • p.tok.Dataは、現在のテキストトークンの生データ(文字列)です。
    • strings.Mapは、文字列の各ルーン(Unicodeコードポイント)に関数を適用し、その結果として新しい文字列を構築します。
    • 無名関数func(c rune) rune { ... }は、各ルーンcをチェックします。
      • もしcがスペース、タブ、改行、フォームフィード、キャリッジリターンのいずれかであれば、そのルーンをそのまま返します。
      • そうでなければ(つまり、空白文字以外の文字であれば)、-1を返します。strings.Mapにおいて、関数が-1を返すと、そのルーンは結果の文字列から除外されます。
    • この結果、sには元のテキストトークンに含まれていた空白文字のみが、その順序を保ったまま格納されます。
  • if s != "": 抽出された空白文字の文字列sが空でない場合(つまり、テキストトークンに実際に空白文字が含まれていた場合)にのみ、次の処理に進みます。
  • p.addText(s): パーサーの内部メソッドaddTextを呼び出し、抽出された空白文字sをDOMツリーにテキストノードとして追加します。これにより、framesetコンテキストで空白文字が適切にDOMに反映されるようになります。

この変更は、HTML5のパース仕様における「空白文字の処理」の要件を満たすためのものであり、特にframesetのような特殊な要素のコンテキストでの正確なDOM構築に貢献します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • HTML5仕様書 (W3C/WHATWG)
  • Go言語のstringsパッケージドキュメント: https://pkg.go.dev/strings
  • Go言語のhtmlパッケージのソースコード (コミット時点のバージョン)
  • WebKitのテストデータ (testdata/webkit/) - このコミットで参照されているtests6.datはWebKitのHTMLパーサーのテストスイートの一部です。