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

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

このコミットは、Go言語の標準ライブラリである html パッケージ内のHTMLパーサーの挙動を修正するものです。具体的には、<button> 要素のパース処理におけるバグを修正し、HTML5のパース仕様に準拠させることで、連続する <button> タグが正しく処理されるように改善しています。

コミット

commit e25a83d03e97edf0d8474ad41ed2edd0a63b19fc
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Wed Dec 14 21:40:31 2011 +1100

    html: close <button> element before opening a new one
    
    Pass tests6.dat, test 13:
    <button><button>
    
    | <html>
    |   <head>
    |   <body>
    |     <button>
    |     <button>
    
    Also pass tests through test 25:
    <table><colgroup>foo
    
    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/5487072

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

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

元コミット内容

このコミットは、HTMLパーサーが連続する <button> 要素を処理する際に発生していた問題を解決します。具体的には、<button><button> のようなマークアップが与えられた場合、最初の <button> が適切に閉じられることなく、2番目の <button> が開かれてしまうという挙動を修正しています。これにより、WebKitのテストスイートである tests6.dat のテスト13がパスするようになります。また、この修正により、tests6.dat のテスト25までがパスするようになり、<colgroup> 要素のパースに関する別の問題も間接的に解決されたことを示唆しています。

変更の背景

HTMLのパースは、ブラウザがウェブページを正しく表示するために非常に重要なプロセスです。HTMLは非常に寛容な言語であり、開発者が記述したマークアップが必ずしも厳密なXML形式に従っているわけではありません。そのため、HTMLパーサーは、不正なマークアップや省略されたタグに対しても、仕様に基づいて適切なDOMツリーを構築する必要があります。

このコミットの背景には、Go言語の html パッケージがHTML5のパース仕様に準拠しようとする努力があります。HTML5のパース仕様は非常に複雑で、様々な要素の組み合わせやエラー処理のルールが詳細に定義されています。特に、特定の要素(例えば <button>)が別の要素の開始タグとして現れた場合、既存の要素を暗黙的に閉じる必要があるというルールが存在します。

元のパーサーは、<button> 要素が連続して出現した場合に、この暗黙的な閉じ処理を正しく行っていなかったと考えられます。これにより、生成されるDOMツリーがブラウザの期待する構造と異なり、ウェブページのレンダリングに問題を引き起こす可能性がありました。この修正は、このような仕様の不一致を解消し、より堅牢で互換性の高いHTMLパーサーを提供することを目的としています。

前提知識の解説

このコミットを理解するためには、以下のHTMLパースに関する前提知識が必要です。

  1. HTML5パースアルゴリズム: HTML5仕様は、ブラウザがHTMLドキュメントをどのようにパースし、DOMツリーを構築するかを詳細に定義しています。これはステートマシンとして記述されており、「トークナイゼーション」と「ツリー構築」の2つの主要なフェーズに分かれます。

    • トークナイゼーション: 入力ストリームをトークン(開始タグ、終了タグ、テキスト、コメントなど)に変換します。
    • ツリー構築: トークンを受け取り、DOMツリーを構築します。このフェーズは「挿入モード (Insertion Mode)」と呼ばれる状態に基づいて動作します。
  2. 挿入モード (Insertion Mode): ツリー構築フェーズの現在の状態を定義します。各挿入モードは、特定のトークンが受信されたときにパーサーがどのように動作するかを決定します。例えば、「in body」モードは <body> 要素の内容をパースする際のルールを定義します。

  3. 開いている要素のスタック (Stack of Open Elements): パーサーが現在開いているHTML要素を追跡するために使用するスタック構造です。新しい要素が開始されるとスタックにプッシュされ、要素が閉じられるとポップされます。このスタックは、要素の正しいネストを保証し、暗黙的な閉じ処理を決定するために重要です。

  4. アクティブなフォーマット要素のリスト (List of Active Formatting Elements): <b>, <i>, <a> などのフォーマット要素が、DOMツリーのどこに適用されるべきかを追跡するために使用されるリストです。これらの要素は、パース中に暗黙的に閉じられたり、再開されたりすることがあります。

  5. popUntil 操作: 開いている要素のスタックから、特定の要素が見つかるか、特定の「スコープ停止タグ (scope stop tags)」のいずれかが見つかるまで要素をポップする操作です。これは、HTMLのネストルールや、特定の要素が別の要素を暗黙的に閉じる挙動を実装するために使用されます。

    • defaultScopeStopTags: 多くの要素で共通して使用されるスコープ停止タグのセット。
    • buttonScopeStopTags: <button> 要素に特有のスコープ停止タグのセット。
  6. reconstructActiveFormattingElements 操作: アクティブなフォーマット要素のリストに基づいて、必要に応じてフォーマット要素を再構築する操作です。これは、パース中にフォーマット要素が暗黙的に閉じられた後、再び有効なスコープに入った場合に、それらをDOMツリーに再挿入するために使用されます。

  7. framesetOK フラグ: このフラグは、<frameset> 要素がまだ挿入可能であるかどうかを示すために使用されます。HTML5の仕様では、特定の要素(例えば <body> の内容の一部として現れる要素)がパースされた後、<frameset> の挿入が許可されなくなります。

技術的詳細

このコミットの核心は、src/pkg/html/parse.go 内の inBodyIM 関数における <button> 要素のハンドリングロジックの追加です。inBodyIM は、HTMLパーサーが「in body」挿入モードにあるときに呼び出される関数で、<body> 要素のコンテンツをパースする際のルールを実装しています。

追加されたコードは以下の通りです。

		case "button":
			p.popUntil(defaultScopeStopTags, "button")
			p.reconstructActiveFormattingElements()
			p.addElement(p.tok.Data, p.tok.Attr)
			p.framesetOK = false

このコードブロックは、パーサーが <button> の開始タグを検出したときに実行されます。

  1. p.popUntil(defaultScopeStopTags, "button"):

    • これは、HTML5パースアルゴリズムの「An end tag whose tag name is "button"」または「A start tag whose tag name is "button"」のルールに相当します。
    • パーサーは、開いている要素のスタックを上から順に見ていき、defaultScopeStopTags に含まれるタグ(例えば <html>, <body>, <table> など)が見つかるか、または button 要素が見つかるまで、スタックから要素をポップします。
    • これにより、<body><button><button> のようなケースで、最初の <button> が適切に閉じられていない場合に、新しい <button> を挿入する前に既存の <button> を強制的に閉じることができます。
  2. p.reconstructActiveFormattingElements():

    • popUntil によってスタックから要素がポップされた後、アクティブなフォーマット要素のリストがDOMツリーの現在の状態と一致しなくなる可能性があります。
    • この関数は、リスト内のフォーマット要素を、現在の開いている要素のスタックに基づいて再構築します。これにより、例えば <b><button></b> のようなマークアップで、<button><b> を暗黙的に閉じた後、新しい要素が挿入される際に <b> のフォーマットが正しく再開されることを保証します。
  3. p.addElement(p.tok.Data, p.tok.Attr):

    • これは、新しい <button> 要素をDOMツリーに挿入する標準的な操作です。
  4. p.framesetOK = false:

    • HTML5の仕様では、<body> 要素内に特定の要素(例えば <button>)が挿入された後、<frameset> 要素を挿入することは許可されません。
    • このフラグを false に設定することで、パーサーは以降のパースで <frameset> 要素の挿入を拒否するようになります。これは、HTMLドキュメントの構造的な整合性を維持するために重要です。

この修正により、GoのHTMLパーサーは、<button> 要素の処理に関してHTML5のパース仕様に厳密に準拠するようになり、より複雑なHTMLドキュメントでも正確なDOMツリーを構築できるようになりました。

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

変更は主に以下の2つのファイルで行われています。

  1. src/pkg/html/parse.go:

    • inBodyIM 関数内に、case "button": の新しいブロックが追加されました。
    • このブロックは、<button> 開始タグが検出された際のパースロジックを定義しています。
  2. src/pkg/html/parse_test.go:

    • TestParser 関数内のテストケースリストで、tests6.dat のテスト上限が 13 から 26 に変更されました。
    • これは、この修正によって tests6.dat 内のより多くのテストケースがパスするようになったことを示しています。

コアとなるコードの解説

src/pkg/html/parse.go の変更は、HTMLパーサーのツリー構築フェーズにおける「in body」挿入モードの挙動を直接変更しています。

// src/pkg/html/parse.go (抜粋)
func inBodyIM(p *parser) bool {
	// ... 既存のコード ...
	switch p.tok.Type {
	// ... 既存のケース ...
	case "plaintext":
		p.popUntil(buttonScopeStopTags, "p")
		p.addElement(p.tok.Data, p.tok.Attr)
	case "button": // <-- ここが追加された部分
		p.popUntil(defaultScopeStopTags, "button")
		p.reconstructActiveFormattingElements()
		p.addElement(p.tok.Data, p.tok.Attr)
		p.framesetOK = false
	case "optgroup", "option":
		if p.top().Data == "option" {
			p.oe.pop()
		}
	// ... 既存のコード ...
	}
	// ... 既存のコード ...
}

この case "button": ブロックの追加により、パーサーは <button> 開始タグを検出した際に、以下の手順で処理を行います。

  1. 既存の <button> 要素の暗黙的な閉じ: p.popUntil(defaultScopeStopTags, "button") が呼び出されます。これは、開いている要素のスタックに既に <button> 要素が存在する場合、その要素を閉じ、スタックからポップすることを意味します。これにより、<button><button> のようなマークアップが、最初の <button> が閉じられた後に2番目の <button> が開かれるという正しいDOM構造に変換されます。

  2. アクティブなフォーマット要素の再構築: p.reconstructActiveFormattingElements() が呼び出されます。これは、popUntil 操作によって、アクティブなフォーマット要素のリストが現在のDOMツリーの状態と同期しなくなった場合に、それを修正するために必要です。例えば、<b><button></b> のようなケースで、<button><b> を暗黙的に閉じた後、新しい <button> が挿入される際に <b> のフォーマットが正しく再開されるようにします。

  3. 新しい <button> 要素の追加: p.addElement(p.tok.Data, p.tok.Attr) が呼び出され、現在のトークン(新しい <button> 開始タグ)に基づいて、DOMツリーに新しい <button> 要素が追加されます。

  4. framesetOK フラグの更新: p.framesetOK = false が設定されます。これは、HTML5の仕様で、<body> 要素内に特定の要素(この場合は <button>)が挿入された後、<frameset> 要素の挿入が許可されなくなるというルールに対応しています。

src/pkg/html/parse_test.go の変更は、この修正が正しく機能することを確認するためのものです。

// src/pkg/html/parse_test.go (抜粋)
func TestParser(t *testing.T) {
	testFiles := []struct {
		filename string
		maxTest  int
	}{
		// ... 既存のテストファイル ...
		{"tests3.dat", -1},
		{"tests4.dat", -1},
		{"tests5.dat", -1},
		{"tests6.dat", 26}, // <-- ここが変更された部分
	}
	// ... 既存のテスト実行ロジック ...
}

tests6.datmaxTest 値が 13 から 26 に変更されたことは、このコミットによって tests6.dat 内のテスト13(<button><button> のケース)だけでなく、テスト25までの他の関連するテストもパスするようになったことを示しています。これは、<button> のパースロジックの改善が、他の要素のパースにも良い影響を与えたか、または関連するパースエラーが同時に修正されたことを意味します。

関連リンク

  • HTML5 Parsing Algorithm: https://html.spec.whatwg.org/multipage/parsing.html
    • 特に「The in body insertion mode」セクションと「A start tag whose tag name is "button"」のルールを参照すると、このコミットの変更がHTML5仕様にどのように対応しているかを深く理解できます。

参考にした情報源リンク

  • Go言語の html パッケージのソースコード: https://pkg.go.dev/golang.org/x/net/html (コミット当時のパスは src/pkg/html でしたが、現在は golang.org/x/net/html に移動しています)
  • WebKitのHTMLテストスイート (tests6.dat): このコミットが参照しているテストデータは、WebKitプロジェクトの一部として公開されているHTML5パースの適合性テストです。具体的なファイルはオンラインで検索することで見つけることができます。
  • HTML5仕様書 (WHATWG): 上記の関連リンクと同じ。HTMLパースの挙動を理解するための最も権威ある情報源です。