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

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

このコミットは、Go言語の標準ライブラリであるhtmlパッケージにおけるHTMLパーサーの改善に関するものです。具体的には、<head>セクション内に存在する<link>要素のパース処理が正しく行われるように修正されました。これにより、特定のHTML構造を持つドキュメントが意図通りに解析され、テストケースがパスするようになりました。

コミット

commit 77aabbf217a93d59dd6c9d77e3b91b153291a79e
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Thu Nov 3 17:12:13 2011 +1100

    html: parse <link> elements in <head>
    
    Pass tests1.dat, test 83:
    <title><meta></title><link><title><meta></title>
    
    | <html>
    |   <head>
    |     <title>
    |       "<meta>"
    |     <link>
    |     <title>
    |       "<meta>"
    |   <body>
    
    Also pass test 84:
    <style><!--</style><meta><script>--><link></script>
    
    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/5331061

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

https://github.com/golang/go/commit/77aabbf217a93d59dd6c9d77e3b91b153291a79e

元コミット内容

このコミットは、Go言語のhtmlパッケージにおいて、HTMLドキュメントの<head>セクション内で<link>要素が正しくパースされるように修正しました。これにより、tests1.datのテスト83とテスト84がパスするようになりました。

テスト83の例: <title><meta></title><link><title><meta></title> このHTMLは、<head>内に複数の<title><meta>、そして<link>が混在する構造を示しており、パーサーがこれらを正しく処理できるかどうかが問われます。期待されるパース結果は、<head>内に<title><link>、そして再度<title>がネストされた形で含まれることです。

テスト84の例: <style><!--</style><meta><script>--><link></script> この例は、コメントや異なる種類の要素(<style>, <meta>, <script>, <link>)が複雑に組み合わさったケースで、パーサーがこれらの要素を適切に識別し、構造を構築できるかを確認します。

変更の背景

HTMLのパースは、ウェブブラウザやHTML処理ライブラリにとって非常に重要な機能です。HTMLは非常に柔軟な構文を持つため、厳密なXMLとは異なり、多少の構文エラーがあってもブラウザはそれを「修正」して表示しようとします。この「エラー回復」のメカニズムは、HTMLパーサーの実装を複雑にします。

このコミットが行われた2011年当時、Go言語のhtmlパッケージはまだ初期段階にあり、HTML5のパースアルゴリズムに準拠するための開発が進められていました。HTML5のパースアルゴリズムは、ブラウザの挙動を標準化し、異なるブラウザ間でのHTMLレンダリングの一貫性を高めることを目的としています。

<head>要素内には、ドキュメントのメタデータや外部リソースへのリンクなど、様々な要素が配置されます。<link>要素は、外部スタイルシートやファビコンなど、ドキュメントと外部リソースとの関係を定義するために使用されます。これらの要素が<head>内で正しくパースされない場合、ウェブページが意図した通りに表示されなかったり、外部リソースが読み込まれなかったりする問題が発生します。

このコミットの背景には、GoのhtmlパッケージがHTML5の仕様に準拠し、より堅牢なHTMLパーサーを提供するための継続的な取り組みがありました。特に、<head>内の要素の処理は、HTMLドキュメントの構造を正確に理解するために不可欠であり、この修正はその一環として行われました。

前提知識の解説

HTMLの構造と<head>要素

HTMLドキュメントは、大きく分けて<head><body>の2つの主要なセクションで構成されます。

  • <head>: ドキュメントのメタデータ(ドキュメント自体に関する情報)を格納するセクションです。ブラウザには直接表示されませんが、ページのタイトル、文字エンコーディング、スタイルシートへのリンク、スクリプト、SEO情報などが含まれます。
  • <body>: 実際にブラウザに表示されるコンテンツ(テキスト、画像、リンクなど)を格納するセクションです。

<head>内で使用される主な要素

  • <title>: ウェブページのタイトルを定義します。ブラウザのタブやウィンドウのタイトルバーに表示されます。
  • <meta>: ドキュメントのメタデータ(文字セット、ビューポート設定、説明、キーワードなど)を定義します。
  • <link>: 外部リソース(主にCSSスタイルシートやファビコン)へのリンクを定義します。
  • <style>: ドキュメントに直接CSSスタイルを記述します。
  • <script>: クライアントサイドのスクリプト(JavaScriptなど)を埋め込むか、外部スクリプトファイルを指定します。
  • <base>: ドキュメント内の相対URLの基準となるURLを指定します。
  • <basefont>: (非推奨) ドキュメントのデフォルトフォントサイズ、色、書体を指定します。
  • <bgsound>: (非推奨、IE独自) バックグラウンドで再生されるサウンドを指定します。
  • <command>: (HTML5で非推奨) コマンドボタンを定義します。

HTML5パースアルゴリズムと挿入モード (Insertion Mode)

HTML5の仕様では、HTMLドキュメントをパースするための詳細なアルゴリズムが定義されています。このアルゴリズムは、ブラウザがHTMLをどのように読み込み、DOMツリーを構築するかを標準化しています。

パースアルゴリズムの重要な概念の一つに「挿入モード (Insertion Mode)」があります。これは、パーサーが現在処理しているHTMLの場所(例: <head>内、<body>内、テーブル内など)に応じて、異なるトークン(タグやテキスト)の処理方法を決定する状態機械のようなものです。

  • inHeadIM (In Head Insertion Mode): パーサーが<head>要素の内部を処理しているときにアクティブになる挿入モードです。このモードでは、<title>, <meta>, <link>, <style>, <script>などの要素が特別に扱われます。これらの要素は、通常、開始タグと同時に終了タグが暗黙的に処理されるか、特定のルールに基づいてDOMツリーに挿入されます。

Go言語のhtmlパッケージ

Go言語の標準ライブラリには、HTMLのパースとレンダリングを扱うhtmlパッケージが含まれています。このパッケージは、HTML5のパースアルゴリズムに準拠しており、ウェブスクレイピング、HTMLテンプレート処理、HTMLのサニタイズなど、様々な用途で利用されます。

htmlパッケージのパーサーは、入力されたHTMLをトークンに分解し、それらのトークンに基づいてDOM(Document Object Model)ツリーを構築します。このDOMツリーは、HTMLドキュメントの構造をメモリ上で表現したものであり、プログラムからHTMLの要素や属性にアクセスしたり、変更したりすることを可能にします。

技術的詳細

このコミットの核心は、src/pkg/html/parse.goファイル内のinHeadIM関数における変更です。inHeadIMは、パーサーがHTMLドキュメントの<head>セクションを処理している際の「挿入モード」を管理する関数です。

HTML5のパース仕様では、<head>要素内に出現する特定の要素(例: <meta>, <link>, <base>, <title>, <script>, <style>など)は、特別なルールに基づいて処理されます。これらの要素は、通常、開始タグが検出された時点でDOMツリーに追加され、その直後に暗黙的に終了タグが処理されるか、あるいは特定の条件でパースが中断されることがあります。

変更前は、inHeadIM関数内のStartTagToken(開始タグが検出された場合)のswitch文において、"meta"タグのみが明示的に処理され、他のタグ(例えば"link")は適切な処理が定義されていませんでした。"meta"タグの箇所には// TODO.というコメントがあり、未実装の状態であったことが伺えます。

このコミットでは、"base", "basefont", "bgsound", "command", "link", "meta"といった要素が、<head>内で同様に処理されるように修正されました。具体的には、これらのタグが検出された場合、以下の処理が行われます。

  1. p.addElement(p.tok.Data, p.tok.Attr): 現在のトークン(タグ名と属性)に基づいて、新しい要素をDOMツリーに追加します。
  2. p.oe.pop(): 「オープン要素スタック (open elements stack)」から現在の要素をポップします。これは、これらの要素が通常、子要素を持たず、開始タグの直後に「閉じられる」と見なされるためです。
  3. p.acknowledgeSelfClosingTag(): 自己終了タグとして認識します。HTML5では、<link><meta>のような要素は自己終了タグとして扱われることが多く、明示的な終了タグがなくても閉じられたものと見なされます。

この変更により、<link>要素が<head>内で検出された際に、パーサーがそれを正しくDOMツリーに挿入し、その後のパース処理を適切に継続できるようになりました。

また、src/pkg/html/parse_test.goファイルでは、TestParser関数内のテストケースのループ範囲が83から85に拡張されました。これは、新たにテスト83とテスト84がテストスイートに含まれるようになったことを意味します。これらのテストは、<head>内の<link>要素のパースに関する特定のシナリオ(コミットメッセージに記載されているような複雑な構造)を検証するために追加されたか、既存のテストがこの修正によってパスするようになったことを示しています。

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

src/pkg/html/parse.go

--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -455,8 +455,10 @@ func inHeadIM(p *parser) (insertionMode, bool) {
 	imply = true
 	case StartTagToken:
 		switch p.tok.Data {
-		case "meta":
-			// TODO.
+		case "base", "basefont", "bgsound", "command", "link", "meta":
+			p.addElement(p.tok.Data, p.tok.Attr)
+			p.oe.pop()
+			p.acknowledgeSelfClosingTag()
 		case "script", "title", "noscript", "noframes", "style":
 			p.addElement(p.tok.Data, p.tok.Attr)
 			p.setOriginalIM(inHeadIM)

src/pkg/html/parse_test.go

--- 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 < 83; i++ {
+		for i := 0; i < 85; i++ {
 			// Parse the #data section.
 			b, err := ioutil.ReadAll(<-rc)
 			if err != nil {

コアとなるコードの解説

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

inHeadIM関数は、HTMLパーサーが<head>要素の内部を処理する際のロジックを定義しています。この関数は、現在のトークン(HTML要素の開始タグ、終了タグ、テキストなど)の種類に基づいて、次のパース動作を決定します。

変更前は、StartTagToken(開始タグ)が検出された際に、switch p.tok.Data文でタグ名が"meta"の場合のみが特別に扱われていました。しかし、その処理は// TODO.とコメントされており、実際には何も行われていませんでした。これは、<meta>タグのパースが未実装であったことを示唆しています。

変更後、"base", "basefont", "bgsound", "command", "link", "meta"といった複数のタグが同じcase文で処理されるようになりました。これらのタグは、HTMLの仕様上、<head>内で出現し、通常は子要素を持たず、開始タグの直後に「閉じられる」と見なされる特性を持っています。

この共通の処理ブロックでは、以下の3つの重要なメソッドが呼び出されます。

  1. p.addElement(p.tok.Data, p.tok.Attr):

    • pはパーサーのインスタンスです。
    • p.tok.Dataは現在のトークン(開始タグ)のタグ名(例: "link", "meta")です。
    • p.tok.Attrは現在のトークンに付随する属性のリストです。
    • このメソッドは、指定されたタグ名と属性を持つ新しいHTML要素をDOMツリーに挿入します。
  2. p.oe.pop():

    • p.oeは「オープン要素スタック (open elements stack)」と呼ばれるデータ構造です。これは、現在開いている(まだ閉じられていない)HTML要素を追跡するために使用されます。
    • pop()メソッドは、スタックの最上位にある要素を削除します。
    • <link><meta>のような要素は、自己終了要素(またはvoid要素)として扱われることが多いため、DOMツリーに追加された直後にスタックから削除されます。これにより、パーサーはこれらの要素が「閉じられた」と認識し、次の要素のパースに進むことができます。
  3. p.acknowledgeSelfClosingTag():

    • このメソッドは、現在のタグが自己終了タグとして認識されたことをパーサーに通知します。HTML5では、<link><meta>のような要素は明示的な終了タグがなくても有効であり、このメソッドはその挙動を反映しています。

この変更により、<link>を含むこれらの要素が<head>内で検出された際に、正しくDOMツリーに追加され、パーサーの状態が適切に更新されるようになりました。

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

TestParser関数は、HTMLパーサーの動作を検証するためのテストスイートです。この関数は、tests1.datというデータファイルからテストケースを読み込み、それぞれのHTMLスニペットをパースし、期待されるDOMツリーと比較します。

変更前は、テストケースを処理するループがfor i := 0; i < 83; i++となっており、テスト83までしか実行されていませんでした。

変更後、ループの条件がfor i := 0; i < 85; i++に変更されました。これにより、テスト83とテスト84もテストスイートに含まれるようになり、これらの特定のシナリオがこのコミットによって正しく処理されるようになったことが確認されます。これは、コードの変更が意図した通りに機能し、以前は失敗していたか、あるいはテストされていなかったケースをカバーできるようになったことを示しています。

関連リンク

参考にした情報源リンク