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

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

このコミットは、Go言語の標準ライブラリである html パッケージ内のHTMLパーサーが、<head> 要素の前に存在する空白文字を適切に無視するように修正するものです。これにより、HTML5のパース仕様に対する準拠性が向上し、特定のテストケースがパスするようになります。

コミット

commit 750de28d6ceb5c42637b08fb87f2de2f826ed0eb
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Tue Nov 22 09:27:27 2011 +1100

    html: ignore whitespace before <head> element
    
    Pass tests2.dat, test 47:
    " \n "
    (That is, two spaces separated by a newline)
    
    | <html>
    |   <head>
    |   <body>
    
    Also pass tests through test 49:
    <!DOCTYPE html><script>
    </script>  <title>x</title>  </head>
    
    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/5422043

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

https://github.com/golang/go/commit/750de28d6ceb5c42637b08fb87f2de2f826ed0eb

元コミット内容

このコミットの目的は、Go言語の html パッケージのパーサーが、HTMLドキュメントの <head> 要素の前に現れる空白文字(スペース、タブ、改行、キャリッジリターン、フォームフィード)を正しく無視するようにすることです。これにより、HTML5のパース仕様に準拠し、tests2.dat のテスト47およびそれ以降のテスト(テスト49まで)がパスするようになります。具体的には、" \n " のような空白文字が <html><head><body> のような構造に正しくパースされること、および <!DOCTYPE html><script></script> <title>x</title> </head> のようなケースも正しく処理されることが示されています。

変更の背景

HTMLのパースは、非常に複雑なプロセスであり、特にHTML5の仕様は、ブラウザ間の互換性を確保するために、エラー処理や空白文字の扱いについて厳密なルールを定めています。このコミットが行われた2011年当時、Go言語の html パッケージはまだ初期段階であり、HTML5の複雑なパースルールへの対応を進めている最中でした。

この特定の変更の背景には、HTML5のパースアルゴリズムにおける「挿入モード(insertion mode)」の概念があります。HTMLパーサーは、ドキュメントの現在の状態に応じて異なる挿入モードで動作し、各モードで特定のトークンがどのように処理されるかが定義されています。特に、ドキュメントの初期段階(initialbefore htmlbefore head などのモード)では、特定の種類の空白文字は「無視可能な空白(ignorable whitespace)」として扱われ、DOMツリーには追加されないことになっています。

Goの html パーサーがこれらの空白文字を適切に処理できていなかったため、一部のHTMLドキュメントが仕様通りにパースされず、結果として tests2.dat のような適合性テストが失敗していました。このコミットは、この不適合を解消し、パーサーの堅牢性と標準への準拠性を高めることを目的としています。

前提知識の解説

HTML5パースアルゴリズム

HTML5のパースアルゴリズムは、ブラウザがHTMLドキュメントをどのように読み込み、DOMツリーを構築するかを詳細に定義しています。これは、トークン化(Tokenization)とツリー構築(Tree Construction)の2つの主要なフェーズに分かれます。

  1. トークン化(Tokenization): 入力ストリーム(HTML文字列)を読み込み、個々のトークン(例: 開始タグ、終了タグ、テキスト、コメント、DOCTYPE)に変換します。
  2. ツリー構築(Tree Construction): トークナイザーから受け取ったトークンに基づいて、DOMツリーを構築します。このフェーズは、現在の「挿入モード」に基づいて動作します。

挿入モード(Insertion Mode)

挿入モードは、HTMLパーサーの現在の状態を定義するもので、どのトークンがどの要素に挿入されるべきか、またはどのように処理されるべきかを決定します。HTML5仕様には、以下のような多数の挿入モードが定義されています。

  • initial: ドキュメントの開始時。DOCTYPEやコメント、空白文字を処理します。
  • before html: <html> タグの前に位置するモード。ここでも空白文字やコメントが処理されます。
  • before head: <html> タグの直後、<head> タグの前に位置するモード。ここでも空白文字やコメント、特定のタグ(例: <title>, <base>)が処理されます。
  • in head: <head> タグ内。
  • in body: <body> タグ内。

各挿入モードには、特定のトークンタイプ(例: テキストトークン、開始タグトークン、コメントトークン)が到着したときに実行される一連のルールがあります。このコミットは、特に initialbefore htmlbefore head モードにおけるテキストトークン(空白文字を含む)の処理ルールに関わっています。

空白文字の扱い

HTML5のパースアルゴリズムでは、特定のコンテキストで現れる空白文字(ASCII空白文字: スペース 、タブ \t、改行 \n、キャリッジリターン \r、フォームフィード \f)は「無視可能な空白」として扱われます。これは、これらの空白文字がDOMツリーの構造に影響を与えないことを意味します。例えば、<html> タグの前に改行やスペースがあっても、それはDOMツリーにはノードとして追加されず、単に無視されます。

Go言語の strings パッケージ

Go言語の strings パッケージは、文字列操作のためのユーティリティ関数を提供します。このコミットでは、strings.TrimLeft 関数が使用されています。

  • strings.TrimLeft(s, cutset string): 文字列 s の先頭から、cutset に含まれる文字をすべて削除した新しい文字列を返します。

技術的詳細

このコミットの核心は、HTMLパーサーの初期段階の挿入モードにおいて、テキストトークンが空白文字のみで構成されている場合に、そのトークンを無視するロジックを追加した点にあります。

具体的には、src/pkg/html/parse.go ファイル内の以下の3つの挿入モード関数が変更されています。

  1. initialIM(p *parser) bool
  2. beforeHTMLIM(p *parser) bool
  3. beforeHeadIM(p *parser) bool

これらの関数内で、TextToken が検出された場合の処理が追加または修正されています。

変更のメカニズム

  1. whitespace 定数の定義: const whitespace = " \t\r\n\f" という定数が定義されています。これは、HTML5仕様で定義されているASCII空白文字のセットを表します。この定数は、以前は beforeHeadIM 関数内にローカルで定義されていましたが、複数の関数で利用されるため、ファイルスコープに移動されました。

  2. TextToken の処理: 各挿入モード関数内で TextToken が検出された場合、以下の処理が実行されます。

    • p.tok.Data = strings.TrimLeft(p.tok.Data, whitespace): 現在のトークン(p.tok)のデータ(p.tok.Data)の先頭から、定義された whitespace 文字をすべて削除します。これにより、トークンの先頭にある無視すべき空白文字が取り除かれます。
    • if len(p.tok.Data) == 0 { return true }: TrimLeft の結果、トークンデータが空になった場合(つまり、元のトークンがすべて空白文字で構成されていた場合)、そのトークンは完全に無視されます。return true は、パーサーが現在のトークンを消費し、次のトークンに進むべきであることを示します。これにより、空白文字のみのテキストノードがDOMツリーに誤って追加されるのを防ぎます。

テストの更新

src/pkg/html/parse_test.go では、TestParser 関数内のテストケースの定義が更新されています。 {"tests2.dat", 47}{"tests2.dat", 50} に変更されています。これは、このコミットによって tests2.dat 内のテスト47だけでなく、テスト48、49、50もパスするようになったことを示唆しています。tests2.dat は、HTML5のパース仕様に準拠しているかを検証するための、W3CのHTMLテストスイートの一部である可能性が高いです。

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

--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -319,9 +319,17 @@ func (p *parser) resetInsertionMode() {
 	p.im = inBodyIM
 }
 
+const whitespace = " \t\r\n\f"
+
 // Section 11.2.5.4.1.
 func initialIM(p *parser) bool {
 	switch p.tok.Type {
+	case TextToken:
+		p.tok.Data = strings.TrimLeft(p.tok.Data, whitespace)
+		if len(p.tok.Data) == 0 {
+			// It was all whitespace, so ignore it.
+			return true
+		}
 	case CommentToken:
 		p.doc.Add(&Node{
 			Type: CommentNode,
@@ -345,6 +353,12 @@ func beforeHTMLIM(p *parser) bool {
 // Section 11.2.5.4.2.
 func beforeHTMLIM(p *parser) bool {
 	switch p.tok.Type {
+	case TextToken:
+		p.tok.Data = strings.TrimLeft(p.tok.Data, whitespace)
+		if len(p.tok.Data) == 0 {
+			// It was all whitespace, so ignore it.
+			return true
+		}
 	case StartTagToken:
 		if p.tok.Data == "html" {
 			p.addElement(p.tok.Data, p.tok.Attr)
@@ -383,7 +397,11 @@ func beforeHeadIM(p *parser) bool {
 	case ErrorToken:
 		implied = true
 	case TextToken:
-\t\t// TODO: distinguish whitespace text from others.
+\t\tp.tok.Data = strings.TrimLeft(p.tok.Data, whitespace)
+\t\tif len(p.tok.Data) == 0 {
+\t\t\t// It was all whitespace, so ignore it.
+\t\t\treturn true
+\t\t}
 	\timplied = true
 	case StartTagToken:
 	\tswitch p.tok.Data {
@@ -417,8 +435,6 @@ func beforeHeadIM(p *parser) bool {
 	return !implied
 }
 
-const whitespace = " \t\r\n\f"
-
 // Section 11.2.5.4.4.
 func inHeadIM(p *parser) bool {
 	var (
--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -134,7 +134,7 @@ func TestParser(t *testing.T) {
 	}{\n \t\t// TODO(nigeltao): Process all the test cases from all the .dat files.\n \t\t{\"tests1.dat\", -1},\n-\t\t{\"tests2.dat\", 47},\n+\t\t{\"tests2.dat\", 50},\n \t\t{\"tests3.dat\", 0},\n \t}\n \tfor _, tf := range testFiles {

コアとなるコードの解説

このコミットの主要な変更は、HTMLパーサーの初期段階の挿入モード(initialIM, beforeHTMLIM, beforeHeadIM)におけるテキストトークンの処理ロジックの強化です。

  1. whitespace 定数の移動と定義: 以前は beforeHeadIM 関数内にローカルで定義されていた const whitespace = " \t\r\n\f" が、initialIM 関数の上、つまりファイルスコープに移動されました。これにより、この定数が複数の挿入モード関数で再利用可能になり、コードの重複が避けられ、保守性が向上します。この定数は、スペース、タブ、改行、キャリッジリターン、フォームフィードといった、HTML5仕様で定義される「空白文字」を正確に表現しています。

  2. initialIMbeforeHTMLIMbeforeHeadIM における TextToken 処理の追加/修正: これらの関数は、それぞれHTMLドキュメントの異なるパース段階に対応しています。

    • initialIM はドキュメントの開始時。
    • beforeHTMLIM<html> タグの直前。
    • beforeHeadIM<html> タグの直後、<head> タグの直前。

    これらのモードでは、HTML5の仕様により、特定のテキストトークン(特に空白文字のみで構成されるもの)は無視されるべきです。 追加された case TextToken: ブロックは、この要件を満たすためのものです。

    case TextToken:
        p.tok.Data = strings.TrimLeft(p.tok.Data, whitespace)
        if len(p.tok.Data) == 0 {
            // It was all whitespace, so ignore it.
            return true
        }
    
    • p.tok.Data = strings.TrimLeft(p.tok.Data, whitespace): ここで、現在のテキストトークン p.tok.Data の先頭から、whitespace 定義に含まれるすべての空白文字が削除されます。これにより、トークンの先頭にある不要な空白が取り除かれます。
    • if len(p.tok.Data) == 0 { return true }: TrimLeft の結果、p.tok.Data が空文字列になった場合、これは元のテキストトークンが完全に空白文字のみで構成されていたことを意味します。このような場合、HTML5のパース仕様ではこのトークンを無視すべきであるため、関数は true を返します。true を返すことで、パーサーは現在のトークンを消費し、DOMツリーに何も追加せずに次のトークンに進みます。

    この変更により、例えば <html> タグの前に \n のような空白文字があっても、それらがDOMツリーに余分なテキストノードとして追加されることなく、正しく無視されるようになります。これは、HTML5の適合性テストをパスするために不可欠な修正です。

  3. parse_test.go の更新: TestParser 関数内の tests2.dat のテストケースが 47 から 50 に変更されました。これは、このコミットによって tests2.dat 内のテスト47だけでなく、テスト48、49、50もパスするようになったことを示唆しています。これは、パーサーの改善が複数の関連するテストケースに影響を与え、全体的な適合性が向上したことを裏付けています。

これらの変更は、GoのHTMLパーサーがより堅牢になり、HTML5の複雑なパースルールに正確に準拠するための重要なステップです。

関連リンク

参考にした情報源リンク