[インデックス 10494] ファイルの概要
このコミットは、Go言語のhtml
パッケージにおけるHTMLパーサーの改善に関するものです。具体的には、HTMLドキュメントの<!DOCTYPE>
宣言をより正確にパースし、その中に含まれるドキュメントタイプ名、公開識別子(Public Identifier)、システム識別子(System Identifier)を抽出して、Node
構造体の属性として保持するように変更しています。これにより、HTMLのレンダリング時にもこれらの情報が正しく再構築されるようになります。
コミット
commit 77b0ad1e806580e47e4f682dfb912c55e1411b73
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Thu Nov 24 09:28:58 2011 +1100
html: parse DOCTYPE into name and public and system identifiers
Pass tests2.dat, test 59:
<!DOCTYPE <!DOCTYPE HTML>><!--<!--x-->-->
| <!DOCTYPE <!doctype>
| <html>
| <head>
| <body>
| ">"
| <!-- <!--x -->
| "-->"
Pass all the tests in doctype01.dat.
Also pass tests2.dat, test 60:
<!doctype html><div><form></form><div></div></div>
R=nigeltao
CC=golang-dev
https://golang.org/cl/5437045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/77b0ad1e806580e47e4f682dfb912c55e1411b73
元コミット内容
このコミットは、Go言語の標準ライブラリであるhtml
パッケージにおいて、HTMLのDOCTYPE宣言のパース処理を改善するものです。具体的には、DOCTYPE宣言からドキュメントタイプ名、公開識別子(PUBLIC ID)、システム識別子(SYSTEM ID)を抽出し、それらをHTMLノードの属性として格納するように変更しました。これにより、パースされたHTMLツリーがより詳細なDOCTYPE情報を持つようになり、レンダリング時に元のDOCTYPE宣言を正確に再構築できるようになります。
この変更は、tests2.dat
のテスト59とテスト60、およびdoctype01.dat
の全てのテストをパスすることを確認しています。
変更の背景
HTMLのDOCTYPE宣言は、ウェブブラウザやHTMLパーサーに対して、そのドキュメントがどのHTMLまたはXHTMLのバージョンに準拠しているかを伝える重要な役割を果たします。従来のパーサーでは、DOCTYPE宣言全体を単一のデータとして扱っていた可能性がありますが、これではDOCTYPE宣言が持つ構造化された情報(ドキュメントタイプ名、公開識別子、システム識別子)を個別に利用したり、正確に再構築したりすることが困難でした。
このコミットの背景には、以下のような課題があったと考えられます。
- 正確なHTMLレンダリングの必要性: パースされたHTMLツリーを元の形式に忠実にレンダリングするためには、DOCTYPE宣言の詳細な情報が必要でした。特に、公開識別子やシステム識別子を含む複雑なDOCTYPE宣言を正しく出力するには、それらを個別に保持する仕組みが不可欠です。
- HTML5のDOCTYPE対応: HTML5では
<!DOCTYPE html>
という簡潔なDOCTYPEが推奨されていますが、それ以前のHTMLバージョンやXHTMLでは、DTD(Document Type Definition)を参照するための公開識別子やシステム識別子が含まれることが一般的でした。これらの多様なDOCTYPE形式に対応するためには、より柔軟なパースロジックが求められました。 - テストケースの網羅性: コミットメッセージに記載されているように、特定の複雑なDOCTYPE宣言を含むテストケース(
tests2.dat
のテスト59など)をパスするためには、既存のパースロジックでは不十分であった可能性があります。これらのテストをクリアすることで、パーサーの堅牢性と正確性を向上させる狙いがありました。
これらの背景から、DOCTYPE宣言の内部構造をより詳細に解析し、その情報をHTMLノードに付加する機能が導入されました。
前提知識の解説
HTML DOCTYPE宣言
HTMLドキュメントの冒頭に記述される<!DOCTYPE ...>
は、Document Type Declaration(文書型宣言)と呼ばれます。これは、ウェブブラウザやHTMLパーサーに対して、そのHTMLドキュメントがどのHTML(またはXHTML)のバージョンやDTD(Document Type Definition)に準拠しているかを宣言するものです。これにより、ブラウザは適切なレンダリングモード(標準モード、互換モードなど)を選択し、ドキュメントを正しく解釈・表示することができます。
DOCTYPE宣言の一般的な形式は以下の通りです。
<!DOCTYPE name PUBLIC "public_identifier" "system_identifier">
<!DOCTYPE name SYSTEM "system_identifier">
<!DOCTYPE html>
name
: ドキュメントタイプ名。通常はhtml
、HTML
、xhtml
など。PUBLIC
: 公開識別子(Public Identifier)が続くことを示します。これは、DTDの公開名(URNなど)を指定します。SYSTEM
: システム識別子(System Identifier)が続くことを示します。これは、DTDのURI(URL)を指定します。"public_identifier"
: 公開識別子の文字列。"system_identifier"
: システム識別子の文字列。
例:
-
HTML 4.01 Strict:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
ここで、
HTML
がname
、"-//W3C//DTD HTML 4.01//EN"
が公開識別子、"http://www.w3.org/TR/html4/strict.dtd"
がシステム識別子です。 -
HTML5:
<!DOCTYPE html>
HTML5では、非常に簡潔な形式が採用されており、公開識別子やシステム識別子は含まれません。
HTMLパーシング
HTMLパーシングとは、HTMLドキュメントのテキストデータを読み込み、それをブラウザが理解できる構造化されたデータ(通常はDOMツリー)に変換するプロセスです。このプロセスには、字句解析(トークン化)と構文解析が含まれます。
-
字句解析(Lexical Analysis / Tokenization): 入力されたHTML文字列を、意味のある最小単位である「トークン」に分割します。例えば、
<p>
、</p>
、<div>
、<!DOCTYPE html>
などがトークンとして認識されます。このコミットでは、DoctypeToken
が生成された際に、そのデータ(DOCTYPE宣言の文字列全体)をさらに詳細に解析する部分が改善されています。 -
構文解析(Syntactic Analysis / Parsing): トークンのストリームを読み込み、それらがHTMLの文法規則に従っているかを確認し、DOMツリーのような階層構造を構築します。このツリーの各ノードは、HTML要素、テキスト、コメント、DOCTYPE宣言などを表します。
Go言語のhtml
パッケージ
Go言語の標準ライブラリには、HTMLのパースとレンダリングを行うためのhtml
パッケージが含まれています。このパッケージは、HTML5の仕様に準拠したパーサーを提供し、ウェブアプリケーションなどでHTMLコンテンツを動的に生成したり、既存のHTMLを解析したりする際に利用されます。
Node
構造体: HTMLツリーの各要素やテキスト、コメントなどを表す基本的なデータ構造です。Type
フィールドでノードの種類(要素、テキスト、DOCTYPEなど)を、Data
フィールドでノードの主要なデータ(要素名、テキスト内容、DOCTYPE名など)を保持します。また、Attr
フィールドは、要素の属性(id="foo"
、class="bar"
など)を格納するために使用されます。このコミットでは、DOCTYPE宣言の公開識別子やシステム識別子をこのAttr
フィールドに格納するように拡張しています。
技術的詳細
このコミットの主要な技術的変更点は、src/pkg/html/parse.go
にparseDoctype
という新しい関数が導入され、DOCTYPE宣言の文字列を解析して、その構成要素(名前、公開識別子、システム識別子)をNode
構造体の属性として格納するようになったことです。また、この変更に伴い、パースされたNode
をHTML文字列として再構築するsrc/pkg/html/render.go
のロジックも更新されています。
parseDoctype
関数の導入 (src/pkg/html/parse.go
)
以前は、DoctypeToken
が検出された際に、そのデータ(DOCTYPE宣言の生文字列)がそのままNode
のData
フィールドに格納されていました。しかし、新しいparseDoctype
関数は、この生文字列を詳細に解析します。
-
名前の抽出: DOCTYPE宣言の最初の空白までの部分をドキュメントタイプ名として抽出し、小文字に変換して
Node.Data
に設定します。 例:<!DOCTYPE HTML PUBLIC ...>
からhtml
を抽出。 -
識別子の検出と抽出: 残りの文字列から、
PUBLIC
またはSYSTEM
キーワードを探します。 キーワードが見つかった場合、その後に続く空白をスキップし、引用符("
または'
)で囲まれた文字列を識別子として抽出します。 抽出された識別子は、Node.Attr
スライスにAttribute
構造体として追加されます。Key
は"public"
または"system"
、Val
は抽出された識別子の値となります。 この処理は、PUBLIC
識別子の後にSYSTEM
識別子が続く場合(例:PUBLIC "..." "..."
)にも対応しています。
initialIM
関数の変更 (src/pkg/html/parse.go
)
パーサーの初期挿入モード(initialIM
)において、DoctypeToken
が処理される際に、これまではp.tok.Data
を直接Node.Data
に設定していましたが、このコミットにより新しく導入されたparseDoctype
関数を呼び出すように変更されました。
// 変更前
// p.doc.Add(&Node{
// Type: DoctypeNode,
// Data: p.tok.Data,
// })
// 変更後
p.doc.Add(parseDoctype(p.tok.Data))
これにより、DOCTYPEトークンがより構造化されたNode
としてDOMツリーに追加されるようになります。
テストの変更 (src/pkg/html/parse_test.go
)
dumpLevel
関数は、パースされたHTMLツリーを文字列としてダンプする際に使用されます。この関数がDoctypeNode
を処理する際、Node.Attr
に格納された公開識別子とシステム識別子を考慮して、より正確なDOCTYPE宣言の文字列を生成するように変更されました。これにより、パースとレンダリングのラウンドトリップテストがより厳密に行えるようになります。
また、テストケースの実行において、tests2.dat
のテスト59だけでなく、doctype01.dat
の全てのテストケースを網羅するように変更されました。これは、DOCTYPEパースの堅牢性を高めるための重要なステップです。
レンダリングの変更 (src/pkg/html/render.go
)
render1
関数は、Node
構造体からHTML文字列を生成する役割を担います。DoctypeNode
をレンダリングする際、Node.Attr
から公開識別子(public
キー)とシステム識別子(system
キー)を抽出し、それらの存在に応じてPUBLIC
またはSYSTEM
キーワードと、引用符で囲まれた識別子文字列を適切に挿入するようにロジックが追加されました。
さらに、識別子文字列を引用符で囲んで出力するためのヘルパー関数writeQuoted
が新しく追加されました。この関数は、文字列内に二重引用符が含まれる場合は単一引用符を使用し、そうでない場合は二重引用符を使用することで、引用符の衝突を避ける賢い挙動をします。
これらの変更により、パースされたDOCTYPE情報が失われることなく、元のDOCTYPE宣言に近い形でHTMLが再構築されることが保証されます。
コアとなるコードの変更箇所
src/pkg/html/parse.go
parseDoctype
関数の新規追加:func parseDoctype(s string) *Node { n := &Node{Type: DoctypeNode} // ... (名前、PUBLIC/SYSTEM識別子のパースロジック) ... return n }
initialIM
関数内のDoctypeToken
処理の変更:case DoctypeToken: p.doc.Add(parseDoctype(p.tok.Data)) // parseDoctypeを呼び出すように変更 p.im = beforeHTMLIM return true
src/pkg/html/parse_test.go
dumpLevel
関数内のDoctypeNode
処理の変更:n.Attr
をチェックし、public
およびsystem
属性が存在する場合に、それらを<!DOCTYPE ... PUBLIC "..." "...">
または<!DOCTYPE ... SYSTEM "...">
の形式で出力するように変更。case DoctypeNode: fmt.Fprintf(w, "<!DOCTYPE %s", n.Data) if n.Attr != nil { var p, s string for _, a := range n.Attr { switch a.Key { case "public": p = a.Val case "system": s = a.Val } } if p != "" || s != "" { fmt.Fprintf(w, ` "%s"`, p) fmt.Fprintf(w, ` "%s"`, s) } } io.WriteString(w, ">")
TestParser
関数内のテストファイルリストの変更:doctype01.dat
が追加され、tests2.dat
の特定のテスト番号指定が削除され、全てのテストが実行されるように変更。// 変更前: {"tests2.dat", 59}, // 変更後: {"doctype01.dat", -1}, {"tests2.dat", -1},
src/pkg/html/render.go
render1
関数内のDoctypeNode
処理の変更:n.Attr
からpublic
とsystem
の識別子を抽出し、それらの存在に応じてPUBLIC
またはSYSTEM
キーワードと引用符付きの識別子を出力するロジックを追加。case DoctypeNode: // ... (既存のn.Data出力) ... if n.Attr != nil { var p, s string for _, a := range n.Attr { switch a.Key { case "public": p = a.Val case "system": s = a.Val } } if p != "" { // PUBLIC識別子とSYSTEM識別子の出力ロジック } else if s != "" { // SYSTEM識別子のみの出力ロジック } } return w.WriteByte('>')
writeQuoted
関数の新規追加:func writeQuoted(w writer, s string) error { var q byte = '"' if strings.Contains(s, `"`) { q = '\'' } // ... (引用符で囲んで文字列を出力するロジック) ... return nil }
コアとなるコードの解説
parseDoctype
関数 (src/pkg/html/parse.go
)
この関数は、DOCTYPE宣言の生文字列(例: "HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\""
)を受け取り、それを構造化されたNode
オブジェクトに変換します。
-
名前の抽出:
strings.IndexAny(s, whitespace)
を使って、文字列s
の最初の空白文字の位置を見つけます。この位置までがDOCTYPEの名前と判断されます。n.Data = strings.ToLower(s[:space])
で、抽出した名前を小文字に変換してNode
のData
フィールドに設定します。HTMLのDOCTYPE名は通常大文字・小文字を区別しないため、小文字に統一することで正規化しています。s = strings.TrimLeft(s[space:], whitespace)
で、名前の後の空白をスキップし、残りの文字列を処理対象とします。 -
識別子のパースループ:
for key == "public" || key == "system"
というループで、PUBLIC
またはSYSTEM
キーワードが続く限り識別子のパースを試みます。s = strings.TrimLeft(s, whitespace)
で、キーワードの後の空白をスキップします。quote := s[0]
で、識別子を囲む引用符("
または'
)を特定します。q := strings.IndexRune(s, rune(quote))
で、対応する閉じ引用符の位置を探します。id = s[:q]
またはid = s
で、引用符で囲まれた識別子の値を抽出します。n.Attr = append(n.Attr, Attribute{Key: key, Val: id})
で、抽出した識別子をNode
のAttr
スライスに追加します。Key
は"public"
または"system"
、Val
は識別子の値です。if key == "public" { key = "system" } else { key = "" }
というロジックにより、PUBLIC
識別子の後にSYSTEM
識別子が続く可能性があることを考慮し、次のループでSYSTEM
を探すか、ループを終了するかを制御します。
この関数により、<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
のような複雑なDOCTYPEも、Data: "html"
, Attr: [{Key: "public", Val: "-//W3C//DTD HTML 4.01//EN"}, {Key: "system", Val: "http://www.w3.org/TR/html4/strict.dtd"}]
のように構造化されて表現されるようになります。
render1
関数内のDoctypeNode
処理 (src/pkg/html/render.go
)
この部分のコードは、パースされたDoctypeNode
を元のHTML文字列形式に再構築する役割を担います。
-
識別子の抽出:
n.Attr
スライスをイテレートし、Key
が"public"
または"system"
であるAttribute
を探し、それぞれのVal
をp
(public)とs
(system)変数に格納します。 -
PUBLIC識別子の出力:
if p != ""
の条件で、公開識別子が存在する場合の処理を行います。 まず、PUBLIC
という文字列を出力します。 次に、writeQuoted(w, p)
を呼び出して、公開識別子p
を適切に引用符で囲んで出力します。 もしシステム識別子s
も存在する場合は、その後にスペースを挟んでwriteQuoted(w, s)
を呼び出し、システム識別子も出力します。 -
SYSTEM識別子のみの出力:
else if s != ""
の条件で、公開識別子は存在しないがシステム識別子のみが存在する場合の処理を行います。SYSTEM
という文字列を出力します。writeQuoted(w, s)
を呼び出して、システム識別子s
を適切に引用符で囲んで出力します。
このロジックにより、Node
に格納された構造化されたDOCTYPE情報が、HTMLの仕様に沿った正しいDOCTYPE宣言としてレンダリングされることが保証されます。
writeQuoted
関数 (src/pkg/html/render.go
)
このヘルパー関数は、DOCTYPE宣言の識別子を引用符で囲んで出力する際に使用されます。
-
引用符の選択: デフォルトでは二重引用符(
"
)を使用します。 しかし、if strings.Contains(s,
")
の条件で、出力する文字列s
の中に二重引用符が含まれているかをチェックします。 もし含まれている場合、引用符の衝突を避けるために単一引用符('
)を使用するようにq
の値を変更します。これは、HTMLの属性値やDOCTYPE識別子において、文字列内に使用されている引用符と異なる種類の引用符で囲むことで、文字列の終端を誤認識させないための一般的なプラクティスです。 -
文字列の出力: 選択された引用符
q
で文字列s
を囲み、w.WriteByte(q)
とw.WriteString(s)
を使って出力します。
この関数は、DOCTYPE識別子のような特殊な文字列を安全にレンダリングするためのユーティリティとして機能します。
関連リンク
- Go言語
html
パッケージのドキュメント: https://pkg.go.dev/golang.org/x/net/html (コミット当時のパッケージパスとは異なる可能性がありますが、現在のドキュメントです) - HTML Living Standard - The DOCTYPE: https://html.spec.whatwg.org/multipage/syntax.html#the-doctype
- W3C HTML 4.01 Specification - Document Type Declaration: https://www.w3.org/TR/html401/struct/global.html#h-7.2
参考にした情報源リンク
- Go言語の公式ドキュメント
- HTML5仕様 (WHATWG)
- W3C HTML 4.01仕様
- Go言語のソースコードリポジトリ (GitHub)
- Go言語のコードレビューシステム (Gerrit/golang.org/cl)