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

[インデックス 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) 関数に、以下の変更を加えています。

  1. input 要素のうち type="hidden" のものに対しては、framesetOK フラグを false に設定しないように修正。
  2. <param>, <source>, <track>, <textarea>, <iframe>, <noembed>, <noscript> といったHTML要素のパース処理を inBodyIM に追加。
  3. これらの変更により、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パースアルゴリズムに関する前提知識が必要です。

  1. HTML5パースアルゴリズム: HTML5のパースアルゴリズムは、ウェブブラウザがHTML文字列をDOMツリーに変換する手順を詳細に定義したものです。これは、トークン化フェーズとツリー構築フェーズの2つの主要なフェーズに分かれています。

    • トークン化 (Tokenization): 入力されたHTML文字列を、タグ、属性、テキストなどの「トークン」に分解するプロセスです。
    • ツリー構築 (Tree Construction): トークンストリームを受け取り、それらを基にDOMツリーを構築するプロセスです。このフェーズは、現在の「挿入モード (Insertion Mode)」に基づいて動作します。
  2. 挿入モード (Insertion Mode): ツリー構築フェーズにおいて、パーサーが次に受け取るトークンをどのように処理するかを決定する状態機械です。HTMLドキュメントの異なる部分(例: <html> の前、<head> 内、<body> 内など)に応じて、パーサーは異なる挿入モードで動作します。このコミットで焦点となっているのは inBodyIM (in Body Insertion Mode) です。

  3. inBodyIM (in Body Insertion Mode): <body> 要素がオープンされた後にパーサーが遷移する主要な挿入モードです。ほとんどのHTMLコンテンツはこのモードでパースされます。このモードでは、様々なHTML要素の開始タグ、終了タグ、テキストトークン、コメントなどが、それぞれ特定のルールに従ってDOMツリーに追加されたり、既存の要素が閉じられたりします。

  4. framesetOK フラグ: HTML5パースアルゴリズムにおける内部フラグの一つです。このフラグは、パーサーが frameset 要素を挿入できる状態にあるかどうかを示します。初期状態では true ですが、特定の要素(例: body 要素の開始タグ、一部の要素の開始タグなど)がパースされると false に設定されます。一度 false になると、ドキュメントの残りの部分では frameset 要素の挿入が許可されなくなります。これは、frameset がHTML5では非推奨であり、特定の条件下でのみ互換性のために許可されるためです。このフラグの適切な管理は、HTMLドキュメントの構造の整合性を保つ上で重要です。

  5. アクティブなフォーマット要素 (Active Formatting Elements): HTML5パースアルゴリズムにおけるもう一つの重要な概念です。これは、<b>, <i>, <u>, <font> などのフォーマット要素がネストされた場合に、それらの開始タグと終了タグが正しく対応しているかを追跡するために使用されるリストです。パーサーは、特定の状況でこのリストを「再構築 (reconstruct)」する必要があります。

  6. 自己終了タグ (Self-closing tags): HTML5では、<br>, <img>, <input> など、内容を持たず、終了タグが不要な要素を指します。これらの要素は、開始タグの直後に /> を付けて自己終了を示すことができます(例: <img src="foo.png" />)。パーサーはこれらのタグを特別に扱い、要素をDOMツリーに追加した後、すぐにその要素を閉じます。

技術的詳細

このコミットにおける技術的な変更点は、主に src/pkg/exp/html/parse.go ファイル内の inBodyIM 関数の switch ステートメントに集中しています。

  1. 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 の設定をスキップするように修正されました。これにより、仕様に準拠した挙動が実現されます。

  2. 新規要素の inBodyIM 処理追加:

    • <param>, <source>, <track>: これらの要素は、それぞれ <object>, <audio>, <video> 要素の子として使用されるメタデータ要素です。これらは自己終了タグとして扱われ、DOMツリーに追加された後、すぐに閉じられます。これらの要素がパースされた際にも framesetOK フラグは false に設定されます。
    • <textarea>: この要素は、prelisting と同様に、内部のテキストコンテンツをそのまま保持する特殊な要素です。inBodyIM<textarea> の開始タグが検出されると、パーサーは textIM (Text Insertion Mode) に遷移し、<textarea> の終了タグが来るまで内部のテキストを処理します。また、<textarea> の直後に改行がある場合、その改行は無視されるべきという仕様があります。このコミットでは、textIM 内で <textarea> の子要素がまだない場合に、先頭の改行をスキップするロジックが追加されました。
    • <iframe>: iframe 要素は、別のHTMLドキュメントを埋め込むために使用されます。この要素がパースされると、framesetOK フラグは false に設定され、パーサーは textIM に遷移して iframe の内容を処理します。
    • <noembed>, <noscript>: これらの要素は、それぞれ <embed> やスクリプトがサポートされていないブラウザのための代替コンテンツを提供します。これらも iframe と同様に、framesetOK フラグを false に設定し、パーサーは textIM に遷移して内部のコンテンツを処理します。
  3. 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 関数に対する変更を示しています。

  1. inBodyIM 関数内の TextToken 処理の変更:

    • case "pre", "listing", "textarea": から textarea が削除され、case "pre", "listing": となっています。これは、textarea のテキスト処理が textIM でより詳細に扱われるようになったためです。
  2. 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 が実行されます。これらの要素も内部のコンテンツをテキストとして処理するためです。
  3. 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 フラグの管理と、特定の要素内でのテキストコンテンツの処理に関して、より正確に準拠するようになりました。

関連リンク

参考にした情報源リンク