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

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

このコミットは、Go言語の実験的なHTMLパーサーライブラリであるexp/htmlパッケージ内のparse.goファイルに対する改善を目的としています。具体的には、HTML5のパースアルゴリズムにおける「in head insertion mode」(ヘッド内挿入モード)の処理ロジックを整理し、DOCTYPEトークンを適切に無視するケースを追加しています。

コミット

commit fca32f02e90b3ea2ddfb744fdd43608821f51220
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Fri Apr 20 09:08:58 2012 +1000

    exp/html: improve InHeadIM

    Clean up the flow of control, and add a case for doctype tokens (to
    ignore them).

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

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

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

元コミット内容

exp/html: improve InHeadIM

Clean up the flow of control, and add a case for doctype tokens (to
ignore them).

変更の背景

この変更は、Go言語のexp/htmlパッケージ、特にHTMLパーサーの堅牢性と正確性を向上させるために行われました。HTML5の仕様は非常に複雑であり、ブラウザがHTMLドキュメントをどのように解析し、DOMツリーを構築するかについて厳密なルールを定めています。この仕様には、特定のコンテキスト(挿入モード)において、特定のトークンがどのように処理されるべきかという詳細な規則が含まれています。

inHeadIM(in head insertion mode)は、パーサーが<head>要素の内部を処理している状態を指します。このモードでは、<title>, <style>, <script>などの特定の要素のみが許可され、それ以外の要素やテキストは異なる方法で処理されるか、無視されるべきです。

元の実装では、このinHeadIMの制御フローが複雑であったり、HTML5仕様で定義されているDOCTYPEトークンの処理が欠落していた可能性があります。DOCTYPE宣言はHTMLドキュメントの冒頭に記述されるものであり、通常は<head>要素の内部には出現しませんが、不正なHTMLが与えられた場合にはパーサーがこれを適切に処理(この場合は無視)する必要があります。

このコミットの目的は、inHeadIMにおけるトークン処理のロジックを簡素化し、より仕様に準拠させることで、パーサーの信頼性を高めることにありました。特に、DOCTYPEトークンを無視するケースを追加することで、不正な入力に対するパーサーの挙動を改善しています。

前提知識の解説

HTML5パースアルゴリズム

HTML5のパースアルゴリズムは、WebブラウザがHTMLドキュメントを解析し、DOM(Document Object Model)ツリーを構築するための詳細な手順を定めたものです。これはW3Cによって標準化されており、すべての主要なブラウザがこのアルゴリズムに従ってHTMLを解析することで、一貫したレンダリングを実現しています。

このアルゴリズムは、大きく分けて以下のステップで構成されます。

  1. トークン化(Tokenization): 入力されたHTML文字列を、意味のある単位(トークン)に分解します。トークンには、開始タグ、終了タグ、テキスト、コメント、DOCTYPE、EOF(ファイルの終わり)などがあります。
  2. ツリー構築(Tree Construction): トークナイザーから受け取ったトークンに基づいて、DOMツリーを構築します。このプロセスは「挿入モード(insertion mode)」と呼ばれる状態機械によって制御されます。

挿入モード(Insertion Mode)

挿入モードは、HTMLパーサーが現在ドキュメントのどの部分を解析しているかに応じて、トークンの処理方法を決定する状態です。HTML5仕様では、以下のような様々な挿入モードが定義されています。

  • Initial insertion mode: 初期状態。DOCTYPEトークンを処理します。
  • Before HTML insertion mode: <html>タグの直前。
  • Before head insertion mode: <head>タグの直前。
  • In head insertion mode (InHeadIM): <head>タグの内部。このモードでは、<title>, <style>, <script>, <meta>, <link>などの要素が許可されます。それ以外のタグ(例: <body>)やテキストは、このモードでは特殊な処理を受けます。
  • After head insertion mode: <head>タグの直後。
  • In body insertion mode: <body>タグの内部。
  • After body insertion mode: <body>タグの直後。
  • In frameset insertion mode: <frameset>タグの内部。
  • After frameset insertion mode: <frameset>タグの直後。
  • In table insertion mode: <table>タグの内部。
  • In caption insertion mode: <caption>タグの内部。
  • In column group insertion mode: <colgroup>タグの内部。
  • In table body insertion mode: <tbody>, <thead>, <tfoot>タグの内部。
  • In row insertion mode: <tr>タグの内部。
  • In cell insertion mode: <td>, <th>タグの内部。
  • In select insertion mode: <select>タグの内部。
  • In select in table insertion mode: <table>内の<select>タグの内部。
  • In template insertion mode: <template>タグの内部。
  • After after body insertion mode: </body>タグの直後。
  • After after frameset insertion mode: </frameset>タグの直後。
  • Text insertion mode: <script>, <style>, <textarea>, <title>などの要素の内部で、テキストとして扱われるべきコンテンツを処理するモード。

各挿入モードでは、受け取ったトークンの種類に応じて、DOMツリーへのノードの追加、既存ノードの変更、パーサーの状態遷移、エラー処理など、異なるアクションが定義されています。

DOCTYPEトークン

DOCTYPEトークンは、HTMLドキュメントの最初のトークンとして期待されるもので、ドキュメントがどのHTMLバージョン(またはXML)の仕様に準拠しているかをブラウザに伝えます。HTML5では、<!DOCTYPE html>という形式が推奨されています。パーサーは通常、このトークンを初期モードで処理し、ドキュメントモードを設定しますが、誤って<head>内部などに出現した場合でも、パーサーはこれを適切に処理(通常は無視)する必要があります。

技術的詳細

このコミットは、src/pkg/exp/html/parse.goファイル内のinHeadIM関数を変更しています。この関数は、HTML5パースアルゴリズムの「in head insertion mode」に対応するGo言語の実装です。

変更の主なポイントは以下の通りです。

  1. 制御フローの簡素化:

    • 元のコードでは、popimpliedという2つのブール変数を使用して、トークン処理後のパーサーの状態遷移を制御していました。
    • 新しいコードでは、これらの変数を削除し、各switchケース内で直接パーサーの状態遷移(p.im = afterHeadIM)や要素のポップ(p.oe.pop())を行うように変更されています。これにより、コードの可読性が向上し、ロジックがより直接的になりました。
    • 特に、EndTagTokenheadタグが来た場合の処理が明確化され、head要素がスタックからポップされた後にafterHeadIMに遷移するロジックが直接記述されています。
  2. DOCTYPEトークンの追加処理:

    • switch p.tok.Typecase DoctypeToken:が追加されました。
    • このケースでは、// Ignore the token.というコメントの通り、DOCTYPEトークンを無視し、return trueで現在のトークンの処理を完了します。これは、DOCTYPE<head>要素の内部に出現することがHTML5仕様では不正であるため、パーサーがこれをエラーとして扱わず、単にスキップすべきであることを意味します。
  3. エラー処理の改善(panicの明確化):

    • EndTagTokenheadタグが来た際に、要素スタック(p.oe)からポップされた要素がheadでなかった場合にpanicを発生させるロジックが、より明確な位置に移動しました。これは、パーサーが予期しない状態になったことを示すための重要なチェックです。
  4. 暗黙的なトークン処理の変更:

    • TextTokenStartTagTokenのデフォルトケース、EndTagTokenbody, html, brケースでimplied = trueとしていたロジックが削除され、代わりにp.parseImpliedToken(EndTagToken, "head", nil)を呼び出すように変更されています。これは、これらのトークンが<head>内で出現した場合に、暗黙的に</head>タグが閉じられたものとして扱い、afterHeadIMに遷移させるというHTML5仕様の挙動をより正確に反映しています。

これらの変更により、inHeadIM関数はHTML5のパースアルゴリズムにさらに厳密に準拠し、特に不正なHTML入力に対する挙動が改善されました。

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

src/pkg/exp/html/parse.goファイルのinHeadIM関数が変更されています。

--- a/src/pkg/exp/html/parse.go
+++ b/src/pkg/exp/html/parse.go
@@ -477,13 +477,7 @@ func beforeHeadIM(p *parser) bool {

 // Section 12.2.5.4.4.
 func inHeadIM(p *parser) bool {
-	var (
-		pop     bool
-		implied bool
-	)
 	switch p.tok.Type {
-	case ErrorToken:
-		implied = true
 	case TextToken:
 		s := strings.TrimLeft(p.tok.Data, whitespace)
 		if len(s) < len(p.tok.Data) {
@@ -494,7 +488,6 @@ func inHeadIM(p *parser) bool {
 			p.tok.Data = s
 		}
-		implied = true
 	case StartTagToken:
 		switch p.tok.Data {
 		case "html":
@@ -503,6 +496,7 @@ func inHeadIM(p *parser) bool {
 			p.addElement(p.tok.Data, p.tok.Attr)
 			p.oe.pop()
 			p.acknowledgeSelfClosingTag()
+			return true
 		case "script", "title", "noscript", "noframes", "style":
 			p.addElement(p.tok.Data, p.tok.Attr)
 			p.setOriginalIM()
@@ -511,15 +505,19 @@ func inHeadIM(p *parser) bool {
 		case "head":
 			// Ignore the token.
 			return true
-		default:
-			implied = true
 		}
 	case EndTagToken:
 		switch p.tok.Data {
 		case "head":
-			pop = true
+			n := p.oe.pop()
+			if n.Data != "head" {
+				panic("html: bad parser state: <head> element not found, in the in-head insertion mode")
+			}
+			p.im = afterHeadIM
+			return true
 		case "body", "html", "br":
-			implied = true
+			p.parseImpliedToken(EndTagToken, "head", nil)
+			return false
 		default:
 			// Ignore the token.
 			return true
@@ -530,16 +528,13 @@ func inHeadIM(p *parser) bool {
 			Data: p.tok.Data,
 		})
 		return true
+	case DoctypeToken:
+		// Ignore the token.
+		return true
 	}\n-	if pop || implied {\n-\t\tn := p.oe.pop()\n-\t\tif n.Data != \"head\" {\n-\t\t\tpanic(\"html: bad parser state: <head> element not found, in the in-head insertion mode\")\n-\t\t}\n-\t\tp.im = afterHeadIM\n-\t\treturn !implied\n-\t}\n-\treturn true
+\n+	p.parseImpliedToken(EndTagToken, "head", nil)
+	return false
 }\n
 // Section 12.2.5.4.6.

コアとなるコードの解説

変更されたinHeadIM関数は、HTMLパーサーが<head>要素の内部にいるときに、次のトークンをどのように処理するかを決定します。

  1. 変数の削除:

    -	var (
    -		pop     bool
    -		implied bool
    -	)
    

    popimpliedというブール変数が削除されました。これらは以前、トークン処理後に共通のロジックでパーサーの状態を遷移させるために使用されていましたが、新しいコードでは各ケースで直接状態遷移を行うことで、ロジックがより明確になりました。

  2. ErrorTokenの削除:

    -	case ErrorToken:
    -		implied = true
    

    ErrorTokenのケースが削除されました。これは、エラー処理のロジックが他の場所でより適切に扱われるようになったか、このモードでのErrorTokenの扱いが不要になったことを示唆しています。

  3. TextTokenの変更:

    -		implied = true
    

    TextTokenの処理からimplied = trueが削除されました。これは、テキストノードが<head>内に現れた場合の暗黙的な</head>の処理が、関数の最後にあるp.parseImpliedTokenに集約されたためです。

  4. StartTagTokenの変更:

    • htmlタグのケースにreturn trueが追加されました。これは、htmlタグが<head>内で出現した場合、そのタグを処理した後、現在の挿入モードを維持して次のトークンを待つことを意味します。
    • defaultケースからimplied = trueが削除されました。これもTextTokenと同様に、暗黙的な</head>の処理が関数の最後に移動したためです。
  5. EndTagTokenの変更:

    • headタグのケースが大幅に変更されました。
      -		case "head":
      -			pop = true
      +		case "head":
      +			n := p.oe.pop()
      +			if n.Data != "head" {
      +				panic("html: bad parser state: <head> element not found, in the in-head insertion mode")
      +			}
      +			p.im = afterHeadIM
      +			return true
      
      以前はpop = trueを設定していましたが、新しいコードでは直接要素スタックからhead要素をポップし、それが実際にhead要素であることを確認しています。もしそうでなければpanicを発生させ、その後パーサーの挿入モードをafterHeadIMに遷移させています。これにより、<head>要素が正しく閉じられた際のパーサーの挙動が明確かつ堅牢になりました。
    • body, html, brタグのケースが変更されました。
      -		case "body", "html", "br":
      -			implied = true
      +		case "body", "html", "br":
      +			p.parseImpliedToken(EndTagToken, "head", nil)
      +			return false
      
      これらのタグが<head>内で出現した場合、HTML5仕様では暗黙的に</head>タグが閉じられたものとして扱われます。この変更は、p.parseImpliedToken(EndTagToken, "head", nil)を呼び出すことで、この挙動を正確に実装しています。return falseは、現在のトークンを再処理する必要があることを示します。
  6. DoctypeTokenの追加:

    +	case DoctypeToken:
    +		// Ignore the token.
    +		return true
    

    DOCTYPEトークンが<head>内で出現した場合、これを無視するように明示的に追加されました。これはHTML5仕様に準拠した挙動です。

  7. 共通ロジックの削除と集約:

    -	}\n-	if pop || implied {\n-\t\tn := p.oe.pop()\n-\t\tif n.Data != \"head\" {\n-\t\t\tpanic(\"html: bad parser state: <head> element not found, in the in-head insertion mode\")\n-\t\t}\n-\t\tp.im = afterHeadIM\n-\t\treturn !implied\n-\t}\n-\treturn true
    +\n+	p.parseImpliedToken(EndTagToken, "head", nil)
    
  • return false
    以前は`pop`または`implied`が`true`の場合に実行されていた共通のロジックが削除されました。代わりに、関数の最後に`p.parseImpliedToken(EndTagToken, "head", nil)`と`return false`が追加されています。これは、上記の特定のケースで明示的に処理されなかったトークン(例: `<head>`内で許可されない開始タグやテキスト)が来た場合に、暗黙的に`</head>`が閉じられたものとして扱い、パーサーを`afterHeadIM`に遷移させるためのフォールバックロジックです。`return false`は、現在のトークンを新しいモードで再処理する必要があることを示します。
    
    

これらの変更により、inHeadIM関数はよりHTML5仕様に忠実になり、コードの構造も改善されました。

関連リンク

参考にした情報源リンク