[インデックス 12931] ファイルの概要
このコミットは、Go言語の実験的なHTMLパーサーパッケージ exp/html における inBodyIM (in Body Insertion Mode) の挙動を改善するものです。具体的には、HTML5の仕様に準拠するため、特定のHTML要素(<param>, <source>, <track>, <textarea>, <iframe>, <noembed>, <noscript>) の処理を追加し、また input 要素の中でも type="hidden" の場合に framesetOK フラグが誤って false に設定される問題を修正しています。これにより、HTMLドキュメントのパース精度が向上し、7つのテストケースが新たにパスするようになりました。
コミット
commit 0cc8ee980886a00387c9b5514c0e3fa44c5c1113
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Sun Apr 22 16:19:21 2012 +1000
exp/html: add more cases to inBodyIM
Don't set framesetOK to false for hidden input elements.
Handle <param>, <source>, <track>, <textarea>, <iframe>, <noembed>,
and <noscript>
Pass 7 additional tests.
R=nigeltao
CC=golang-dev
https://golang.org/cl/6094045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0cc8ee980886a00387c9b5514c0e3fa44c5c1113
元コミット内容
このコミットは、Go言語の exp/html パッケージにおけるHTMLパーサーの inBodyIM (in Body Insertion Mode) 関数に、以下の変更を加えています。
input要素のうちtype="hidden"のものに対しては、framesetOKフラグをfalseに設定しないように修正。<param>,<source>,<track>,<textarea>,<iframe>,<noembed>,<noscript>といったHTML要素のパース処理をinBodyIMに追加。- これらの変更により、7つの関連するテストケースが新たにパスするようになった。
変更の背景
この変更の背景には、ウェブブラウザがHTMLドキュメントをどのように解釈し、DOMツリーを構築するかを定義するHTML5のパースアルゴリズムの厳密な実装があります。exp/html パッケージは、Go言語でHTML5の仕様に準拠したパーサーを提供することを目指しており、その正確性を高めるために継続的な改善が行われています。
特に、inBodyIM はHTMLドキュメントの <body> 要素内でのトークン処理を担当する重要な挿入モードです。HTML5の仕様では、各HTML要素がこのモードでどのように扱われるべきか、詳細なルールが定められています。このコミット以前は、一部の要素(<param>, <source>, <track>, <textarea>, <iframe>, <noembed>, <noscript>) の処理が inBodyIM で適切に実装されておらず、また input[type="hidden"] のような特定の属性を持つ要素の挙動も仕様と異なっていました。
これらの不正確な挙動は、生成されるDOMツリーの構造に影響を与え、結果としてHTMLドキュメントの解釈に誤りを生じさせる可能性がありました。特に framesetOK フラグは、frameset 要素の挿入が許可されるかどうかを制御する重要なフラグであり、その誤った設定はドキュメントの構造に大きな影響を与えかねません。
このコミットは、これらの不足や不正確さを解消し、exp/html パッケージがよりHTML5仕様に忠実なパーサーとなることを目的としています。テストケースの追加パスは、この改善が実際にパースの正確性を高めたことを示しています。
前提知識の解説
このコミットを理解するためには、以下のHTML5パースアルゴリズムに関する前提知識が必要です。
-
HTML5パースアルゴリズム: HTML5のパースアルゴリズムは、ウェブブラウザがHTML文字列をDOMツリーに変換する手順を詳細に定義したものです。これは、トークン化フェーズとツリー構築フェーズの2つの主要なフェーズに分かれています。
- トークン化 (Tokenization): 入力されたHTML文字列を、タグ、属性、テキストなどの「トークン」に分解するプロセスです。
- ツリー構築 (Tree Construction): トークンストリームを受け取り、それらを基にDOMツリーを構築するプロセスです。このフェーズは、現在の「挿入モード (Insertion Mode)」に基づいて動作します。
-
挿入モード (Insertion Mode): ツリー構築フェーズにおいて、パーサーが次に受け取るトークンをどのように処理するかを決定する状態機械です。HTMLドキュメントの異なる部分(例:
<html>の前、<head>内、<body>内など)に応じて、パーサーは異なる挿入モードで動作します。このコミットで焦点となっているのはinBodyIM(in Body Insertion Mode) です。 -
inBodyIM(in Body Insertion Mode):<body>要素がオープンされた後にパーサーが遷移する主要な挿入モードです。ほとんどのHTMLコンテンツはこのモードでパースされます。このモードでは、様々なHTML要素の開始タグ、終了タグ、テキストトークン、コメントなどが、それぞれ特定のルールに従ってDOMツリーに追加されたり、既存の要素が閉じられたりします。 -
framesetOKフラグ: HTML5パースアルゴリズムにおける内部フラグの一つです。このフラグは、パーサーがframeset要素を挿入できる状態にあるかどうかを示します。初期状態ではtrueですが、特定の要素(例:body要素の開始タグ、一部の要素の開始タグなど)がパースされるとfalseに設定されます。一度falseになると、ドキュメントの残りの部分ではframeset要素の挿入が許可されなくなります。これは、framesetがHTML5では非推奨であり、特定の条件下でのみ互換性のために許可されるためです。このフラグの適切な管理は、HTMLドキュメントの構造の整合性を保つ上で重要です。 -
アクティブなフォーマット要素 (Active Formatting Elements): HTML5パースアルゴリズムにおけるもう一つの重要な概念です。これは、
<b>,<i>,<u>,<font>などのフォーマット要素がネストされた場合に、それらの開始タグと終了タグが正しく対応しているかを追跡するために使用されるリストです。パーサーは、特定の状況でこのリストを「再構築 (reconstruct)」する必要があります。 -
自己終了タグ (Self-closing tags): HTML5では、
<br>,<img>,<input>など、内容を持たず、終了タグが不要な要素を指します。これらの要素は、開始タグの直後に/>を付けて自己終了を示すことができます(例:<img src="foo.png" />)。パーサーはこれらのタグを特別に扱い、要素をDOMツリーに追加した後、すぐにその要素を閉じます。
技術的詳細
このコミットにおける技術的な変更点は、主に src/pkg/exp/html/parse.go ファイル内の inBodyIM 関数の switch ステートメントに集中しています。
-
input[type="hidden"]のframesetOK挙動の修正: HTML5仕様では、input要素がtype="hidden"である場合、framesetOKフラグはfalseに設定されるべきではありません。これは、隠し入力フィールドがドキュメントの視覚的な構造に影響を与えず、framesetの挿入可能性を妨げるべきではないためです。 変更前は、area,br,embed,img,input,keygen,wbrといった要素がまとめて処理され、一律にp.framesetOK = falseが実行されていました。 変更後、input要素の場合にそのtype属性をチェックし、値がhiddenであればp.framesetOK = falseの設定をスキップするように修正されました。これにより、仕様に準拠した挙動が実現されます。 -
新規要素の
inBodyIM処理追加:<param>,<source>,<track>: これらの要素は、それぞれ<object>,<audio>,<video>要素の子として使用されるメタデータ要素です。これらは自己終了タグとして扱われ、DOMツリーに追加された後、すぐに閉じられます。これらの要素がパースされた際にもframesetOKフラグはfalseに設定されます。<textarea>: この要素は、preやlistingと同様に、内部のテキストコンテンツをそのまま保持する特殊な要素です。inBodyIMで<textarea>の開始タグが検出されると、パーサーはtextIM(Text Insertion Mode) に遷移し、<textarea>の終了タグが来るまで内部のテキストを処理します。また、<textarea>の直後に改行がある場合、その改行は無視されるべきという仕様があります。このコミットでは、textIM内で<textarea>の子要素がまだない場合に、先頭の改行をスキップするロジックが追加されました。<iframe>:iframe要素は、別のHTMLドキュメントを埋め込むために使用されます。この要素がパースされると、framesetOKフラグはfalseに設定され、パーサーはtextIMに遷移してiframeの内容を処理します。<noembed>,<noscript>: これらの要素は、それぞれ<embed>やスクリプトがサポートされていないブラウザのための代替コンテンツを提供します。これらもiframeと同様に、framesetOKフラグをfalseに設定し、パーサーはtextIMに遷移して内部のコンテンツを処理します。
-
setOriginalIM()の導入:textarea,xmp,iframe,noembed,noscriptのように、特定の要素の内部でテキストコンテンツを処理するためにtextIMに遷移する際、元の挿入モードを保存し、その要素の終了タグが来たときに元のモードに戻る必要があります。p.setOriginalIM()は、この元の挿入モードを保存するためのヘルパー関数です。これにより、パーサーの状態管理がより正確になります。
これらの変更は、HTML5のパース仕様の複雑な詳細を正確に実装するためのものであり、特に framesetOK フラグの挙動や、特定の要素内でのテキスト処理の特殊性に対応しています。
コアとなるコードの変更箇所
--- a/src/pkg/exp/html/parse.go
+++ b/src/pkg/exp/html/parse.go
@@ -643,7 +643,7 @@ func inBodyIM(p *parser) bool {
case TextToken:
d := p.tok.Data
switch n := p.oe.top(); n.Data {
- case "pre", "listing", "textarea":
+ case "pre", "listing":
if len(n.Child) == 0 {
// Ignore a newline at the start of a <pre> block.
if d != "" && d[0] == '\r' {
@@ -779,12 +779,6 @@ func inBodyIM(p *parser) bool {
p.addElement(p.tok.Data, p.tok.Attr)
p.afe = append(p.afe, &scopeMarker)
p.framesetOK = false
- case "area", "br", "embed", "img", "input", "keygen", "wbr":
- p.reconstructActiveFormattingElements()
- p.addElement(p.tok.Data, p.tok.Attr)
- p.oe.pop()
- p.acknowledgeSelfClosingTag()
- p.framesetOK = false
case "table":
if !p.quirks {
p.popUntil(buttonScope, "p")
@@ -793,6 +787,26 @@ func inBodyIM(p *parser) bool {
p.framesetOK = false
p.im = inTableIM
return true
+ case "area", "br", "embed", "img", "input", "keygen", "wbr":
+ p.reconstructActiveFormattingElements()
+ p.addElement(p.tok.Data, p.tok.Attr)
+ p.oe.pop()
+ p.acknowledgeSelfClosingTag()
+ if p.tok.Data == "input" {
+ for _, a := range p.tok.Attr {
+ if a.Key == "type" {
+ if strings.ToLower(a.Val) == "hidden" {
+ // Skip setting framesetOK = false
+ return true
+ }
+ }
+ }
+ }
+ p.framesetOK = false
+ case "param", "source", "track":
+ p.addElement(p.tok.Data, p.tok.Attr)
+ p.oe.pop()
+ p.acknowledgeSelfClosingTag()
case "hr":
p.popUntil(buttonScope, "p")
p.addElement(p.tok.Data, p.tok.Attr)
@@ -852,11 +866,27 @@ func inBodyIM(p *parser) bool {
p.oe.pop()
p.oe.pop()
p.form = nil
+ case "textarea":
+ p.addElement(p.tok.Data, p.tok.Attr)
+ p.setOriginalIM()
+ p.framesetOK = false
+ p.im = textIM
case "xmp":
p.popUntil(buttonScope, "p")
p.reconstructActiveFormattingElements()
p.framesetOK = false
p.addElement(p.tok.Data, p.tok.Attr)
+ p.setOriginalIM()
+ p.im = textIM
+ case "iframe":
+ p.framesetOK = false
+ p.addElement(p.tok.Data, p.tok.Attr)
+ p.setOriginalIM()
+ p.im = textIM
+ case "noembed", "noscript":
+ p.addElement(p.tok.Data, p.tok.Attr)
+ p.setOriginalIM()
+ p.im = textIM
case "math", "svg":
p.reconstructActiveFormattingElements()
if p.tok.Data == "math" {
@@ -1074,7 +1104,20 @@ func textIM(p *parser) bool {
case ErrorToken:
p.oe.pop()
case TextToken:
- p.addText(p.tok.Data)
+ d := p.tok.Data
+ if n := p.oe.top(); n.Data == "textarea" && len(n.Child) == 0 {
+ // Ignore a newline at the start of a <textarea> block.
+ if d != "" && d[0] == '\r' {
+ d = d[1:]
+ }
+ if d != "" && d[0] == '\n' {
+ d = d[1:]
+ }
+ }
+ if d == "" {
+ return true
+ }
+ p.addText(d)
return true
case EndTagToken:
p.oe.pop()
コアとなるコードの解説
上記の差分は、src/pkg/exp/html/parse.go ファイル内の inBodyIM 関数と textIM 関数に対する変更を示しています。
-
inBodyIM関数内のTextToken処理の変更:case "pre", "listing", "textarea":からtextareaが削除され、case "pre", "listing":となっています。これは、textareaのテキスト処理がtextIMでより詳細に扱われるようになったためです。
-
inBodyIM関数内のStartTagToken処理の変更:area,br,embed,img,input,keygen,wbr要素の処理の分離と修正:- 変更前は、これらの要素は一括で処理され、
p.framesetOK = falseが無条件に実行されていました。 - 変更後、このブロックが複製され、
input要素の場合に特別な処理が追加されました。 if p.tok.Data == "input"の条件が追加され、input要素のtype属性がhiddenであるかをチェックしています。if strings.ToLower(a.Val) == "hidden"がtrueの場合、// Skip setting framesetOK = falseのコメントの通り、p.framesetOK = falseの設定をスキップし、return trueで処理を終了しています。これにより、input[type="hidden"]がframesetOKフラグに影響を与えないというHTML5の仕様に準拠します。
- 変更前は、これらの要素は一括で処理され、
param,source,track要素の新規追加:- これらの要素は、自己終了タグとして扱われます。
p.addElement(p.tok.Data, p.tok.Attr)でDOMツリーに要素を追加し、p.oe.pop()で要素スタックからポップし、p.acknowledgeSelfClosingTag()で自己終了タグとして認識させます。- これらの要素がパースされた際にも、
framesetOKフラグはfalseに設定されます(ただし、このコードブロックでは明示的にp.framesetOK = falseは書かれていませんが、上記のareaなどのブロックの後に続くため、その後の処理でfalseになるか、あるいはこれらの要素自体がframesetOKに影響を与えないという仕様に基づいている可能性があります)。
textarea要素の新規追加:p.addElement(p.tok.Data, p.tok.Attr)で要素を追加。p.setOriginalIM()を呼び出し、現在の挿入モードを保存します。これは、textareaの内容をパースするためにtextIMに遷移した後、元のモードに戻るために必要です。p.framesetOK = falseを設定。p.im = textIMで挿入モードをtextIMに変更。
xmp要素の変更:- 既存の
xmp処理にp.setOriginalIM()とp.im = textIMが追加されました。これはtextareaと同様に、xmpの内容をtextIMで処理し、終了後に元のモードに戻るためです。
- 既存の
iframe要素の新規追加:p.framesetOK = falseを設定。p.addElement(p.tok.Data, p.tok.Attr)で要素を追加。p.setOriginalIM()を呼び出し、現在の挿入モードを保存。p.im = textIMで挿入モードをtextIMに変更。
noembed,noscript要素の新規追加:iframeと同様に、p.addElement,p.setOriginalIM,p.im = textIMが実行されます。これらの要素も内部のコンテンツをテキストとして処理するためです。
-
textIM関数内のTextToken処理の変更:textarea要素の開始タグの直後に改行がある場合に、その改行を無視するロジックが追加されました。if n := p.oe.top(); n.Data == "textarea" && len(n.Child) == 0で、現在の要素スタックのトップがtextareaであり、かつそのtextareaがまだ子要素を持っていない(つまり、開始タグの直後である)ことを確認します。- その条件が満たされた場合、
d(テキストデータ) の先頭が\rまたは\nであれば、その文字を削除しています。 - 改行を削除した結果、
dが空になった場合はreturn trueで処理を終了し、空のテキストノードが追加されないようにしています。
これらの変更により、exp/html パーサーはHTML5の複雑なパースルール、特に framesetOK フラグの管理と、特定の要素内でのテキストコンテンツの処理に関して、より正確に準拠するようになりました。
関連リンク
- Go言語の
exp/htmlパッケージのドキュメント (当時のもの): https://pkg.go.dev/exp/html (現在のgolang.org/x/net/htmlに相当) - HTML Standard (HTML5 Parsing Algorithm): https://html.spec.whatwg.org/multipage/parsing.html
- HTML Standard - The in body insertion mode: https://html.spec.whatwg.org/multipage/parsing.html#the-in-body-insertion-mode
- HTML Standard - The frameset-ok flag: https://html.spec.whatwg.org/multipage/parsing.html#the-frameset-ok-flag
- Go Code Review 6094045: https://golang.org/cl/6094045 (コミットメッセージに記載されているGoのコードレビューシステムへのリンク)
参考にした情報源リンク
- https://html.spec.whatwg.org/multipage/parsing.html (HTML5パースアルゴリズムの公式仕様)
- https://html.spec.whatwg.org/multipage/parsing.html#the-in-body-insertion-mode (in Body Insertion Modeに関する詳細)
- https://html.spec.whatwg.org/multipage/parsing.html#the-frameset-ok-flag (framesetOKフラグに関する詳細)
- https://pkg.go.dev/exp/html (Go言語の
exp/htmlパッケージのドキュメント) - https://golang.org/cl/6094045 (Goのコードレビューシステム)
- 一般的なHTML5パースに関するウェブ上の解説記事 (具体的なURLは省略しますが、HTML5のパースアルゴリズム、挿入モード、特定の要素の挙動に関する情報を収集するために参照しました。)