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

[インデックス 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宣言が持つ構造化された情報(ドキュメントタイプ名、公開識別子、システム識別子)を個別に利用したり、正確に再構築したりすることが困難でした。

このコミットの背景には、以下のような課題があったと考えられます。

  1. 正確なHTMLレンダリングの必要性: パースされたHTMLツリーを元の形式に忠実にレンダリングするためには、DOCTYPE宣言の詳細な情報が必要でした。特に、公開識別子やシステム識別子を含む複雑なDOCTYPE宣言を正しく出力するには、それらを個別に保持する仕組みが不可欠です。
  2. HTML5のDOCTYPE対応: HTML5では<!DOCTYPE html>という簡潔なDOCTYPEが推奨されていますが、それ以前のHTMLバージョンやXHTMLでは、DTD(Document Type Definition)を参照するための公開識別子やシステム識別子が含まれることが一般的でした。これらの多様なDOCTYPE形式に対応するためには、より柔軟なパースロジックが求められました。
  3. テストケースの網羅性: コミットメッセージに記載されているように、特定の複雑な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: ドキュメントタイプ名。通常はhtmlHTMLxhtmlなど。
  • 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">
    

    ここで、HTMLname"-//W3C//DTD HTML 4.01//EN"が公開識別子、"http://www.w3.org/TR/html4/strict.dtd"がシステム識別子です。

  • HTML5:

    <!DOCTYPE html>
    

    HTML5では、非常に簡潔な形式が採用されており、公開識別子やシステム識別子は含まれません。

HTMLパーシング

HTMLパーシングとは、HTMLドキュメントのテキストデータを読み込み、それをブラウザが理解できる構造化されたデータ(通常はDOMツリー)に変換するプロセスです。このプロセスには、字句解析(トークン化)と構文解析が含まれます。

  1. 字句解析(Lexical Analysis / Tokenization): 入力されたHTML文字列を、意味のある最小単位である「トークン」に分割します。例えば、<p></p><div><!DOCTYPE html>などがトークンとして認識されます。このコミットでは、DoctypeTokenが生成された際に、そのデータ(DOCTYPE宣言の文字列全体)をさらに詳細に解析する部分が改善されています。

  2. 構文解析(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.goparseDoctypeという新しい関数が導入され、DOCTYPE宣言の文字列を解析して、その構成要素(名前、公開識別子、システム識別子)をNode構造体の属性として格納するようになったことです。また、この変更に伴い、パースされたNodeをHTML文字列として再構築するsrc/pkg/html/render.goのロジックも更新されています。

parseDoctype関数の導入 (src/pkg/html/parse.go)

以前は、DoctypeTokenが検出された際に、そのデータ(DOCTYPE宣言の生文字列)がそのままNodeDataフィールドに格納されていました。しかし、新しいparseDoctype関数は、この生文字列を詳細に解析します。

  1. 名前の抽出: DOCTYPE宣言の最初の空白までの部分をドキュメントタイプ名として抽出し、小文字に変換してNode.Dataに設定します。 例: <!DOCTYPE HTML PUBLIC ...> から html を抽出。

  2. 識別子の検出と抽出: 残りの文字列から、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からpublicsystemの識別子を抽出し、それらの存在に応じて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オブジェクトに変換します。

  1. 名前の抽出: strings.IndexAny(s, whitespace)を使って、文字列sの最初の空白文字の位置を見つけます。この位置までがDOCTYPEの名前と判断されます。 n.Data = strings.ToLower(s[:space])で、抽出した名前を小文字に変換してNodeDataフィールドに設定します。HTMLのDOCTYPE名は通常大文字・小文字を区別しないため、小文字に統一することで正規化しています。 s = strings.TrimLeft(s[space:], whitespace)で、名前の後の空白をスキップし、残りの文字列を処理対象とします。

  2. 識別子のパースループ: 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})で、抽出した識別子をNodeAttrスライスに追加します。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文字列形式に再構築する役割を担います。

  1. 識別子の抽出: n.Attrスライスをイテレートし、Key"public"または"system"であるAttributeを探し、それぞれのValp(public)とs(system)変数に格納します。

  2. PUBLIC識別子の出力: if p != ""の条件で、公開識別子が存在する場合の処理を行います。 まず、PUBLICという文字列を出力します。 次に、writeQuoted(w, p)を呼び出して、公開識別子pを適切に引用符で囲んで出力します。 もしシステム識別子sも存在する場合は、その後にスペースを挟んでwriteQuoted(w, s)を呼び出し、システム識別子も出力します。

  3. SYSTEM識別子のみの出力: else if s != ""の条件で、公開識別子は存在しないがシステム識別子のみが存在する場合の処理を行います。 SYSTEMという文字列を出力します。 writeQuoted(w, s)を呼び出して、システム識別子sを適切に引用符で囲んで出力します。

このロジックにより、Nodeに格納された構造化されたDOCTYPE情報が、HTMLの仕様に沿った正しいDOCTYPE宣言としてレンダリングされることが保証されます。

writeQuoted関数 (src/pkg/html/render.go)

このヘルパー関数は、DOCTYPE宣言の識別子を引用符で囲んで出力する際に使用されます。

  1. 引用符の選択: デフォルトでは二重引用符(")を使用します。 しかし、if strings.Contains(s, ")の条件で、出力する文字列sの中に二重引用符が含まれているかをチェックします。 もし含まれている場合、引用符の衝突を避けるために単一引用符(')を使用するようにqの値を変更します。これは、HTMLの属性値やDOCTYPE識別子において、文字列内に使用されている引用符と異なる種類の引用符で囲むことで、文字列の終端を誤認識させないための一般的なプラクティスです。

  2. 文字列の出力: 選択された引用符qで文字列sを囲み、w.WriteByte(q)w.WriteString(s)を使って出力します。

この関数は、DOCTYPE識別子のような特殊な文字列を安全にレンダリングするためのユーティリティとして機能します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • HTML5仕様 (WHATWG)
  • W3C HTML 4.01仕様
  • Go言語のソースコードリポジトリ (GitHub)
  • Go言語のコードレビューシステム (Gerrit/golang.org/cl)