[インデックス 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パーサーは、ドキュメントの現在の状態に応じて異なる挿入モードで動作し、各モードで特定のトークンがどのように処理されるかが定義されています。特に、ドキュメントの初期段階(initial
、before html
、before head
などのモード)では、特定の種類の空白文字は「無視可能な空白(ignorable whitespace)」として扱われ、DOMツリーには追加されないことになっています。
Goの html
パーサーがこれらの空白文字を適切に処理できていなかったため、一部のHTMLドキュメントが仕様通りにパースされず、結果として tests2.dat
のような適合性テストが失敗していました。このコミットは、この不適合を解消し、パーサーの堅牢性と標準への準拠性を高めることを目的としています。
前提知識の解説
HTML5パースアルゴリズム
HTML5のパースアルゴリズムは、ブラウザがHTMLドキュメントをどのように読み込み、DOMツリーを構築するかを詳細に定義しています。これは、トークン化(Tokenization)とツリー構築(Tree Construction)の2つの主要なフェーズに分かれます。
- トークン化(Tokenization): 入力ストリーム(HTML文字列)を読み込み、個々のトークン(例: 開始タグ、終了タグ、テキスト、コメント、DOCTYPE)に変換します。
- ツリー構築(Tree Construction): トークナイザーから受け取ったトークンに基づいて、DOMツリーを構築します。このフェーズは、現在の「挿入モード」に基づいて動作します。
挿入モード(Insertion Mode)
挿入モードは、HTMLパーサーの現在の状態を定義するもので、どのトークンがどの要素に挿入されるべきか、またはどのように処理されるべきかを決定します。HTML5仕様には、以下のような多数の挿入モードが定義されています。
- initial: ドキュメントの開始時。DOCTYPEやコメント、空白文字を処理します。
- before html:
<html>
タグの前に位置するモード。ここでも空白文字やコメントが処理されます。 - before head:
<html>
タグの直後、<head>
タグの前に位置するモード。ここでも空白文字やコメント、特定のタグ(例:<title>
,<base>
)が処理されます。 - in head:
<head>
タグ内。 - in body:
<body>
タグ内。
各挿入モードには、特定のトークンタイプ(例: テキストトークン、開始タグトークン、コメントトークン)が到着したときに実行される一連のルールがあります。このコミットは、特に initial
、before html
、before 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つの挿入モード関数が変更されています。
initialIM(p *parser) bool
beforeHTMLIM(p *parser) bool
beforeHeadIM(p *parser) bool
これらの関数内で、TextToken
が検出された場合の処理が追加または修正されています。
変更のメカニズム
-
whitespace
定数の定義:const whitespace = " \t\r\n\f"
という定数が定義されています。これは、HTML5仕様で定義されているASCII空白文字のセットを表します。この定数は、以前はbeforeHeadIM
関数内にローカルで定義されていましたが、複数の関数で利用されるため、ファイルスコープに移動されました。 -
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
)におけるテキストトークンの処理ロジックの強化です。
-
whitespace
定数の移動と定義: 以前はbeforeHeadIM
関数内にローカルで定義されていたconst whitespace = " \t\r\n\f"
が、initialIM
関数の上、つまりファイルスコープに移動されました。これにより、この定数が複数の挿入モード関数で再利用可能になり、コードの重複が避けられ、保守性が向上します。この定数は、スペース、タブ、改行、キャリッジリターン、フォームフィードといった、HTML5仕様で定義される「空白文字」を正確に表現しています。 -
initialIM
、beforeHTMLIM
、beforeHeadIM
における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
や -
parse_test.go
の更新:TestParser
関数内のtests2.dat
のテストケースが47
から50
に変更されました。これは、このコミットによってtests2.dat
内のテスト47だけでなく、テスト48、49、50もパスするようになったことを示唆しています。これは、パーサーの改善が複数の関連するテストケースに影響を与え、全体的な適合性が向上したことを裏付けています。
これらの変更は、GoのHTMLパーサーがより堅牢になり、HTML5の複雑なパースルールに正確に準拠するための重要なステップです。
関連リンク
- Go言語の
html
パッケージのドキュメント: https://pkg.go.dev/golang.org/x/net/html (コミット当時のパスとは異なる可能性がありますが、現在のパッケージ情報) - HTML5仕様 (W3C勧告): https://www.w3.org/TR/html5/ (特に「Parsing HTML documents」セクション)
- HTML5 Parsing Algorithm Visualizer: https://html.spec.whatwg.org/multipage/parsing.html#parsing (WHATWG版のHTML Living Standardですが、HTML5のパースアルゴリズムの理解に役立ちます)
参考にした情報源リンク
- https://github.com/golang/go/commit/750de28d6ceb5c42637b08fb87f2de2f826ed0eb
- Go言語の
strings
パッケージ: https://pkg.go.dev/strings - HTML5 Parsing Algorithm (W3C): https://www.w3.org/TR/html5/syntax.html#parsing
- HTML Living Standard (WHATWG): https://html.spec.whatwg.org/multipage/parsing.html (HTML5の進化版であり、パースアルゴリズムの最新情報が含まれています)
- HTML5 Conformance Test Suite: https://github.com/web-platform-tests/wpt/tree/master/html/dom/parsing (
tests2.dat
のようなテストファイルが含まれる可能性のあるリポジトリ)