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

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

このコミットは、Go言語の標準ライブラリである html パッケージにおけるHTMLパーサーのバグ修正に関するものです。具体的には、<marquee> 要素の閉じ方を適切に処理するようにパーサーを改善し、関連するテストケースを通過できるようにしています。

コミット

  • コミットハッシュ: cf6a71216211d8d3f487ab158cbf681742e790d4
  • 作者: Andrew Balholm (andybalholm@gmail.com)
  • 日付: 2011年11月3日 木曜日 10:11:06 +1100
  • 変更ファイル:
    • src/pkg/html/parse.go: 4行追加
    • src/pkg/html/parse_test.go: 1行追加、1行削除

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

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

元コミット内容

html: properly close <marquee> elements.

Pass tests1.dat, test 80:
<a href=a>aa<marquee>aa<a href=b>bb</marquee>aa

| <html>
|   <head>
|   <body>
|     <a>
|       href="a"
|       "aa"
|       <marquee>
|         "aa"
|         <a>
|           href="b"
|           "bb"
|       "aa"

Also pass tests through test 82:
<!DOCTYPE html><spacer>foo

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

変更の背景

このコミットの主な目的は、Go言語の html パッケージがHTMLドキュメントをパースする際に、<marquee> 要素の閉じ方を正しく処理できるようにすることです。

元のパーサーでは、特定の状況下で<marquee>要素が適切に閉じられず、DOMツリーの構造が期待通りにならない問題がありました。コミットメッセージに示されているテストケース tests1.dat, test 80 は、この問題を具体的に示しています。

<a href=a>aa<marquee>aa<a href=b>bb</marquee>aa

このHTMLスニペットでは、<marquee>要素の内部に別の<a>要素がネストされています。HTMLの仕様では、<marquee>のような特定の要素が閉じられる際に、その内部のアクティブなフォーマット要素(例えば、<a>タグなど)をクリアする必要がある場合があります。この処理が欠けていたため、パーサーが生成するDOMツリーが、上記のコミットメッセージに示されているような、<a>要素が<marquee>要素の内部に不適切にネストされた状態になっていました。

この修正により、パーサーはHTML5の仕様に準拠し、より堅牢なHTMLパースを実現します。また、tests1.dat のテスト82までが通過するようになったことも示されており、これはパーサーの全体的な安定性と正確性が向上したことを意味します。

前提知識の解説

このコミットを理解するためには、以下のHTMLパースに関する基本的な概念を理解しておく必要があります。

  1. HTMLパーシングアルゴリズム: HTMLのパースは、非常に複雑なプロセスです。ブラウザは、HTML5の仕様で定義されている詳細なアルゴリズムに従ってHTMLを解析し、DOM(Document Object Model)ツリーを構築します。このアルゴリズムは、エラー耐性があり、不正なHTMLでも可能な限りDOMツリーを構築できるように設計されています。

  2. 挿入モード (Insertion Mode): HTMLパーシングアルゴリズムの重要な概念の一つに「挿入モード」があります。パーサーは、現在のトークンと現在の挿入モードに基づいて、DOMツリーにノードを挿入する方法を決定します。例えば、inBodyIM は "in body" 挿入モードを指し、<body> タグの内部で要素を処理する際のルールを定義します。

  3. アクティブなフォーマット要素 (Active Formatting Elements): HTMLパーシングアルゴリズムには、「アクティブなフォーマット要素のリスト」という概念があります。これは、<a>, <b>, <i>, <font> などの特定のフォーマット要素が開始されたときに、それらを追跡するために使用されるリストです。これらの要素は、DOMツリーの構造に影響を与えるだけでなく、テキストのフォーマットにも影響を与えます。特定の要素(例えば、<marquee>, <applet>, <object>)が閉じられる際に、このリストをクリアする必要がある場合があります。これは、これらの要素が特殊なコンテンツモデルを持つため、その内部で開始されたフォーマット要素が、その要素の終了タグによって「閉じられる」べきではない、あるいはその要素のスコープ外に影響を及ぼすべきではない、というルールがあるためです。

  4. popUntil 関数: Goの html パッケージのパーサーにおける popUntil 関数は、DOMツリーのスタックから要素をポップ(削除)していく操作を行います。これは、特定のタグが見つかるまで、または特定の条件が満たされるまで、要素をスタックから取り除くために使用されます。このコミットでは、defaultScopeStopTags という一連のタグ(例えば、<html>, <body> など、特定のスコープの境界を示すタグ)と、現在のトークンデータ(閉じようとしているタグの名前)を引数としています。

  5. clearActiveFormattingElements 関数: この関数は、前述の「アクティブなフォーマット要素のリスト」をクリアする役割を担います。特定の要素(このコミットでは <marquee>, <applet>, <object>)が閉じられる際にこの関数が呼び出されることで、その要素の内部で開始されたフォーマット要素が、その要素のスコープ外に影響を与えないように、リストから削除されます。

  6. <marquee>, <applet>, <object> 要素:

    • <marquee>: テキストや画像をスクロール表示させるためのHTML要素。HTML5では非推奨(廃止)されていますが、古いHTMLドキュメントではまだ見られます。
    • <applet>: Javaアプレットを埋め込むためのHTML要素。HTML5では廃止されています。
    • <object>: 汎用的な埋め込みオブジェクト(Flash、PDF、ActiveXなど)を埋め込むためのHTML要素。

これらの要素は、その特殊なコンテンツモデルと、ブラウザがそれらをどのようにレンダリングするかという点で、HTMLパーシングにおいて特別な扱いを受けることがあります。

技術的詳細

このコミットの技術的な核心は、HTML5のパースアルゴリズムにおける「インサートモード」と「アクティブなフォーマット要素のリスト」の扱いにあります。

HTML5の仕様では、特定の要素(特に、レガシーな埋め込みコンテンツやスクリプト関連の要素)が閉じられる際に、アクティブなフォーマット要素のリストをクリアするよう指示されています。これは、これらの要素が独自の「コンテキスト」を持ち、その内部で開始されたフォーマット要素が、その要素の終了タグによって「閉じられる」べきではない、あるいはその要素のスコープ外に影響を及ぼすべきではない、というセマンティクスを反映しています。

具体的には、src/pkg/html/parse.goinBodyIM 関数は、<body> 要素の内部でトークンを処理する際のロジックを定義しています。この関数は、終了タグが検出されたときに、そのタグの種類に応じて異なる処理を行います。

修正前は、<marquee>, <applet>, <object> の終了タグが検出された場合、default ケースにフォールバックし、p.inBodyEndTagOther(p.tok.Data) が呼び出されていました。この inBodyEndTagOther 関数は、一般的な終了タグの処理を行いますが、アクティブなフォーマット要素のリストをクリアする特定のロジックは含まれていませんでした。

修正後は、これらの要素に対して専用の case が追加されました。この新しい case では、まず p.popUntil(defaultScopeStopTags, p.tok.Data) を呼び出して、現在の要素をDOMツリーのスタックからポップします。そして、この popUntil が成功した場合(つまり、対応する開始タグがスタックから見つかり、ポップされた場合)、p.clearActiveFormattingElements() が呼び出されます。

この clearActiveFormattingElements() の呼び出しが重要です。これにより、<marquee> などの要素の内部で開始された <a> などのフォーマット要素が、その <marquee> 要素の終了タグによって「閉じられた」と見なされ、アクティブなフォーマット要素のリストから削除されます。これにより、パーサーはHTML5の仕様に準拠し、テストケース80のような複雑なネスト構造でも正しいDOMツリーを構築できるようになります。

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

このコミットによる主要なコード変更は、以下の2つのファイルにあります。

  1. src/pkg/html/parse.go: HTMLパーサーの主要なロジックが含まれるファイル。

    --- a/src/pkg/html/parse.go
    +++ b/src/pkg/html/parse.go
    @@ -635,6 +635,10 @@ func inBodyIM(p *parser) (insertionMode, bool) {
     		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(defaultScopeStopTags, p.tok.Data)
    +	case "applet", "marquee", "object":
    +		if p.popUntil(defaultScopeStopTags, p.tok.Data) {
    +			p.clearActiveFormattingElements()
    +		}
     	default:
     		p.inBodyEndTagOther(p.tok.Data)
     	}
    
  2. src/pkg/html/parse_test.go: HTMLパーサーのテストが含まれるファイル。

    --- a/src/pkg/html/parse_test.go
    +++ b/src/pkg/html/parse_test.go
    @@ -133,7 +133,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 < 80; i++ {
    +	for i := 0; i < 83; i++ {
     		// Parse the #data section.
     		b, err := ioutil.ReadAll(<-rc)
     		if err != nil {
    

コアとなるコードの解説

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

この変更は、inBodyIM 関数内の switch ステートメントに新しい case を追加しています。inBodyIM 関数は、HTMLパーサーが <body> 要素の内部にいるときに、受信したトークン(この場合は終了タグ)をどのように処理するかを決定する役割を担っています。

  • 追加された case "applet", "marquee", "object":: この新しいケースは、パーサーが </applet>, </marquee>, または </object> の終了タグを検出したときに実行されます。
    • if p.popUntil(defaultScopeStopTags, p.tok.Data): まず、popUntil 関数が呼び出されます。この関数は、現在のトークンデータ(つまり、"applet", "marquee", "object" のいずれか)に対応する開始タグがDOMツリーのスタックから見つかるまで、要素をスタックからポップします。defaultScopeStopTags は、ポップを停止するべき特定のタグ(例えば、<html>, <body> など)のセットを定義しています。この popUntil が成功した場合(つまり、対応する開始タグが見つかり、ポップされた場合)、true を返します。
    • p.clearActiveFormattingElements(): popUntiltrue を返した場合、つまり対応する要素が正常に閉じられた場合にのみ、clearActiveFormattingElements() 関数が呼び出されます。この関数は、パーサーが追跡している「アクティブなフォーマット要素のリスト」をクリアします。これにより、<marquee> などの要素の内部で開始された <a> などのフォーマット要素が、その要素の終了タグによって「閉じられた」と見なされ、リストから削除されます。これは、HTML5のパースアルゴリズムにおける特定のルールに準拠するための重要なステップです。

この変更により、<marquee>, <applet>, <object> のような特殊な要素が閉じられる際に、その内部のアクティブなフォーマット要素が適切に処理され、DOMツリーの構造がHTML5の仕様に沿ったものになります。

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

このファイルでは、TestParser 関数内のループ条件が変更されています。

  • - for i := 0; i < 80; i++ {
  • + for i := 0; i < 83; i++ {

これは、tests1.dat ファイルに含まれるテストケースのうち、以前は80番目までしか実行していなかったものを、83番目まで実行するように変更したことを意味します。この変更は、新しい修正がテスト80(<marquee> の問題)だけでなく、テスト82までの他の関連するテストケースも正しく通過することを確認するために行われました。これにより、パーサーの修正が広範囲にわたる影響を持ち、全体的な堅牢性が向上したことが示唆されます。

関連リンク

参考にした情報源リンク