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

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

このコミットは、Go言語のhtmlパッケージにおけるHTMLパーサーの挙動を修正するものです。具体的には、HTMLのパース処理において「Adoption Agency Algorithm(養子縁組アルゴリズム)」が適用される条件を厳密化し、スコープ内にない要素に対しては同アルゴリズムを実行しないように変更しています。これにより、特定の不正なHTMLマークアップが、HTML5のパース仕様に沿って正しく処理されるようになります。

コミット

commit 03f163c7f22bfaab69a56d48160b0a184ce6bf54
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Fri Oct 28 16:04:58 2011 +1100

    html: don't run "adoption agency" on elements that aren't in scope.
    
    Pass tests1.dat, test 55:
    <!DOCTYPE html><font><table></font></table></font>
    
    | <!DOCTYPE html>
    | <html>
    |   <head>
    |   <body>
    |     <font>
    |       <table>
    
    Also pass tests through test 69:
    <DIV> abc <B> def <I> ghi <P> jkl
    
    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/5309074

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

https://github.com/golang/go/commit/03f163c7f22bfaab69a56d48160b0a184ce6bf54

元コミット内容

このコミットは、Go言語のhtmlパッケージのパーサーにおいて、HTMLの「Adoption Agency Algorithm(養子縁組アルゴリズム)」が、スコープ内に存在しない要素に対して誤って実行される問題を修正します。

具体的には、tests1.datのテスト55(<!DOCTYPE html><font><table></font></table></font>)と、テスト69までの他のテストケース(例: <DIV> abc <B> def <I> ghi <P> jkl)が正しくパースされるように改善されています。

テスト55の例では、<table>要素が</font>タグによって閉じられるべきではない状況で、パーサーが誤ったDOMツリーを構築してしまう問題がありました。この修正により、HTML5のパース仕様に則り、<table></font>の外側に適切に配置されるようになります。

変更の背景

HTMLのパースは非常に複雑であり、特に不正なマークアップ(タグの閉じ忘れ、誤ったネストなど)をどのように処理するかは、Webブラウザ間の互換性を保つ上で非常に重要です。HTML5の仕様では、これらの不正なマークアップに対する厳密なエラー処理ルールが定義されており、その一つが「Adoption Agency Algorithm(養子縁組アルゴリズム)」です。

このコミット以前のhtmlパッケージのパーサーは、このアルゴリズムを適用する際に、要素が現在のスコープ内に存在するかどうかのチェックが不十分でした。その結果、スコープ外の要素に対してもアルゴリズムが実行され、HTML5の仕様とは異なるDOMツリーが構築されてしまう問題が発生していました。

コミットメッセージに記載されているテストケースは、この問題を示す具体的な例です。

  • <!DOCTYPE html><font><table></font></table></font>: このケースでは、<table>要素が</font>タグによって閉じられていますが、HTMLの構造上、<table></font>の子要素として適切ではありません。正しいパース結果は、<table><body>の直下、またはfont要素の兄弟要素として配置されるべきです。しかし、誤ったアルゴリズムの適用により、<table>font要素の子として「養子縁組」されてしまう可能性がありました。
  • <DIV> abc <B> def <I> ghi <P> jkl: このような連続したインライン要素とブロック要素の混在も、HTMLのパースにおいて複雑な挙動を引き起こす可能性があります。

このコミットは、これらのテストケースがHTML5の仕様に沿って正しくパースされるように、Adoption Agency Algorithmの適用条件を厳密化することで、パーサーの堅牢性と標準準拠性を向上させることを目的としています。

前提知識の解説

HTMLパースとDOMツリー構築

Webブラウザは、HTMLドキュメントを読み込むと、それを解析(パース)して「Document Object Model (DOM)」と呼ばれるツリー構造を構築します。このDOMツリーは、JavaScriptなどからHTML要素にアクセスしたり、スタイルを適用したりするための基盤となります。

HTMLのパースは、大きく分けて以下の2つのフェーズで構成されます。

  1. トークン化 (Tokenization): HTMLの文字列を、タグ、属性、テキストなどの意味のある「トークン」に分解します。
  2. ツリー構築 (Tree Construction): トークンを基に、DOMツリーを構築します。このフェーズで、要素のネスト関係や、不正なマークアップに対するエラー処理が行われます。

Adoption Agency Algorithm(養子縁組アルゴリズム)

「Adoption Agency Algorithm」は、HTML5のツリー構築アルゴリズムの一部であり、特に不正にネストされた要素を処理するために使用されます。HTMLは非常に寛容な言語であり、開発者がタグを誤って閉じたり、不適切な場所に配置したりしても、ブラウザは可能な限りDOMツリーを構築しようとします。このアルゴリズムは、その「修正」プロセスの中核をなすものです。

このアルゴリズムの目的は、誤って閉じられた要素や、親要素のスコープ外に存在する要素を、DOMツリー内の適切な位置に「養子縁組」させることです。例えば、<em><p>X</em>Y</p>のようなマークアップがあった場合、<em>タグが</p>タグの前に閉じられていますが、HTMLのセマンティクス上、<em>pの子要素として適切ではありません。Adoption Agency Algorithmは、このような状況で<em>要素をDOMツリー内のより適切な親(例えば、pの親)に移動させることで、DOMツリーの整合性を保とうとします。

このアルゴリズムは、内部的に「スタック上のオープン要素 (stack of open elements)」や「アクティブなフォーマット要素のリスト (list of active formatting elements)」といったデータ構造を操作し、要素の正しい親を決定します。

Element Scope(要素のスコープ)

HTMLパースにおける「要素のスコープ」とは、特定の要素がDOMツリー内で有効な子要素として配置され得る範囲を指します。HTMLの仕様では、各要素がどの要素の子として配置できるか、また、どの要素が特定の要素を暗黙的に閉じるかなど、厳密なルールが定められています。

例えば、<table>要素の内部には、<tr><thead><tbody><tfoot>などの要素しか直接配置できません。もし<table>の内部に<div>のような要素が直接記述された場合、ブラウザはこれを不正なマークアップと判断し、<div><table>の外に「フォスターペアレンティング (foster parenting)」と呼ばれるメカニズムで移動させたり、無視したりすることがあります。

elementInScopeという概念は、特定の要素が現在のパースコンテキストにおいて、特定のスコープ(例えば、デフォルトのスコープ停止タグのセット)内に存在するかどうかを判断するために使用されます。これは、Adoption Agency Algorithmのような複雑なエラー処理アルゴリズムを適用する前に、要素の有効性を確認するために重要です。スコープ内にない要素に対しては、特定のアルゴリズムを適用しないことで、より正確なDOMツリー構築が可能になります。

技術的詳細

このコミットは、src/pkg/html/parse.go内のinBodyEndTagFormatting関数に修正を加えています。この関数は、HTMLのツリー構築アルゴリズムにおいて、in body挿入モードでフォーマット要素の終了タグを処理する際に呼び出されます。この処理の一部として、Adoption Agency Algorithmが実行される可能性があります。

修正前は、この関数内でAdoption Agency Algorithmを適用する際に、対象となる要素が現在のパーススコープ内に存在するかどうかのチェックが不十分でした。HTML5の仕様では、Adoption Agency Algorithmは、特定の条件(例えば、要素が特定のスコープ内に存在すること)を満たす場合にのみ実行されるべきです。

コミットによって追加された行は、まさにこのチェックを導入しています。

		if !p.elementInScope(defaultScopeStopTags, tag) {
			// Ignore the tag.
			return
		}

このコードは、p.elementInScope(defaultScopeStopTags, tag)という関数呼び出しによって、現在処理しているtagdefaultScopeStopTagsで定義されたスコープ停止タグのセット内でスコープ内に存在するかどうかを確認しています。

  • p: パーサーのインスタンス。
  • elementInScope: パーサーのメソッドで、指定されたタグが現在のスコープ内に存在するかどうかをチェックします。
  • defaultScopeStopTags: HTML5の仕様で定義されている、特定のスコープを停止させる要素のセット。例えば、<html>, <body>, <table>などが含まれることがあります。
  • tag: 現在処理している終了タグの要素名。

もしelementInScopefalseを返した場合、つまり、対象の要素が現在のスコープ内に存在しない場合、// Ignore the tag.というコメントの通り、そのタグは無視され、Adoption Agency Algorithmは実行されずにreturnします。

これにより、<!DOCTYPE html><font><table></font></table></font>のような不正なマークアップにおいて、<table></font>によって閉じられようとした際に、<table>fontのスコープ内にないため、Adoption Agency Algorithmが適用されず、結果としてHTML5の仕様に沿った正しいDOMツリーが構築されるようになります。

src/pkg/html/parse_test.goの変更は、この修正が正しく機能することを確認するためのテスト範囲の拡大です。

--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -132,7 +132,7 @@ func TestParser(t *testing.T) {
 		rc := make(chan io.Reader)
 		go readDat(filename, rc)
 		// TODO(nigeltao): Process all test cases, not just a subset.
-		for i := 0; i < 55; i++ {
+		for i := 0; i < 70; i++ {
 			// Parse the #data section.
 			b, err := ioutil.ReadAll(<-rc)
 			if err != nil {

この変更により、TestParser関数がtests1.datのテストケースを55番目までではなく、70番目まで実行するようになります。これにより、修正によって解決されたテスト55や、その他の関連するテストケースが網羅的に検証されることになります。

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

diff --git a/src/pkg/html/parse.go b/src/pkg/html/parse.go
index fdd6f75aab..b0348790c1 100644
--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -675,6 +675,10 @@ func (p *parser) inBodyEndTagFormatting(tag string) {\n 		\tp.afe.remove(formattingElement)\n 		\treturn\n 		}\n+\t\tif !p.elementInScope(defaultScopeStopTags, tag) {\n+\t\t\t// Ignore the tag.\n+\t\t\treturn\n+\t\t}\n \n \t\t// Steps 5-6. Find the furthest block.\n \t\tvar furthestBlock *Node
diff --git a/src/pkg/html/parse_test.go b/src/pkg/html/parse_test.go
index ae4ecd6658..e86a36f18a 100644
--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -132,7 +132,7 @@ func TestParser(t *testing.T) {\n 		rc := make(chan io.Reader)\n 		go readDat(filename, rc)\n 		// TODO(nigeltao): Process all test cases, not just a subset.\n-\t\tfor i := 0; i < 55; i++ {\n+\t\tfor i := 0; i < 70; i++ {\n \t\t\t// Parse the #data section.\n \t\t\tb, err := ioutil.ReadAll(<-rc)\n \t\t\tif err != nil {\n```

## コアとなるコードの解説

### `src/pkg/html/parse.go` の変更

追加された4行のコードが、このコミットの核心です。

```go
		if !p.elementInScope(defaultScopeStopTags, tag) {
			// Ignore the tag.
			return
		}

このコードブロックは、inBodyEndTagFormatting関数内で、Adoption Agency Algorithmの実行前に挿入されています。

  • p.elementInScope(defaultScopeStopTags, tag): このメソッド呼び出しは、現在のパーサーの状態において、tagで指定された要素がdefaultScopeStopTagsで定義されたスコープ停止要素のセット内で「スコープ内」に存在するかどうかを判定します。
    • defaultScopeStopTagsは、HTML5のパース仕様において、特定の要素のスコープを停止させるために使用される要素の集合です。例えば、<html>, <body>, <table>などがこれに該当し、これらの要素が出現すると、それ以前の特定の要素のスコープが終了すると見なされます。
  • !: 論理否定演算子です。つまり、p.elementInScope(...)false(スコープ内にない)を返した場合に、if文の条件がtrueとなります。
  • // Ignore the tag.: コメントが示す通り、もし要素がスコープ内にない場合、そのタグは無視されます。
  • return: 関数から即座に抜け出し、それ以降のAdoption Agency Algorithmの処理は行われません。

この変更により、パーサーは、HTML5の仕様に厳密に従い、スコープ内に存在しない要素に対してはAdoption Agency Algorithmを適用しないようになりました。これにより、不正なHTMLマークアップがより正確に、かつ予測可能な形でDOMツリーに変換されるようになります。

src/pkg/html/parse_test.go の変更

テストファイルの変更は、単にテストの実行範囲を広げるものです。

-		for i := 0; i < 55; i++ {
+		for i := 0; i < 70; i++ {

これは、TestParser関数がtests1.datというテストデータファイルから読み込むテストケースの数を、55番目までから70番目までに増やしています。これにより、このコミットで修正された問題(特にテスト55)や、その他の関連するテストケースが、自動テストスイートによって確実に検証されるようになります。

関連リンク

参考にした情報源リンク