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

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

このコミットは、Go言語のhtmlパッケージにおいて、HTMLパーサーが「quirks mode(互換モード)」を検出する機能を追加するものです。これにより、古い、または非標準的なHTMLドキュメントの解析時に、ウェブブラウザが歴史的に行ってきた特定の挙動を模倣できるようになります。具体的には、DOCTYPE宣言に基づいてquirks modeを判定し、それに応じてパーシングロジックを調整します。

コミット

commit c32b60768785684342ebf6efdf50a7476326f473
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Tue Nov 29 11:18:49 2011 +1100

    html: detect quirks mode
    
    Pass tests3.dat, test 23:
    <p><table></table>
    
    | <html>
    |   <head>
    |   <body>
    |     <p>
    |       <table>
    
    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/5446043

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

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

元コミット内容

html: detect quirks mode

Pass tests3.dat, test 23:
<p><table></table>

| <html>
|   <head>
|   <body>
|     <p>
|       <table>

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

変更の背景

この変更の背景には、ウェブブラウザがHTMLドキュメントを解析する際の複雑な歴史的経緯があります。ウェブの初期には、HTMLの標準がまだ確立されておらず、多くのウェブページが非標準的なマークアップや誤った構造で作成されていました。しかし、ブラウザはこれらのページを「壊れた」ものとして表示するのではなく、可能な限りレンダリングしようと努めました。この「寛容な」解析挙動が「quirks mode(互換モード)」の起源です。

Go言語のhtmlパッケージは、HTML5の仕様に準拠したパーサーを目指していますが、現実世界のウェブページは必ずしも厳密なHTML5に準拠しているわけではありません。特に、古いウェブサイトや、特定のブラウザの挙動に依存して作成されたページを正確に解析し、DOMツリーを構築するためには、ブラウザがquirks modeでどのように動作するかを模倣する必要があります。

このコミットは、tests3.datのテスト23(<p><table></table>というHTML断片の解析)が失敗していた問題を解決するために導入されました。このテストケースは、<table>要素がp要素の子として配置されているという、HTMLの仕様では不正な構造を示しています。標準モードではこのような構造は許容されませんが、quirks modeではブラウザが特定の修正(この場合は<table>pの外に「持ち上げる」)を行うことがあります。このコミットは、GoのHTMLパーサーがこのような非標準的なケースでもブラウザ互換のDOMツリーを生成できるように、quirks modeの検出とそれに基づくパーシング挙動の調整を実装しています。

前提知識の解説

HTMLパーシングとDOM

HTMLパーシングとは、HTMLドキュメントのテキストデータを読み込み、それをウェブブラウザが理解できる構造化されたデータ(DOMツリー)に変換するプロセスです。DOM(Document Object Model)は、HTMLやXMLドキュメントの論理構造を表現し、プログラムからドキュメントのコンテンツ、構造、スタイルにアクセスし、操作するためのAPIを提供します。

DOCTYPE宣言

HTMLドキュメントの冒頭には、通常<!DOCTYPE html>のようなDOCTYPE宣言があります。これは、ドキュメントがどのHTMLまたはXHTMLのバージョンに準拠しているかを示すもので、ブラウザにドキュメントをどのようにレンダリングすべきかを伝えます。

標準モード、ほぼ標準モード、Quirksモード

ウェブブラウザは、DOCTYPE宣言の有無や内容に基づいて、HTMLドキュメントのレンダリングモードを切り替えます。

  1. 標準モード (Standards Mode): 最新のウェブ標準(HTML5、CSS3など)に厳密に従ってレンダリングされるモードです。現代のウェブサイトのほとんどはこのモードで表示されます。
  2. ほぼ標準モード (Almost Standards Mode): 標準モードと非常に似ていますが、テーブルセルの垂直方向のサイズ調整など、ごく一部の古いブラウザのバグを模倣する点が異なります。
  3. Quirksモード (Quirks Mode): 古いブラウザ(特にInternet Explorer 6以前)の非標準的な挙動やバグを模倣してレンダリングされるモードです。これは、古いウェブサイトが正しく表示されるようにするために存在します。DOCTYPE宣言がなかったり、特定の古いDOCTYPE宣言が使用されている場合にこのモードがトリガーされます。

Quirksモードでは、CSSのボックスモデルの解釈(IEの「ボックスモデルのバグ」など)、テーブルのレンダリング、画像の配置、要素の継承など、多くの点で標準モードとは異なる挙動を示します。

HTML5パーシングアルゴリズムとQuirksモードの検出

HTML5の仕様では、ブラウザがHTMLドキュメントを解析する際の詳細なアルゴリズムが定義されています。これには、DOCTYPE宣言に基づいてどのレンダリングモードに入るべきかというルールも含まれています。

Quirksモードの検出は、主にDOCTYPE宣言のPUBLIC識別子やSYSTEM識別子の内容に依存します。特定の古い、または非標準的な識別子が存在する場合、ブラウザはQuirksモードに入ります。例えば、HTML 4.01 TransitionalやFramesetのDOCTYPE宣言でSYSTEM識別子がない場合、またはHTML 3.2以前のDOCTYPE宣言などがQuirksモードをトリガーします。

技術的詳細

このコミットは、Go言語のhtmlパッケージのパーサーにQuirksモード検出ロジックを組み込んでいます。

  1. parser構造体へのquirksフィールドの追加: src/pkg/html/parse.goparser構造体にquirks boolフィールドが追加されました。これは、現在のパーサーがQuirksモードで動作しているかどうかを示すフラグです。

    type parser struct {
        // ...
        // quirks is whether the parser is operating in "quirks mode."
        quirks bool
    }
    
  2. quirkyIDsリストの導入: Quirksモードをトリガーする既知のPUBLIC識別子のリストがquirkyIDsというグローバル変数として定義されました。このリストには、Netscape Navigator、Internet Explorer、W3Cの古いHTMLバージョンなど、歴史的にQuirksモードを引き起こしてきた多数のDOCTYPE識別子が含まれています。これらの識別子はすべて小文字に変換されています。

    var quirkyIDs = []string{
        "+//silmaril//dtd html pro v0r11 19970101//",
        // ... 多数の古いDOCTYPE識別子 ...
        "//webtechs//dtd mozilla html//",
    }
    
  3. parseDoctype関数の変更: parseDoctype関数は、DOCTYPEトークンから名前、PUBLIC識別子、SYSTEM識別子を解析し、DoctypeNodeを生成する役割を担っています。この関数が変更され、Nodeだけでなく、quirksフラグも返すようになりました。

    • 名前の比較: DOCTYPE宣言の名前が"html"(大文字小文字を区別)でない場合、quirkstrueに設定されます。その後、名前は小文字に変換されます。
    • PUBLIC識別子とSYSTEM識別子に基づくQuirksモード判定:
      • PUBLICまたはSYSTEMキーワードの後に続く文字列が空でない場合、またはDOCTYPE宣言全体が不正な形式である場合、quirkstrueに設定されます。
      • 解析されたPUBLIC識別子が特定の既知のQuirksモードをトリガーする文字列(例: "-//w3o//dtd w3 html strict 3.0//en//")と一致する場合、quirkstrueに設定されます。
      • quirkyIDsリスト内のいずれかの識別子でPUBLIC識別子が始まる場合、quirkstrueに設定されます。
      • 特定のPUBLIC識別子(例: "-//w3c//dtd html 4.01 frameset//""-//w3c//dtd html 4.01 transitional//")が、SYSTEM識別子が存在しない場合にのみQuirksモードをトリガーする特殊なケースも考慮されています。
      • 特定のSYSTEM識別子(例: "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd")が存在する場合も、quirkstrueに設定されます。
  4. initialIM関数の変更: initialIM関数は、パーサーの初期挿入モードを処理します。DoctypeTokenが検出された場合、parseDoctypeから返されたquirksフラグがパーサーのp.quirksフィールドに設定されるようになりました。 また、DoctypeToken以外のトークンが初期状態で検出された場合(つまり、DOCTYPE宣言がない場合)、パーサーは無条件にQuirksモードに入るように変更されました。これは、DOCTYPE宣言がないHTMLドキュメントは通常Quirksモードで処理されるというブラウザの挙動を模倣しています。

    case DoctypeToken:
        n, quirks := parseDoctype(p.tok.Data)
        p.doc.Add(n)
        p.quirks = quirks // parseDoctypeの結果をp.quirksに設定
        p.im = beforeHTMLIM
        return true
    }
    p.quirks = true // DOCTYPEがない場合はquirks mode
    p.im = beforeHTMLIM
    return false
    
  5. inBodyIM関数の変更: inBodyIM関数は、HTMLの<body>要素内でのパーシングロジックを扱います。この関数内で、<table>要素が検出された際の処理にQuirksモードの考慮が追加されました。

    HTML5の仕様では、<table>要素はp要素の子として配置されることは許されていません。標準モードでは、このような構造が検出された場合、パーサーはp要素を閉じてからtable要素を挿入するなどの「再親付け(foster parenting)」ルールを適用します。しかし、Quirksモードでは、ブラウザはしばしばこのルールを適用せず、<table>p要素の内部に直接挿入しようとします。

    このコミットでは、<table>要素の処理において、p.popUntil(buttonScopeStopTags, "p")という行が!p.quirksの条件で囲まれました。これは、「p要素をポップする(閉じる)処理は、Quirksモードでない場合にのみ実行する」ことを意味します。これにより、Quirksモードでは<table>p要素の内部に挿入されるという、ブラウザ互換の挙動が実現されます。

    case "table":
        if !p.quirks { // Quirksモードでない場合にのみpをポップ
            p.popUntil(buttonScopeStopTags, "p")
        }
        p.addElement(p.tok.Data, p.tok.Attr)
        p.framesetOK = false
        p.im = inTableIM
    

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

  • src/pkg/html/parse.go:
    • parser構造体にquirks boolフィールドを追加。
    • quirkyIDsという[]string型のグローバル変数を追加し、QuirksモードをトリガーするDOCTYPE識別子を定義。
    • parseDoctype関数のシグネチャをfunc parseDoctype(s string) *Nodeからfunc parseDoctype(s string) (n *Node, quirks bool)に変更し、Quirksモードフラグを返すように修正。
    • parseDoctype関数内で、DOCTYPE宣言の内容に基づいてquirksフラグを計算するロジックを追加。
    • initialIM関数内で、parseDoctypeから返されたquirksフラグをp.quirksに設定。また、DOCTYPE宣言がない場合にp.quirkstrueに設定。
    • inBodyIM関数内で、<table>要素の処理において、p.popUntil(buttonScopeStopTags, "p")の呼び出しを!p.quirksの条件で囲む。
  • src/pkg/html/parse_test.go:
    • TestParser関数内のtests3.datのテストケースの期待値が23から-1に変更されました。これは、テスト23がQuirksモードの挙動を正しく反映するようになったため、特定の失敗を期待する必要がなくなったことを示唆しています。

コアとなるコードの解説

このコミットの核心は、HTMLパーサーがウェブブラウザのQuirksモードの挙動を模倣できるようにすることです。

  1. parser.quirksフィールド: このフラグは、パーサーが現在Quirksモードで動作しているかどうかを追跡します。このフラグの状態に基づいて、パーシングロジックが動的に調整されます。
  2. quirkyIDsリストとparseDoctypeのロジック:
    • quirkyIDsは、歴史的にQuirksモードをトリガーしてきた特定のDOCTYPE識別子の「ブラックリスト」として機能します。
    • parseDoctype関数は、入力されたDOCTYPE宣言を解析し、その内容がquirkyIDsリストに含まれるか、またはHTML5仕様で定義されているQuirksモードのトリガー条件(例: DOCTYPE名が"html"でない、特定のPUBLIC/SYSTEM識別子の組み合わせ)に合致するかどうかを判断します。
    • この関数がquirksフラグを返すことで、パーサーの初期化段階でQuirksモードが適切に設定されます。
  3. initialIMでのQuirksモード設定:
    • DOCTYPE宣言が存在する場合、その内容に基づいてparseDoctypeがQuirksモードを決定します。
    • DOCTYPE宣言が全く存在しないHTMLドキュメントは、ブラウザでは常にQuirksモードで処理されるため、initialIMDOCTYPEトークン以外のトークンが来た場合に無条件でp.quirks = trueを設定します。
  4. inBodyIMでの<table>処理の調整:
    • これはQuirksモードがパーシング挙動に影響を与える具体的な例です。標準モードでは、<p><table>のような不正なネストは修正されますが、Quirksモードではブラウザが異なる(より「寛容な」)方法で処理します。
    • if !p.quirks { p.popUntil(...) }という条件は、Quirksモードの場合にはp要素を閉じる(ポップする)処理をスキップし、<table>p要素の「内部」に挿入されるようなDOM構造を許容します。これにより、古いウェブページが意図した通りに解析され、レンダリングされる可能性が高まります。

これらの変更により、Goのhtmlパーサーは、厳密なHTML5準拠のドキュメントだけでなく、現実世界の多様な(しばしば非標準的な)HTMLドキュメントに対しても、より堅牢でブラウザ互換性のある解析結果を提供できるようになります。

関連リンク

参考にした情報源リンク