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

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

このコミットは、Go言語の実験的なHTMLパーサーパッケージ exp/html における、HTML5仕様への準拠度を高めるための変更を導入しています。具体的には、inBodyIM (in Body Insertion Mode) と呼ばれるHTML解析モードにおける、開始タグと終了タグの処理ロジックが改善されています。これにより、特に<body>タグと<html>タグの扱いが仕様に近づき、関連するテストが2つ追加でパスするようになりました。

コミット

commit eea5a432cb629670522dc2903d3c464b58652fee
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Fri Apr 20 15:48:13 2012 +1000

    exp/html: start making inBodyIM match the spec

    Reorder some start tags.

    Improve handling of </body>.
    Handle </html>.

    Pass 2 additional tests (by handling </html>).

    R=golang-dev, nigeltao
    CC=golang-dev
    https://golang.org/cl/6082043

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

https://github.com/golang/go/commit/eea5a432cb629670522dc2903d3c464b58652fee

元コミット内容

このコミットは、Go言語のexp/htmlパッケージにおいて、HTML5の解析仕様に準拠するための初期段階の変更を実装しています。主な目的は、inBodyIM(body要素内での挿入モード)におけるタグの処理を改善することです。

具体的には以下の点が変更されました。

  • いくつかの開始タグ(base, basefont, bgsound, command, link, meta, noframes, script, style, title, body, frameset)の処理順序が変更されました。
  • </body>終了タグの処理が改善されました。
  • </html>終了タグの処理が追加されました。

これらの変更により、</html>の処理に関連する2つのテストが追加でパスするようになりました。

変更の背景

HTML5の仕様は、ウェブブラウザがHTMLドキュメントをどのように解析し、DOMツリーを構築するかについて非常に詳細なアルゴリズムを定義しています。このアルゴリズムは、エラーのあるHTML(ほとんどのウェブページがこれに該当します)をどのように処理すべきかについても厳密に規定しています。

Go言語のexp/htmlパッケージは、このHTML5解析仕様に準拠したパーサーを提供することを目指していました。このコミットが行われた時点では、まだ完全に仕様に準拠しているわけではなく、特に<body>要素内でのタグ処理において、仕様との乖離がありました。

このコミットの背景には、以下のような課題認識があったと考えられます。

  1. 仕様準拠の必要性: 堅牢で互換性のあるHTMLパーサーを構築するためには、HTML5仕様に厳密に準拠することが不可欠です。これにより、様々な形式のHTMLドキュメントを正確に解析し、予測可能なDOMツリーを生成できます。
  2. 既存の不整合: inBodyIMにおける特定の開始タグ(特にメタデータコンテンツやスクリプト関連のタグ)の処理が、仕様の期待する動作と異なっていた可能性があります。これらのタグは、たとえ<body>内で見つかったとしても、inHeadIMで処理されるべき挙動を持つ場合があります。
  3. 終了タグの厳密な処理: </body></html>のような重要な終了タグの処理は、DOMツリーの最終的な構造に大きく影響します。特に</html>は、<body>がまだ明示的に閉じられていない場合でも、暗黙的に<body>を閉じるトリガーとなることがあります。これらの終了タグの処理が不正確だと、生成されるDOMツリーがブラウザの挙動と一致せず、互換性の問題を引き起こす可能性があります。
  4. テストの失敗: 既存のテストが特定のHTML構造で失敗していたことは、パーサーの挙動が仕様から逸脱している明確な証拠でした。このコミットは、これらの失敗していたテストをパスさせることを直接的な目標の一つとしています。

これらの背景から、パーサーの正確性と堅牢性を向上させるために、inBodyIMにおけるタグ処理ロジックの再評価と修正が必要とされました。

前提知識の解説

このコミットの変更内容を理解するためには、以下のHTML5解析に関する前提知識が役立ちます。

  1. HTML5解析アルゴリズム:

    • HTML5仕様は、ブラウザがHTMLドキュメントをバイトストリームからDOMツリーに変換するまでの詳細なステップを定義しています。これは、トークナイゼーション(バイトをトークンに変換)とツリー構築(トークンをDOMノードに変換)の2つの主要なフェーズに分かれます。
    • トークン: HTMLパーサーは、入力ストリームを解析して、開始タグ、終了タグ、コメント、文字データ、DOCTYPEなどの「トークン」を生成します。
    • 挿入モード (Insertion Mode): ツリー構築フェーズにおいて、パーサーは現在の状態を示す「挿入モード」を持ちます。このモードは、次に受け取るトークンをどのように処理すべきかを決定します。HTMLドキュメントの異なる部分(例: <html>の外、<head>内、<body>内、<table>内など)に応じて、異なる挿入モードが存在します。
    • スタック上のオープン要素 (Stack of Open Elements): パーサーは、現在開いているHTML要素のスタックを維持します。新しい要素が追加されるとスタックにプッシュされ、要素が閉じられるとポップされます。このスタックは、DOMツリーの階層構造を追跡するために使用されます。
    • スコープ (Scope): 特定の要素が「スコープ内にある」とは、その要素がスタック上のオープン要素のどこかに存在し、特定のルールセット(例: defaultScope, buttonScopeなど)に従って処理されることを意味します。
  2. inBodyIM (In Body Insertion Mode):

    • この挿入モードは、パーサーが<body>要素内(または<body>が暗黙的に開かれている状態)でトークンを処理しているときにアクティブになります。
    • inBodyIMはHTML解析アルゴリズムの中で最も複雑なモードの一つであり、非常に多くの種類のタグと文字データを処理するための詳細なルールが定義されています。
    • このモードでは、通常のコンテンツ(段落、見出し、画像など)の他に、スクリプト、スタイル、メタデータ関連のタグなど、様々な要素が予期せぬ場所で出現する可能性も考慮して処理されます。
  3. 特定のタグの挙動:

    • メタデータコンテンツ: <base>, <link>, <meta>, <noscript>, <script>, <style>, <title>などのタグは、通常<head>要素内に配置されるべき「メタデータコンテンツ」です。しかし、これらが<body>内で見つかった場合でも、HTML5仕様は特定の処理を要求します。多くの場合、これらはinHeadIMで処理されるか、無視されるか、あるいはDOMツリーの特定の場所に移動されます。
    • <body>タグ: <body>開始タグがinBodyIMで再度見つかった場合、通常は無視され、既存の<body>要素の属性が更新されることがあります。
    • <html>タグ: <html>開始タグがinBodyIMで再度見つかった場合も、通常は無視され、既存の<html>要素の属性が更新されることがあります。
    • </body>終了タグ: このタグは、スタック上のオープン要素を適切に閉じ、パーサーの挿入モードをafterBodyIMに切り替える役割を果たします。
    • </html>終了タグ: このタグは、</body>がまだ明示的に閉じられていない場合でも、暗黙的に<body>を閉じ、パーサーの挿入モードをafterAfterBodyIMに切り替えることがあります。
  4. framesetOKフラグ:

    • HTML5解析アルゴリズムの一部として存在する内部フラグです。
    • このフラグは、<frameset>要素が挿入可能かどうかを制御します。通常、<body>要素が構築されたり、特定の要素が解析されたりすると、このフラグはfalseに設定され、それ以降<frameset>の挿入が許可されなくなります。

これらの概念を理解することで、コミットがなぜ特定のタグの処理順序を変更したり、特定の条件を追加したりしたのかが明確になります。それは、HTML5仕様の複雑なルールセットにパーサーの挙動を合わせるためです。

技術的詳細

このコミットは、src/pkg/exp/html/parse.goファイル内のinBodyIM関数に焦点を当て、HTML5解析仕様の「8.2.5.4.7 The "in body" insertion mode」セクションに準拠するための変更を加えています。

主要な変更点は以下の通りです。

  1. 開始タグの処理順序の変更とinHeadIMへの委譲:

    • 変更前は、inBodyIM関数内で、base, basefont, bgsound, command, link, meta, noframes, script, style, titleといったメタデータコンテンツやスクリプト関連の開始タグが、他の一般的なブロックレベル要素(address, articleなど)の後に処理されていました。
    • 変更後、これらのタグの処理がinBodyIMのより早い段階に移動され、return inHeadIM(p)が呼び出されるようになりました。
      • 理由: HTML5仕様では、これらの要素が<body>内で見つかった場合でも、多くの場合、<head>内で処理されるべきルールが適用されます。inHeadIMに処理を委譲することで、パーサーはこれらのタグを仕様に沿って適切に処理できるようになります。例えば、<script>タグが<body>の途中で見つかった場合でも、そのスクリプトは通常、ドキュメントのヘッド部分に属するものとして扱われるべきです。
  2. <body>開始タグの処理の移動と改善:

    • <body>開始タグの処理も、他のメタデータ関連タグと同様に、inBodyIMのより早い段階に移動されました。
    • 既存の<body>要素が存在する場合(p.oe[1]body要素である場合)、その属性を新しいトークンの属性で上書きし、p.framesetOKフラグをfalseに設定します。
      • 理由: HTML5仕様では、<body>開始タグが複数回出現した場合、最初の<body>要素の属性を更新し、新しい<body>要素を作成しないように規定されています。また、<body>要素が構築された後は、<frameset>の挿入が許可されないため、framesetOKfalseに設定することが重要です。
  3. frameset開始タグの処理の移動と改善:

    • frameset開始タグの処理も、inBodyIMのより早い段階に移動されました。
    • 処理ロジックがより厳密になりました。!p.framesetOK || len(p.oe) < 2 || p.oe[1].Data != "body"という条件が追加され、framesetOKfalseであるか、スタックにbody要素がない場合はトークンを無視します。
    • 有効なframesetの場合、既存のbody要素をDOMツリーから削除し、スタックからポップし、新しいframeset要素を追加して、挿入モードをinFramesetIMに切り替えます。
      • 理由: frameset要素は、<body>要素と同時に存在できないため、framesetが有効な場合にbodyを適切に置き換える必要があります。framesetOKフラグのチェックは、この置き換えが許可されるタイミングを制御します。
  4. </body>終了タグの処理の改善:

    • 変更前は、</body>終了タグが検出された際に、単にp.im = afterBodyIMを設定するだけでした(コメントでTODO: autoclose the stack of open elements.と記載)。
    • 変更後、p.elementInScope(defaultScope, "body")という条件が追加されました。この条件が真である場合にのみ、p.im = afterBodyIMが設定されます。
      • 理由: </body>終了タグは、body要素が実際に「デフォルトスコープ内」に存在する場合にのみ、パーサーのモードをafterBodyIMに切り替えるべきです。これにより、不適切な</body>タグが誤ってパーサーの状態を変更するのを防ぎ、より堅牢なエラー処理が可能になります。
  5. </html>終了タグの処理の追加:

    • inBodyIMEndTagToken処理に、htmlケースが新たに追加されました。
    • このケースでは、p.elementInScope(defaultScope, "body")が真である場合、p.parseImpliedToken(EndTagToken, "body", nil)を呼び出し、return falseとします。
      • 理由: HTML5仕様では、</html>終了タグがinBodyIMで検出された場合、まず暗黙的に</body>終了タグが検出されたかのように処理されます。p.parseImpliedTokenは、この暗黙的なトークン処理を実行し、return falseは現在の</html>トークンを再処理させることを意味します。これにより、</html></body>の暗黙的な閉鎖を引き起こし、その後にafterBodyIMモードで適切に処理されるという仕様の挙動が実現されます。

これらの変更は、HTML5解析アルゴリズムの複雑な状態遷移と要素のスコープルールを正確に実装することを目的としています。特に、タグの出現順序や、特定のタグが他のタグの存在にどのように影響するかといった細かな挙動が、仕様に沿って調整されています。

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

変更は主にsrc/pkg/exp/html/parse.goファイル内のinBodyIM関数に集中しています。

--- a/src/pkg/exp/html/parse.go
+++ b/src/pkg/exp/html/parse.go
@@ -640,6 +640,29 @@ func inBodyIM(p *parser) bool {
 		switch p.tok.Data {
 		case "html":
 			copyAttributes(p.oe[0], p.tok)
+		case "base", "basefont", "bgsound", "command", "link", "meta", "noframes", "script", "style", "title":
+			return inHeadIM(p)
+		case "body":
+			if len(p.oe) >= 2 {
+				body := p.oe[1]
+				if body.Type == ElementNode && body.Data == "body" {
+					p.framesetOK = false
+					copyAttributes(body, p.tok)
+				}
+			}
+		case "frameset":
+			if !p.framesetOK || len(p.oe) < 2 || p.oe[1].Data != "body" {
+				// Ignore the token.
+				return true
+			}
+			body := p.oe[1]
+			if body.Parent != nil {
+				body.Parent.Remove(body)
+			}
+			p.oe = p.oe[:1]
+			p.addElement(p.tok.Data, p.tok.Attr)
+			p.im = inFramesetIM
+			return true
 		case "address", "article", "aside", "blockquote", "center", "details", "dir", "div", "dl", "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "menu", "nav", "ol", "p", "section", "summary", "ul":
 		p.popUntil(buttonScope, "p")
 		p.addElement(p.tok.Data, p.tok.Attr)
@@ -758,29 +781,6 @@ func inBodyIM(p *parser) bool {
 			p.reconstructActiveFormattingElements()
 			p.addElement(p.tok.Data, p.tok.Attr)
-		case "body":
-			if len(p.oe) >= 2 {
-				body := p.oe[1]
-				if body.Type == ElementNode && body.Data == "body" {
-					p.framesetOK = false
-					copyAttributes(body, p.tok)
-				}
-			}
-		case "frameset":
-			if !p.framesetOK || len(p.oe) < 2 || p.oe[1].Data != "body" {
-				// Ignore the token.
-				return true
-			}
-			body := p.oe[1]
-			if body.Parent != nil {
-				body.Parent.Remove(body)
-			}
-			p.oe = p.oe[:1]
-			p.addElement(p.tok.Data, p.tok.Attr)
-			p.im = inFramesetIM
-			return true
-		case "base", "basefont", "bgsound", "command", "link", "meta", "noframes", "script", "style", "title":
-			return inHeadIM(p)
 		case "image":
 		p.tok.Data = "img"
 		return false
@@ -847,8 +847,14 @@ func inBodyIM(p *parser) bool {
 	case EndTagToken:
 		switch p.tok.Data {
 		case "body":
-			// TODO: autoclose the stack of open elements.
-			p.im = afterBodyIM
+			if p.elementInScope(defaultScope, "body") {
+				p.im = afterBodyIM
+			}
+		case "html":
+			if p.elementInScope(defaultScope, "body") {
+				p.parseImpliedToken(EndTagToken, "body", nil)
+				return false
+			}
 			return true
 		case "p":
 		if !p.elementInScope(buttonScope, "p") {

また、以下のテストログファイルも変更され、関連するテストがパスしたことを示しています。

  • src/pkg/exp/html/testlogs/tests15.dat.log
  • src/pkg/exp/html/testlogs/webkit01.dat.log

コアとなるコードの解説

inBodyIM関数は、HTMLパーサーが<body>要素のコンテンツを解析している際の挙動を定義しています。この関数は、入力トークン(p.tok)の種類に応じて異なる処理を行います。

開始タグ (StartTagToken) の処理:

変更の核心は、特定の開始タグの処理順序とロジックの変更です。

  • メタデータ/スクリプト関連タグの移動:

    		case "base", "basefont", "bgsound", "command", "link", "meta", "noframes", "script", "style", "title":
    			return inHeadIM(p)
    

    これらのタグがinBodyIMで検出された場合、パーサーは直ちにinHeadIM(ヘッド挿入モード)に切り替えて処理を委譲します。これは、これらの要素がたとえ<body>内で見つかったとしても、HTML5仕様上は<head>内で処理されるべき性質を持つためです。return inHeadIM(p)は、現在のトークンをinHeadIMのルールに従って処理し、その結果を返すことを意味します。

  • <body>開始タグの処理:

    		case "body":
    			if len(p.oe) >= 2 {
    				body := p.oe[1]
    				if body.Type == ElementNode && body.Data == "body" {
    					p.framesetOK = false
    					copyAttributes(body, p.tok)
    				}
    			}
    

    p.oeは「スタック上のオープン要素」を表します。len(p.oe) >= 2は、少なくとも<html><body>がスタックに存在することを示唆します。p.oe[1]が既存の<body>要素である場合、新しい<body>トークンの属性を既存のbody要素にコピーします。また、p.framesetOK = falseを設定することで、これ以降<frameset>要素の挿入が許可されないようにします。これは、HTML5仕様で<body>要素が既に存在する場合の<body>開始タグの処理方法に準拠しています。

  • frameset開始タグの処理:

    		case "frameset":
    			if !p.framesetOK || len(p.oe) < 2 || p.oe[1].Data != "body" {
    				// Ignore the token.
    				return true
    			}
    			body := p.oe[1]
    			if body.Parent != nil {
    				body.Parent.Remove(body)
    			}
    			p.oe = p.oe[:1]
    			p.addElement(p.tok.Data, p.tok.Attr)
    			p.im = inFramesetIM
    			return true
    

    このロジックは、<frameset>タグが有効な場合にのみ処理を進めます。

    • !p.framesetOK: framesetOKフラグがfalseの場合(つまり、<frameset>の挿入が許可されていない場合)。
    • len(p.oe) < 2 || p.oe[1].Data != "body": スタックに<body>要素が存在しないか、<body>が適切に開かれていない場合。 これらの条件のいずれかが真であれば、トークンは無視されます。 有効な<frameset>の場合、既存の<body>要素をDOMツリーから削除し、スタックからポップします。その後、新しい<frameset>要素をDOMに追加し、挿入モードをinFramesetIM(フレームセット挿入モード)に切り替えます。これは、<body><frameset>が同時に存在できないというHTMLのルールを反映しています。

終了タグ (EndTagToken) の処理:

  • </body>終了タグの処理:

    		case "body":
    			if p.elementInScope(defaultScope, "body") {
    				p.im = afterBodyIM
    			}
    			return true
    

    p.elementInScope(defaultScope, "body")は、<body>要素が「デフォルトスコープ内」に存在するかどうかを確認します。つまり、スタック上のオープン要素の中に<body>が適切に開かれている状態であるかをチェックします。この条件が満たされた場合にのみ、パーサーの挿入モードをafterBodyIM(body要素後挿入モード)に切り替えます。これにより、</body>タグが不適切な場所で出現した場合に、パーサーの状態が誤って変更されるのを防ぎます。

  • </html>終了タグの処理:

    		case "html":
    			if p.elementInScope(defaultScope, "body") {
    				p.parseImpliedToken(EndTagToken, "body", nil)
    				return false
    			}
    			return true
    

    </html>終了タグが検出された場合、まずp.elementInScope(defaultScope, "body")<body>がデフォルトスコープ内にあるかを確認します。もしそうであれば、p.parseImpliedToken(EndTagToken, "body", nil)を呼び出します。これは、あたかも</body>終了タグが検出されたかのように、暗黙的にbody要素を閉じる処理を実行します。その後、return falseとすることで、現在の</html>トークンを再処理させます。これにより、</html></body>の暗黙的な閉鎖を引き起こし、その後にafterBodyIMモードで適切に処理されるというHTML5仕様の挙動が実現されます。

これらの変更は、HTML5解析アルゴリズムの複雑な状態遷移と要素のスコープルールを正確に実装し、パーサーの堅牢性と仕様準拠度を向上させるためのものです。

関連リンク

  • Go言語のexp/htmlパッケージのドキュメント(当時のもの、現在はgolang.org/x/net/htmlに統合されている可能性が高い)
  • HTML5仕様: https://html.spec.whatwg.org/multipage/parsing.html (特に "8.2.5.4.7 The "in body" insertion mode" セクション)

参考にした情報源リンク