[インデックス 10124] ファイルの概要
このコミットは、Go言語の標準ライブラリであるhtmlパッケージにおけるHTMLパーサーの改善に関するものです。具体的には、<head>要素内に配置された<style>、<noscript>、<noframes>といった要素のパース処理を修正し、特に<style>要素の内部でファイル終端(EOF)に達した場合の挙動を正しくハンドリングするように変更しています。これにより、HTML5の仕様に準拠したより堅牢なパースが可能になりました。
コミット
commit 833fb4198d2f4ff3add2e8a14bfe6c91413f7601
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Thu Oct 27 10:26:11 2011 +1100
html: parse <style> elements inside <head> element.
Also correctly handle EOF inside a <style> element.
Pass tests1.dat, test 49:
<!DOCTYPE html><style> EOF
| <!DOCTYPE html>
| <html>
| <head>
| <style>
| " EOF"
| <body>
R=nigeltao
CC=golang-dev
https://golang.org/cl/5321057
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/833fb4198d2f4ff3add2e8a14bfe6c91413f7601
元コミット内容
html: parse <style> elements inside <head> element.
Also correctly handle EOF inside a <style> element.
Pass tests1.dat, test 49:
<!DOCTYPE html><style> EOF
| <!DOCTYPE html>
| <html>
| <head>
| <style>
| " EOF"
| <body>
R=nigeltao
CC=golang-dev
https://golang.org/cl/5321057
変更の背景
HTMLのパースは、その複雑な仕様と多様な記述方法のため、非常に困難なタスクです。特に、ブラウザはエラーのあるHTMLに対しても寛容に解釈し、表示しようとします。Go言語のhtmlパッケージは、HTML5の仕様に準拠した堅牢なパーサーを提供することを目指しています。
このコミットが行われた背景には、以下の問題があったと考えられます。
<head>内の特定要素のパース漏れ: 以前のパーサーでは、<head>要素内に配置された<style>、<noscript>、<noframes>といった要素が正しく認識されず、その内容が適切にパースされない可能性がありました。HTML5の仕様では、これらの要素は<head>内に配置されることが許容されており、その内容(CSSやスクリプト、代替コンテンツなど)はテキストとして扱われるべきです。<style>要素内でのEOFハンドリングの不備: HTMLパーサーは、入力の終端(EOF)に達した場合でも、現在のパース状態を適切に終了させる必要があります。特に、<style>要素のように内部にテキストデータを持つ要素の途中でEOFに達した場合、パーサーが無限ループに陥ったり、不正なDOMツリーを構築したりするリスクがありました。コミットメッセージにある<!DOCTYPE html><style> EOFというテストケースは、この問題を示唆しています。パーサーは、<style>タグが閉じられていない状態でEOFに達しても、その内容をテキストとして扱い、適切に要素を閉じる(またはエラーを処理する)必要があります。
これらの問題を解決し、より仕様に準拠した堅牢なHTMLパーシングを実現するために、このコミットが導入されました。
前提知識の解説
HTMLパーシングとDOMツリー
HTMLパーシングとは、HTMLドキュメントのテキストデータを読み込み、それをブラウザが理解できる構造化されたデータ(DOMツリー:Document Object Model Tree)に変換するプロセスです。DOMツリーは、HTML要素、属性、テキストなどをノードとして表現し、それらの親子関係をツリー構造で表します。
HTML5のパースアルゴリズム
HTML5の仕様は、非常に詳細なパースアルゴリズムを定義しています。これは、異なるブラウザ間でのHTMLの解釈の一貫性を保証することを目的としています。このアルゴリズムは、「挿入モード(Insertion Mode)」という概念に基づいており、パーサーが現在どのHTML要素の内部をパースしているかによって、次にどのようなトークンを期待し、どのようにDOMツリーを構築するかを決定します。
<head>要素と挿入モード
<head>要素は、HTMLドキュメントのメタデータ(タイトル、スタイルシートへのリンク、スクリプトなど)を含むセクションです。HTML5のパースアルゴリズムでは、パーサーが<head>要素の内部を処理している間は「in head insertion mode」と呼ばれる特定の挿入モードで動作します。このモードでは、<title>、<link>、<meta>、<style>、<script>などの特定のタグが特別に扱われます。
<style>、<noscript>、<noframes>要素
<style>: ドキュメントのスタイル情報(CSS)を埋め込むために使用されます。通常、<head>内に配置されます。その内容はCSSコードとして解釈されるべきテキストデータです。<noscript>: スクリプトが無効になっているブラウザや、スクリプトをサポートしないブラウザで表示される代替コンテンツを提供します。通常、<body>内または<head>内に配置されます。<noframes>:<frameset>要素を使用するフレームベースのHTMLドキュメントで、フレームをサポートしないブラウザ向けの代替コンテンツを提供します。HTML5では非推奨ですが、古いHTMLのパースを考慮する際には重要です。
これらの要素は、その内部にテキストデータ(CSS、代替HTMLコンテンツ)を持つことが特徴であり、パーサーはこれらのタグの開始タグを検出した後、その終了タグを検出するまで、内部のコンテンツを「テキスト」として扱う「text insertion mode」に切り替える必要があります。
EOF (End Of File) ハンドリング
パーサーが入力ストリームの終端(EOF)に達した場合、それは通常、ドキュメントの終わりを意味します。しかし、HTMLが不完全な場合(例えば、開始タグはあるが対応する終了タグがない場合)、パーサーは開いている要素を適切に閉じ、DOMツリーを完成させる必要があります。不適切なEOFハンドリングは、パーサーのクラッシュ、無限ループ、または不正なDOMツリーの生成につながる可能性があります。
技術的詳細
このコミットは、Go言語のhtmlパッケージ内のparse.goとparse_test.goの2つのファイルを変更しています。
src/pkg/html/parse.goの変更点
-
inHeadIM関数の修正:inHeadIM関数は、パーサーが<head>要素の内部にいるときの挿入モードを処理します。以前は、<script>と<title>タグのみが特別に扱われていましたが、この変更により、<noscript>、<noframes>、<style>タグも同様に扱われるようになりました。--- a/src/pkg/html/parse.go +++ b/src/pkg/html/parse.go @@ -443,7 +443,7 @@ func inHeadIM(p *parser) (insertionMode, bool) { switch p.tok.Data { case "meta": // TODO. - case "script", "title": + case "script", "title", "noscript", "noframes", "style": p.addElement(p.tok.Data, p.tok.Attr) p.setOriginalIM(inHeadIM) return textIM, trueこの変更の意図は、これらのタグが
<head>内で検出された際に、パーサーがその内容をテキストとしてパースするためにtextIM(テキスト挿入モード)に正しく切り替えるようにすることです。p.addElementはDOMツリーに要素を追加し、p.setOriginalIMは現在の挿入モードを保存し、textIMを返してテキストパースを開始します。これにより、<style>タグ内のCSSコードなどが正しくテキストとして取り込まれるようになります。 -
textIM関数の修正:textIM関数は、パーサーが要素の内部のテキストコンテンツを処理しているときの挿入モードを扱います。この変更では、ErrorTokenが検出された場合のハンドリングが追加されました。--- a/src/pkg/html/parse.go +++ b/src/pkg/html/parse.go @@ -763,6 +763,8 @@ func (p *parser) inBodyEndTagOther(tag string) { // Section 11.2.5.4.8. func textIM(p *parser) (insertionMode, bool) { switch p.tok.Type { + case ErrorToken: + p.oe.pop() case TextToken: p.addText(p.tok.Data) return textIM, trueErrorTokenは、入力ストリームの終端(EOF)やその他のパースエラーが発生した際に生成されるトークンです。p.oe.pop()は、オープン要素スタック(p.oe)から最も内側の要素をポップ(削除)します。これは、例えば<style>タグが閉じられていない状態でEOFに達した場合に、パーサーがその<style>要素を適切に閉じ、スタックの整合性を保つために重要です。これにより、パーサーが不正な状態に陥るのを防ぎ、堅牢性が向上します。
src/pkg/html/parse_test.goの変更点
- テストケースの増加:
TestParser関数内で、tests1.datから読み込むテストケースの数が49から50に増加しました。
この変更は、コミットメッセージで言及されている「Pass tests1.dat, test 49」に対応しています。新しいテストケース(または以前はスキップされていたテストケース)がパーサーのテストスイートに含まれるようになり、このコミットによってそのテストがパスするようになったことを示しています。--- a/src/pkg/html/parse_test.go +++ b/src/pkg/html/parse_test.go @@ -132,7 +132,7 @@ func TestParser(t *testing.T) { rc := make(chan io.Reader) go readDat(filename, rc) // TODO(nigeltao): Process all test cases, not just a subset. - for i := 0; i < 49; i++ { + for i := 0; i < 50; i++ { // Parse the #data section. b, err := ioutil.ReadAll(<-rc) if err != nil {
コアとなるコードの変更箇所
diff --git a/src/pkg/html/parse.go b/src/pkg/html/parse.go
index 823f7aad29..276f0b7fbf 100644
--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -443,7 +443,7 @@ func inHeadIM(p *parser) (insertionMode, bool) {
switch p.tok.Data {
case "meta":
// TODO.
- case "script", "title":
+ case "script", "title", "noscript", "noframes", "style":
p.addElement(p.tok.Data, p.tok.Attr)
p.setOriginalIM(inHeadIM)
return textIM, true
@@ -763,6 +763,8 @@ func (p *parser) inBodyEndTagOther(tag string) {
// Section 11.2.5.4.8.
func textIM(p *parser) (insertionMode, bool) {
switch p.tok.Type {
+ case ErrorToken:
+ p.oe.pop()
case TextToken:
p.addText(p.tok.Data)
return textIM, true
diff --git a/src/pkg/html/parse_test.go b/src/pkg/html/parse_test.go
index 5022a4f779..86f1298d5e 100644
--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -132,7 +132,7 @@ func TestParser(t *testing.T) {
rc := make(chan io.Reader)
go readDat(filename, rc)
// TODO(nigeltao): Process all test cases, not just a subset.
- for i := 0; i < 49; i++ {
+ for i := 0; i < 50; i++ {
// Parse the #data section.
b, err := ioutil.ReadAll(<-rc)
if err != nil {
コアとなるコードの解説
src/pkg/html/parse.go
-
inHeadIM関数内のswitch文の変更: この変更は、HTMLパーサーが<head>要素の内部をパースしている際の挙動を定義するinHeadIM関数にあります。以前は、<script>と<title>タグが検出された場合にのみ、パーサーはそれらの内容をテキストとして扱うためのtextIM(テキスト挿入モード)に切り替えていました。このコミットでは、このリストに新たに"noscript"、"noframes"、"style"が追加されました。 これにより、パーサーは<head>内にこれらのタグが出現した場合でも、その内部コンテンツを正しくテキストとして認識し、DOMツリーに組み込むことができるようになります。例えば、<style>タグ内のCSSコードが、単なる無視されるのではなく、DOMツリーの適切な位置にテキストノードとして追加されるようになります。これは、HTML5の仕様に準拠し、ブラウザの挙動を模倣するために不可欠な変更です。 -
textIM関数内のswitch文へのErrorTokenケースの追加:textIM関数は、パーサーが要素の内部のテキストコンテンツを処理しているときに使用される挿入モードです。このモード中にErrorToken(エラーを示すトークン)が検出された場合、p.oe.pop()が呼び出されます。p.oeは「open elements stack」(開いている要素のスタック)を表し、現在パース中の要素の階層構造を管理しています。pop()操作は、スタックの最上位にある要素(つまり、現在最も内側で開いている要素)を削除します。 この変更は、特に<style>要素の途中でファイル終端(EOF)に達するような不完全なHTMLを処理する際に重要です。例えば、<style>タグが閉じられていない状態でドキュメントが終了した場合、パーサーはErrorTokenを受け取ります。このとき、p.oe.pop()を実行することで、不完全に開いたままの<style>要素をスタックから取り除き、パーサーの状態をクリーンアップします。これにより、パーサーが無限ループに陥ったり、不正なDOMツリーを構築したりするのを防ぎ、より堅牢なエラーハンドリングを実現します。
src/pkg/html/parse_test.go
TestParser関数内のループ範囲の変更: この変更は、TestParser関数内でtests1.datというデータファイルから読み込むテストケースの数を調整しています。ループの条件がi < 49からi < 50に変更されたことで、テストケースの総数が1つ増え、以前はスキップされていた「テスト49」が実行されるようになりました。 これは、このコミットが修正した問題(特に<style>要素内でのEOFハンドリング)を検証するための新しいテストケースが追加され、そのテストがこのコミットによってパスするようになったことを示しています。テストの網羅性を高め、回帰を防ぐ上で重要な変更です。
これらの変更は全体として、Go言語のhtmlパーサーがHTML5の仕様により厳密に準拠し、特に<head>内の特定の要素のパースと、不完全なHTML入力に対するエラーハンドリングの堅牢性を向上させることを目的としています。
関連リンク
- Gerrit Change-ID: https://golang.org/cl/5321057 このリンクは、GoプロジェクトのコードレビューシステムであるGerritにおけるこのコミットの変更リスト(CL: Change List)を示しています。ここには、コミットの詳細な変更内容、レビューコメント、および関連する議論が含まれている可能性があります。
参考にした情報源リンク
- HTML5 Parsing Algorithm: https://html.spec.whatwg.org/multipage/parsing.html (HTML5のパースアルゴリズムに関するWHATWGの公式仕様)
- Go html package documentation:
https://pkg.go.dev/golang.org/x/net/html
(Go言語の
htmlパッケージの公式ドキュメント。このコミットは古いsrc/pkg/htmlに属しますが、概念は共通です。) - HTML
<head>element: https://developer.mozilla.org/ja/docs/Web/HTML/Element/head (MDN Web Docsにおける<head>要素の解説) - HTML
<style>element: https://developer.mozilla.org/ja/docs/Web/HTML/Element/style (MDN Web Docsにおける<style>要素の解説) - HTML
<noscript>element: https://developer.mozilla.org/ja/docs/Web/HTML/Element/noscript (MDN Web Docsにおける<noscript>要素の解説) - HTML
<noframes>element: https://developer.mozilla.org/ja/docs/Web/HTML/Element/noframes (MDN Web Docsにおける<noframes>要素の解説) - Go言語のソースコード管理とGerrit: GoプロジェクトはGerritを使用してコードレビューと変更管理を行っています。GerritのCLリンクは、その変更がどのようにレビューされ、承認されたかを示す重要な情報源です。 https://go.dev/doc/contribute (Go言語への貢献方法に関する公式ドキュメント)