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

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

このコミットは、Go言語の実験的なHTMLパーサー(exp/htmlパッケージ)におけるHTML解析ロジックの改善を目的としています。具体的には、HTML5の「inBody」挿入モード(inBodyIM)における要素の処理をより正確にするための変更が含まれています。特に、<pre>, <listing>, </form>, <li>, </dd>, </dt>, <h1>から<h6>といった特定のタグの処理が修正され、関連するテストケースが追加または修正されています。

コミット

commit 904c7c8e9905c7ef7dfe817f8acb50a5f9fdd04b
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Sat Apr 21 09:20:38 2012 +1000

    exp/html: more work on inBodyIM
    
    Reorder some cases.
    Handle <pre>, <listing>, </form>, </li>, </dd>, </dt>, </h1>, </h2>,
    </h3>, </h4>, </h5>, and </h6> tags.
    
    Pass 6 additional tests.
    
    R=golang-dev, nigeltao
    CC=golang-dev
    https://golang.org/cl/6089043

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

https://github.com/golang/go/commit/904c7c8e9905c7ef7dfe817f8acb50a5f9fdd04b

元コミット内容

exp/html: more work on inBodyIM

Reorder some cases.
Handle <pre>, <listing>, </form>, </li>, </dd>, </dt>, </h1>, </h2>,
</h3>, </h4>, </h5>, and <h6> tags.

Pass 6 additional tests.

変更の背景

この変更は、Go言語のHTMLパーサーがHTML5仕様に準拠し、より堅牢になることを目的としています。HTMLの解析は非常に複雑であり、特にエラーのあるHTMLやブラウザが許容する非標準的なマークアップを正確に処理するためには、詳細な仕様(HTML5 Parsing Algorithm)に厳密に従う必要があります。

inBodyIM(inBody Insertion Mode)は、HTMLドキュメントの<body>要素内でのトークン処理を司る、HTML5解析アルゴリズムの中核をなす状態の一つです。このモードでは、様々なタグやテキストデータがどのようにDOMツリーに挿入されるか、あるいは既存の要素がどのように閉じられるかが定義されています。

このコミットは、特定のタグ(特にブロックレベル要素やリスト関連要素、見出し要素)の開始タグおよび終了タグが、inBodyIMにおいてHTML5仕様に沿って正しく処理されるように、パーサーのロジックを調整しています。これにより、より多くのエッジケースや複雑なHTML構造が正確に解析され、生成されるDOMツリーがブラウザの挙動と一致するようになります。テストの追加と合格は、これらの改善が実際に機能していることを示しています。

前提知識の解説

HTML5解析アルゴリズム

HTML5の解析アルゴリズムは、ウェブブラウザがHTMLドキュメントをどのように読み込み、DOMツリーを構築するかを厳密に定義しています。これは、単なる構文解析にとどまらず、エラー処理や、ブラウザが非整形式のHTMLをどのように「修正」するか(エラー回復)についても詳細に規定しています。

主要な概念は以下の通りです。

  • トークナイゼーション(Tokenization): 入力ストリーム(HTML文字列)をトークン(開始タグ、終了タグ、テキスト、コメントなど)に分解するプロセス。
  • ツリー構築(Tree Construction): トークンストリームをDOMツリーに変換するプロセス。このプロセスは、一連の「挿入モード(Insertion Modes)」によって制御されます。
  • 挿入モード(Insertion Modes): HTMLドキュメントの現在の解析状態に応じて、異なるトークン処理ルールを適用するための状態機械。例えば、initial, before html, before head, in head, in bodyなどがあります。
  • スタック・オブ・オープン・エレメンツ(Stack of Open Elements): 現在開いているHTML要素のスタック。新しい要素が開始されるとプッシュされ、要素が閉じられるとポップされます。このスタックは、DOMツリーの階層構造を追跡するために使用されます。
  • アクティブ・フォーマット要素(Active Formatting Elements): <b>, <i>, <a>などのフォーマット要素が、そのスコープ内でどのように処理されるかを追跡するためのリスト。ネストされたフォーマット要素や、誤って閉じられたフォーマット要素の回復処理に利用されます。
  • 含意的な終了タグの生成(Generating Implied End Tags): HTML5仕様では、特定の状況下で、明示的な終了タグがなくても要素が自動的に閉じられる(含意的に終了する)と定義されています。例えば、<p>タグの後に別のブロックレベル要素が来た場合などです。

inBodyIM (inBody Insertion Mode)

inBodyIMは、HTMLパーサーが<body>要素のコンテンツを解析しているときにアクティブになる挿入モードです。このモードは、HTML解析アルゴリズムの中で最も複雑で、多くの異なるタグや状況を処理します。

inBodyIMの主な役割は以下の通りです。

  • テキストトークンの処理: テキストノードとしてDOMツリーに追加されます。
  • 開始タグの処理: タグの種類に応じて、新しい要素がスタック・オブ・オープン・エレメンツにプッシュされ、DOMツリーに追加されます。特定のタグ(例: <img>, <br>)は自己終了要素として扱われます。
  • 終了タグの処理: 対応する開始タグがスタック・オブ・オープン・エレメンツからポップされ、要素が閉じられます。多くの終了タグは、その前に開いている特定の要素を自動的に閉じる(含意的な終了タグを生成する)挙動を持っています。
  • 特殊な要素の処理: <form>, <table>, <select>などの要素は、それぞれ独自の解析ルールや、パーサーの挿入モードを一時的に変更する場合があります。

このコミットは、特にinBodyIMにおけるこれらのルール、特に含意的な終了タグの生成や、特定のタグが来た際のスタック操作の正確性を向上させています。

技術的詳細

このコミットの主要な変更点は、src/pkg/exp/html/parse.goファイル内のinBodyIM関数と、新しく追加されたgenerateImpliedEndTags関数のロジックにあります。

generateImpliedEndTags関数の追加

この新しい関数は、HTML5解析アルゴリズムにおける「含意的な終了タグの生成」のステップを実装しています。これは、特定の要素(dd, dt, li, option, optgroup, p, rp, rt)がスタック・オブ・オープン・エレメンツの最上位にある場合に、それらを自動的にポップ(閉じる)する役割を担います。

  • 目的: HTML5仕様では、これらの要素は特定の状況下で明示的な終了タグがなくても自動的に閉じられるべきだとされています。この関数は、その挙動をパーサーに組み込みます。
  • 例外処理: exceptions引数を使用することで、特定の要素が自動的に閉じられるのを防ぐことができます。これは、例えば<p>タグの後に別の<p>タグが来た場合に、最初の<p>が自動的に閉じられるが、その中に特定の要素が含まれている場合は閉じない、といった複雑なルールに対応するために重要です。
  • 実装: スタックを逆順に走査し、指定されたタグ名の要素が見つかった場合に、その要素をスタックからポップします。例外が指定されている場合は、その要素が例外リストに含まれていればポップせずにループを終了します。

inBodyIM関数の変更

inBodyIM関数は、HTML5の「inBody」挿入モードのロジックを実装しています。このコミットでは、以下の点が変更されています。

  1. ケースの並べ替え: switch p.tok.Data文内のケースの順序が変更されています。これは、HTML5仕様のアルゴリズムのフローに合わせるため、またはパフォーマンスの最適化のために行われた可能性があります。
  2. <pre>および<listing>タグの処理:
    • 以前は、これらのタグが来た際にp.oe.pop()p.acknowledgeSelfClosingTag()が呼び出されていましたが、これは削除されました。
    • 代わりに、コメントで「The newline, if any, will be dealt with by the TextToken case.」と記述されており、<pre><listing>内の改行文字の処理がテキストトークン処理に委ねられることを示唆しています。これは、これらの要素がホワイトスペースを保持する特性を持つため、その内部のテキスト処理をより正確に行うための変更と考えられます。
  3. </form>終了タグの処理:
    • </form>終了タグが来た際の処理が追加されました。
    • パーサーが保持しているformノード(現在開いている<form>要素)が存在し、かつそれがスコープ内に存在する場合にのみ、含意的な終了タグを生成し、formノードをスタックから削除します。
    • これにより、HTML5仕様に沿って<form>要素が正しく閉じられるようになります。
  4. </li>, </dd>, </dt>, </h1>から</h6>終了タグの処理:
    • これらの終了タグが来た際の処理が追加または修正されました。
    • <li>終了タグの場合、以前はp.popUntil(listItemScope, "li")が呼び出されていましたが、p.oe = p.oe[:i]に変更されました。これは、<li>要素が閉じられる際に、スタック上の<li>要素までをポップするのではなく、その要素自体をスタックから削除する、より直接的な操作に変わったことを示唆しています。
    • dd, dt, h1からh6の終了タグについては、p.popUntil(defaultScope, p.tok.Data)またはp.popUntil(defaultScope, "h1", "h2", "h3", "h4", "h5", "h6")が呼び出されるようになりました。これは、これらの要素が閉じられる際に、対応する開始タグまでスタックをポップする挙動を実装しています。
  5. 既存のタグ処理の移動:
    • <a>, <b>, big, code, em, font, i, s, small, strike, strong, tt, u, nobr, applet, marquee, object, area, br, embed, img, input, keygen, wbr, table, hr, selectなどのタグの処理が、inBodyIM関数内で移動しています。これは、HTML5仕様のアルゴリズムのフローに合わせるための再編成と考えられます。機能的な変更というよりは、コードの構造と可読性の改善、および仕様への準拠をより明確にするためのものです。

テストログの変更

src/pkg/exp/html/testlogs/tests19.dat.logファイルでは、6つのテストケースの結果がFAILからPASSに変更されています。これは、上記のparse.goの変更によって、これらのテストケースが示す特定のHTML構造が正しく解析されるようになったことを意味します。

具体的には、以下のテストケースが合格するようになりました。

  • <!doctype html><body><p><pre>
  • <!doctype html><body><p><listing>
  • <!doctype html><h1><div><h3><span></h1>foo
  • <!doctype html><h3><li>abc</h2>foo
  • <!doctype html><pre><frameset>
  • <!doctype html><listing><frameset>

これらのテストケースは、<pre>, <listing>, 見出しタグ(<h1>など)、リストアイテム(<li>)などの要素が、特定のコンテキスト(例: <p>要素の内部、ネストされた見出し、<div>の内部)でどのように処理されるべきか、そしてframesetのような特殊な要素との組み合わせでどのように振る舞うべきかを検証しています。

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

--- a/src/pkg/exp/html/parse.go
+++ b/src/pkg/exp/html/parse.go
@@ -166,6 +166,31 @@ func (p *parser) clearStackToContext(s scope) {
 	}
 }
 
+// generateImpliedEndTags pops nodes off the stack of open elements as long as
+// the top node has a tag name of dd, dt, li, option, optgroup, p, rp, or rt.
+// If exceptions are specified, nodes with that name will not be popped off.
+func (p *parser) generateImpliedEndTags(exceptions ...string) {
+	var i int
+loop:
+	for i = len(p.oe) - 1; i >= 0; i-- {
+		n := p.oe[i]
+		if n.Type == ElementNode {
+			switch n.Data {
+			case "dd", "dt", "li", "option", "optgroup", "p", "rp", "rt":
+				for _, except := range exceptions {
+					if n.Data == except {
+						break loop
+					}
+				}
+				continue
+			}
+		}
+		break
+	}
+
+	p.oe = p.oe[:i+1]
+}
+
 // addChild adds a child node n to the top element, and pushes n onto the stack
 // of open elements if it is an element node.\n func (p *parser) addChild(n *Node) {
 @@ -673,58 +698,11 @@ func inBodyIM(p *parser) bool {
 			p.oe.pop()
 		}
 		p.addElement(p.tok.Data, p.tok.Attr)
-		case "a":
-			for i := len(p.afe) - 1; i >= 0 && p.afe[i].Type != scopeMarkerNode; i-- {
-				if n := p.afe[i]; n.Type == ElementNode && n.Data == "a" {
-					p.inBodyEndTagFormatting("a")
-					p.oe.remove(n)
-					p.afe.remove(n)
-					break
-				}
-			}
-			p.reconstructActiveFormattingElements()
-			p.addFormattingElement(p.tok.Data, p.tok.Attr)
-		case "b", "big", "code", "em", "font", "i", "s", "small", "strike", "strong", "tt", "u":
-			p.reconstructActiveFormattingElements()
-			p.addFormattingElement(p.tok.Data, p.tok.Attr)
-		case "nobr":
-			p.reconstructActiveFormattingElements()
-			if p.elementInScope(defaultScope, "nobr") {
-				p.inBodyEndTagFormatting("nobr")
-				p.reconstructActiveFormattingElements()
-			}
-			p.addFormattingElement(p.tok.Data, p.tok.Attr)
-		case "applet", "marquee", "object":
-			p.reconstructActiveFormattingElements()
-			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")
-			}
-			p.addElement(p.tok.Data, p.tok.Attr)
-			p.framesetOK = false
-			p.im = inTableIM
-			return true
-		case "hr":
+		case "pre", "listing":
 			p.popUntil(buttonScope, "p")
 			p.addElement(p.tok.Data, p.tok.Attr)
-			p.oe.pop()
-			p.acknowledgeSelfClosingTag()
-			p.framesetOK = false
-		case "select":
-			p.reconstructActiveFormattingElements()
-			p.addElement(p.tok.Data, p.tok.Attr)
 			p.framesetOK = false
-			p.im = inSelectIM
-			return true
 		case "form":
 			if p.form == nil {
 				p.popUntil(buttonScope, "p")
@@ -737,7 +715,7 @@ func inBodyIM(p *parser) bool {
 			node := p.oe[i]
 			switch node.Data {
 			case "li":
-				p.popUntil(listItemScope, "li")
+				p.oe = p.oe[:i]
 			case "address", "div", "p":
 				continue
 			default:
@@ -775,6 +753,58 @@ func inBodyIM(p *parser) bool {
 			p.reconstructActiveFormattingElements()
 			p.addElement(p.tok.Data, p.tok.Attr)
 			p.framesetOK = false
+		case "a":
+			for i := len(p.afe) - 1; i >= 0 && p.afe[i].Type != scopeMarkerNode; i-- {
+				if n := p.afe[i]; n.Type == ElementNode && n.Data == "a" {
+					p.inBodyEndTagFormatting("a")
+					p.oe.remove(n)
+					p.afe.remove(n)
+					break
+				}
+			}
+			p.reconstructActiveFormattingElements()
+			p.addFormattingElement(p.tok.Data, p.tok.Attr)
+		case "b", "big", "code", "em", "font", "i", "s", "small", "strike", "strong", "tt", "u":
+			p.reconstructActiveFormattingElements()
+			p.addFormattingElement(p.tok.Data, p.tok.Attr)
+		case "nobr":
+			p.reconstructActiveFormattingElements()
+			if p.elementInScope(defaultScope, "nobr") {
+				p.inBodyEndTagFormatting("nobr")
+				p.reconstructActiveFormattingElements()
+			}
+			p.addFormattingElement(p.tok.Data, p.tok.Attr)
+		case "applet", "marquee", "object":
+			p.reconstructActiveFormattingElements()
+			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")
+			}
+			p.addElement(p.tok.Data, p.tok.Attr)
+			p.framesetOK = false
+			p.im = inTableIM
+			return true
+		case "hr":
+			p.popUntil(buttonScope, "p")
+			p.addElement(p.tok.Data, p.tok.Attr)
+			p.oe.pop()
+			p.acknowledgeSelfClosingTag()
+			p.framesetOK = false
+		case "select":
+			p.reconstructActiveFormattingElements()
+			p.addElement(p.tok.Data, p.tok.Attr)
+			p.framesetOK = false
+			p.im = inSelectIM
+			return true
 		case "optgroup", "option":
 		if p.top().Data == "option" {
 			p.oe.pop()
@@ -856,15 +886,31 @@ func inBodyIM(p *parser) bool {
 				return false
 			}
 			return true
+		case "address", "article", "aside", "blockquote", "button", "center", "details", "dir", "div", "dl", "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "listing", "menu", "nav", "ol", "pre", "section", "summary", "ul":
+			p.popUntil(defaultScope, p.tok.Data)
+		case "form":
+			node := p.form
+			p.form = nil
+			i := p.indexOfElementInScope(defaultScope, "form")
+			if node == nil || i == -1 || p.oe[i] != node {
+				// Ignore the token.
+				return true
+			}
+			p.generateImpliedEndTags()
+			p.oe.remove(node)
 		case "p":
 		if !p.elementInScope(buttonScope, "p") {
 			p.addElement("p", nil)
 		}
 		p.popUntil(buttonScope, "p")
+		case "li":
+			p.popUntil(listItemScope, "li")
+		case "dd", "dt":
+			p.popUntil(defaultScope, p.tok.Data)
+		case "h1", "h2", "h3", "h4", "h5", "h6":
+			p.popUntil(defaultScope, "h1", "h2", "h3", "h4", "h5", "h6")
 		case "a", "b", "big", "code", "em", "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u":
 		p.inBodyEndTagFormatting(p.tok.Data)
-		case "address", "article", "aside", "blockquote", "button", "center", "details", "dir", "div", "dl", "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "listing", "menu", "nav", "ol", "pre", "section", "summary", "ul":
-		p.popUntil(defaultScope, p.tok.Data)
 		case "applet", "marquee", "object":
 		if p.popUntil(defaultScope, p.tok.Data) {
 			p.clearActiveFormattingElements()

コアとなるコードの解説

generateImpliedEndTags関数

この関数は、HTML5の解析アルゴリズムにおける重要なステップである「含意的な終了タグの生成」を実装しています。

func (p *parser) generateImpliedEndTags(exceptions ...string) {
	var i int
loop:
	for i = len(p.oe) - 1; i >= 0; i-- { // スタック・オブ・オープン・エレメンツを逆順に走査
		n := p.oe[i]
		if n.Type == ElementNode { // 要素ノードの場合のみ処理
			switch n.Data {
			case "dd", "dt", "li", "option", "optgroup", "p", "rp", "rt": // 含意的に閉じられる可能性のあるタグ
				for _, except := range exceptions { // 例外が指定されているかチェック
					if n.Data == except { // 現在のノードが例外リストに含まれていれば
						break loop // ループを終了し、このノードより上位はポップしない
					}
				}
				continue // 例外でなければ、このノードはポップされる対象なので、次のノードへ
			}
		}
		break // 上記のタグ以外が見つかったらループを終了
	}

	p.oe = p.oe[:i+1] // ループが終了した位置までスタックを切り詰める(ポップする)
}

この関数は、p.oe(stack of open elements)を最上位から下位に向かって走査します。dd, dt, li, option, optgroup, p, rp, rtのいずれかのタグ名を持つ要素が見つかった場合、その要素をスタックからポップします。ただし、exceptions引数で指定されたタグ名はその限りではありません。このロジックにより、HTML5仕様で定められた自動的な要素の閉じられ方が正確に再現されます。

inBodyIM関数の変更点

inBodyIM関数は、HTMLの<body>要素内の解析ロジックを定義しています。このコミットでは、特に終了タグの処理と、特定のブロックレベル要素の開始タグ処理が改善されています。

  • <pre>および<listing>開始タグの処理:

    		case "pre", "listing":
    			p.popUntil(buttonScope, "p") // buttonScope内の"p"要素までポップ
    			p.addElement(p.tok.Data, p.tok.Attr) // 新しい要素を追加
    			// The newline, if any, will be dealt with by the TextToken case.
    			p.framesetOK = false
    

    以前はp.oe.pop()p.acknowledgeSelfClosingTag()が呼び出されていましたが、これらが削除されました。これは、<pre><listing>が自己終了要素ではないこと、およびその内部の改行文字の処理をテキストトークン処理に任せるというHTML5の挙動に合わせた変更です。

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

    		case "form":
    			node := p.form // パーサーが保持しているformノードを取得
    			p.form = nil // formノードをクリア
    			i := p.indexOfElementInScope(defaultScope, "form") // defaultScope内で"form"要素のインデックスを探す
    			if node == nil || i == -1 || p.oe[i] != node {
    				// Ignore the token.
    				return true
    			}
    			p.generateImpliedEndTags() // 含意的な終了タグを生成
    			p.oe.remove(node) // formノードをスタックから削除
    

    </form>終了タグが来た場合、パーサーが現在処理中の<form>要素(p.form)が存在し、かつそれがスコープ内に存在する場合にのみ、generateImpliedEndTagsを呼び出して含意的な終了タグを生成し、その後form要素をスタックから削除します。これにより、<form>要素の正しい閉じられ方が保証されます。

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

    			case "li":
    				// 以前: p.popUntil(listItemScope, "li")
    				p.oe = p.oe[:i] // 現在のli要素までスタックを切り詰める
    

    </li>終了タグの処理がp.popUntil(listItemScope, "li")からp.oe = p.oe[:i]に変更されました。これは、<li>要素が閉じられる際に、スタック上の<li>要素までをポップするのではなく、その要素自体をスタックから削除する、より直接的な操作に変わったことを示唆しています。これにより、<li>要素の閉じられ方がより正確にHTML5仕様に準拠するようになります。

  • </dd>, </dt>, </h1>から</h6>終了タグの処理:

    		case "dd", "dt":
    			p.popUntil(defaultScope, p.tok.Data) // 対応するタグまでポップ
    		case "h1", "h2", "h3", "h4", "h5", "h6":
    			p.popUntil(defaultScope, "h1", "h2", "h3", "h4", "h5", "h6") // いずれかの見出しタグまでポップ
    

    これらの終了タグが来た場合、popUntil関数が呼び出され、対応する開始タグ(または見出しタグのグループ)までスタックがポップされます。これにより、これらの要素が正しく閉じられ、DOMツリーの整合性が保たれます。

これらの変更は、HTML5の複雑な解析ルール、特にエラー回復と含意的な要素の閉じ方を正確に実装するために不可欠です。

関連リンク

参考にした情報源リンク