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

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

このコミットは、Go言語の実験的なHTMLパーサー (exp/html) において、HTML5仕様に準拠するように特定の挿入モード(afterBodyIM, afterAfterBodyIM, afterAfterFramesetIM)でのテキスト、コメント、およびDOCTYPEトークンの処理を調整するものです。これにより、いくつかのテストがパスするようになります。

コミット

commit 33a89b5fdad1917e292b7a8aea5f164c1460177d
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Wed May 23 11:11:34 2012 +1000

    exp/html: adjust the last few insertion modes to match the spec
    
    Handle text, comment, and doctype tokens in afterBodyIM, afterAfterBodyIM,
    and afterAfterFramesetIM.
    
    Pass three more tests.
    
    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/6231043

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

https://github.com/golang/go/commit/33a89b5fdad1917e292b7a8aea5f164c1460177d

元コミット内容

exp/html: adjust the last few insertion modes to match the spec

Handle text, comment, and doctype tokens in afterBodyIM, afterAfterBodyIM,
and afterAfterFramesetIM.

Pass three more tests.

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

変更の背景

この変更の背景には、HTML5の複雑なパースアルゴリズムへの正確な準拠があります。HTMLドキュメントのパースは、単にタグを読み込むだけでなく、現在のコンテキスト(「挿入モード」)に基づいて、予期せぬトークンや不正なマークアップをどのように処理するかを厳密に定義しています。Go言語のexp/htmlパッケージは、HTML5仕様に準拠したパーサーを提供することを目指しており、このコミットは、特にドキュメントの終盤や特定の構造(framesetなど)の後に現れる可能性のあるテキスト、コメント、DOCTYPEトークンの処理が仕様と一致していない点を修正することを目的としています。

HTML5のパース仕様は非常に詳細であり、ブラウザ間の互換性を保証するために、すべてのエッジケースを正確に処理する必要があります。このコミットは、既存のテストが失敗していた特定のシナリオを修正し、パーサーの堅牢性と仕様への準拠を向上させるものです。

前提知識の解説

HTML5パースアルゴリズム

HTML5のパースアルゴリズムは、W3Cによって厳密に定義されており、ウェブブラウザがHTMLドキュメントをどのように解析し、DOMツリーを構築するかを規定しています。このアルゴリズムは、ステートマシンとして機能し、入力ストリームからトークンを読み込み、現在の「挿入モード」に基づいて次のアクションを決定します。

トークン化とツリー構築

HTMLパースは大きく分けて二つのフェーズがあります。

  1. トークン化 (Tokenization): 入力されたHTML文字列を、意味のある単位(トークン)に分解します。例えば、<p>は「開始タグトークン」、Helloは「テキストトークン」、<!-- comment -->は「コメントトークン」、<!DOCTYPE html>は「DOCTYPEトークン」となります。
  2. ツリー構築 (Tree Construction): トークナイザーから受け取ったトークンを基に、DOMツリーを構築します。このフェーズが「挿入モード」の概念と密接に関連しています。

挿入モード (Insertion Modes)

挿入モードは、HTML5パースアルゴリズムの中心的な概念です。これは、パーサーが現在ドキュメントのどの部分を処理しているかを示す状態であり、受け取ったトークンに対してどのようなアクションを取るべきかを決定します。HTML5仕様には、以下のような多数の挿入モードが定義されています。

  • initial
  • before html
  • before head
  • in head
  • in head noscript
  • after head
  • in body
  • text
  • in table
  • in table text
  • in caption
  • in column group
  • in table body
  • in row
  • in cell
  • in select
  • in select in table
  • in template
  • after body (本コミットで関連)
  • in frameset
  • after frameset
  • after after body (本コミットで関連)
  • after after frameset (本コミットで関連)

各挿入モードは、特定のトークンタイプ(開始タグ、終了タグ、テキスト、コメント、DOCTYPEなど)が与えられたときに、DOMツリーにノードを追加したり、モードを切り替えたり、エラーを処理したりする具体的なルールを持っています。

afterBodyIM, afterAfterBodyIM, afterAfterFramesetIM

これらの挿入モードは、ドキュメントの終盤や特定の構造の後にパーサーが到達した状態を表します。

  • afterBodyIM: <body>タグが閉じられた後、または暗黙的に閉じられた後にパーサーが到達するモードです。通常、このモードでは、HTMLドキュメントの残りの部分(例えば、</html>タグやコメント、空白文字など)を処理します。
  • afterAfterBodyIM: <body>タグと<html>タグの両方が閉じられた後にパーサーが到達するモードです。このモードでは、ドキュメントの末尾にある可能性のあるコメントや空白文字などを処理します。
  • afterAfterFramesetIM: frameset要素が閉じられた後、かつ<html>タグが閉じられた後にパーサーが到達するモードです。このモードも、ドキュメントの末尾に近い部分での特定の要素の処理に関連します。

これらのモードでは、通常は新しい要素が追加されることは稀ですが、仕様では特定のトークン(特にテキスト、コメント、DOCTYPE)がどのように扱われるべきかが厳密に定義されています。例えば、空白文字のみのテキストトークンは無視されるべきか、それともinBodyIMに切り替えて処理されるべきか、といった詳細なルールが存在します。

トークンの種類

  • TextToken: HTMLコンテンツ内の通常のテキストを表します。
  • CommentToken: <!-- ... -->形式のHTMLコメントを表します。
  • DoctypeToken: <!DOCTYPE ...>宣言を表します。

技術的詳細

このコミットは、Go言語のexp/htmlパッケージ内のHTMLパーサーのparse.goファイルに対して行われています。具体的には、afterBodyIMafterAfterBodyIMafterAfterFramesetIMという3つの挿入モード関数における、TextTokenCommentTokenDoctypeTokenの処理ロジックが修正されています。

afterBodyIM の変更点

以前のafterBodyIMでは、TextTokenが来た場合の処理が不足していました。このコミットでは、TextTokenが来た場合に、そのデータから先頭の空白文字をトリムし、残りの文字列の長さが0(つまり、すべて空白文字だった場合)であれば、パーサーの挿入モードをinBodyIMに切り替えるように修正されています。これは、HTML5仕様において、after bodyモードで空白文字のみのテキストトークンが来た場合、in bodyモードに切り替えて処理を継続するというルールに準拠するためです。

afterAfterBodyIM の変更点

afterAfterBodyIMも同様に、TextTokenの処理が不足していました。afterBodyIMと同様に、テキストトークンから空白文字をトリムし、残りが空であればinBodyIMに切り替えるロジックが追加されています。 さらに、DoctypeTokenが来た場合の処理も追加されました。DoctypeTokenが来た場合も、パーサーの挿入モードをinBodyIMに切り替えるように修正されています。これは、after after bodyモードでDOCTYPEトークンが来た場合の仕様に合わせたものです。

afterAfterFramesetIM の変更点

afterAfterFramesetIMでは、CommentTokenTextTokenの処理が修正されています。

  • CommentToken: 以前はp.addChildを使ってコメントノードを追加していましたが、p.doc.Addに変更されています。これは、コメントノードの追加方法をより適切にするための修正と考えられます。
  • TextToken: 以前はテキストを処理した後、p.reconstructActiveFormattingElements()p.addText(s)を呼び出していましたが、これらが削除され、代わりにトークンのデータ(空白文字をトリムした後)をp.tok.Dataに再設定し、パーサーの挿入モードをinBodyIMに切り替えるように変更されています。これは、after after framesetモードでテキストトークンが来た場合のHTML5仕様の挙動に合わせたものです。
  • DoctypeToken: afterAfterFramesetIMでもDoctypeTokenが来た場合にinBodyIMに切り替えるロジックが追加されました。

これらの変更は、HTML5パースアルゴリズムの厳密なルールに従い、特定の挿入モードで予期されるトークンが来た場合に、パーサーが正しい状態遷移を行い、DOMツリーを正確に構築できるようにするためのものです。特に、空白文字の扱い、コメントの追加、DOCTYPEの再処理といった細かな挙動が仕様に合致するように調整されています。

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

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

--- a/src/pkg/exp/html/parse.go
+++ b/src/pkg/exp/html/parse.go
@@ -1597,6 +1597,12 @@ func afterBodyIM(p *parser) bool {
 	case ErrorToken:
 		// Stop parsing.
 		return true
+	case TextToken:
+		s := strings.TrimLeft(p.tok.Data, whitespace)
+		if len(s) == 0 {
+			// It was all whitespace.
+			return inBodyIM(p)
+		}
 	case StartTagToken:
 		if p.tok.Data == "html" {
 			return inBodyIM(p)
@@ -1717,7 +1723,11 @@ func afterAfterBodyIM(p *parser) bool {
 		// Stop parsing.
 		return true
 	case TextToken:
-		// TODO.
+		s := strings.TrimLeft(p.tok.Data, whitespace)
+		if len(s) == 0 {
+			// It was all whitespace.
+			return inBodyIM(p)
+		}
 	case StartTagToken:
 		if p.tok.Data == "html" {
 			return inBodyIM(p)
@@ -1728,6 +1738,8 @@ func afterAfterBodyIM(p *parser) bool {
 			Data: p.tok.Data,
 		})
 		return true
+	case DoctypeToken:
+		return inBodyIM(p)
 	}\n
 	p.im = inBodyIM
 	return false
@@ -1737,7 +1749,7 @@ func afterAfterFramesetIM(p *parser) bool {
 	switch p.tok.Type {
 	case CommentToken:
-		p.addChild(&Node{
+		p.doc.Add(&Node{
 			Type: CommentNode,
 			Data: p.tok.Data,
 		})
@@ -1751,8 +1763,8 @@ func afterAfterFramesetIM(p *parser) bool {
 		}, p.tok.Data)
 		if s != "" {
-			p.reconstructActiveFormattingElements()
-			p.addText(s)
+			p.tok.Data = s
+			return inBodyIM(p)
 		}
 	case StartTagToken:
 		switch p.tok.Data {
@@ -1761,6 +1773,8 @@ func afterAfterFramesetIM(p *parser) bool {
 		case "noframes":
 			return inHeadIM(p)
 		}
+	case DoctypeToken:
+		return inBodyIM(p)
 	default:
 		// Ignore the token.
 	}

また、以下のテストログファイルが更新され、以前失敗していたテストがパスするようになったことが示されています。

  • src/pkg/exp/html/testlogs/tests18.dat.log
  • src/pkg/exp/html/testlogs/tests19.dat.log
  • src/pkg/exp/html/testlogs/webkit01.dat.log

コアとなるコードの解説

afterBodyIM および afterAfterBodyIMTextToken 処理

	case TextToken:
		s := strings.TrimLeft(p.tok.Data, whitespace)
		if len(s) == 0 {
			// It was all whitespace.
			return inBodyIM(p)
		}

このコードブロックは、afterBodyIMafterAfterBodyIMの両方に追加されています。

  1. s := strings.TrimLeft(p.tok.Data, whitespace): 現在のトークンデータ(テキスト)の先頭から空白文字(whitespace定数で定義されている文字)を削除します。
  2. if len(s) == 0: トリムした結果、文字列sの長さが0であれば、それは元のテキストトークンがすべて空白文字で構成されていたことを意味します。
  3. return inBodyIM(p): この場合、パーサーの挿入モードをinBodyIM(in body insertion mode)に切り替えます。HTML5仕様では、after bodyafter after bodyモードで空白文字のみのテキストトークンが来た場合、in bodyモードに切り替えて処理を継続するよう規定されています。これにより、ドキュメントの末尾にある余分な空白文字が適切に処理されるようになります。

afterAfterBodyIM および afterAfterFramesetIMDoctypeToken 処理

	case DoctypeToken:
		return inBodyIM(p)

このコードブロックは、afterAfterBodyIMafterAfterFramesetIMに追加されています。 DoctypeTokenがこれらのモードで現れた場合、パーサーはinBodyIMに切り替えます。これは、HTML5仕様において、これらのモードでDOCTYPEトークンが来た場合の挙動を定義しており、通常はエラーとして扱われるか、特定の条件下でin bodyモードに遷移することがあります。この修正は、その仕様に合わせたものです。

afterAfterFramesetIMCommentToken 処理

	case CommentToken:
		p.doc.Add(&Node{
			Type: CommentNode,
			Data: p.tok.Data,
		})

以前はp.addChildが使われていましたが、p.doc.Addに変更されました。p.docはドキュメントのルートノードを表す可能性が高く、Addメソッドはドキュメントツリーに直接ノードを追加するためのより適切な方法であると考えられます。これにより、コメントノードがDOMツリーに正しく追加されることが保証されます。

afterAfterFramesetIMTextToken 処理の変更

	case TextToken:
		s := strings.TrimLeftFunc(p.tok.Data, func(r rune) bool {
			return unicode.IsSpace(r) || r == '\t' || r == '\n' || r == '\r' || r == '\f'
		}, p.tok.Data) // この行は元のdiffと異なりますが、意図を推測して記述
		if s != "" {
			p.tok.Data = s
			return inBodyIM(p)
		}

元のコードではp.reconstructActiveFormattingElements()p.addText(s)が呼び出されていましたが、これらが削除され、代わりにトークンのデータが更新され、inBodyIMに切り替わるようになりました。 これは、after after framesetモードでテキストトークンが来た場合のHTML5仕様の挙動に合わせたものです。このモードでは、テキストが空白文字でない場合、そのテキストをin bodyモードで処理するように再キューイングされるか、または直接in bodyモードに遷移して処理されることが期待されます。この変更は、その挙動を模倣しています。

これらの変更により、GoのHTMLパーサーは、HTML5仕様の複雑なエッジケース、特にドキュメントの終盤における空白文字、コメント、DOCTYPE宣言の処理において、より正確に動作するようになります。これにより、パーサーの堅牢性が向上し、より広範なHTMLドキュメントを正しく解析できるようになります。

関連リンク

参考にした情報源リンク