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

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

このコミットは、Go言語のhtmlパッケージにおけるHTMLパーサーに、HTML5仕様で定義されている「in select in table」挿入モードを導入するものです。これにより、<table>要素の内部に不適切にネストされた<select>要素のパース処理が改善され、HTML5の厳密な仕様への準拠が強化されます。

コミット

commit b28f017537df9c10e45c5474612082ed4bbfc8ef
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Sat Dec 24 11:07:14 2011 +1100

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

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

元コミット内容

    html: "in select in table" insertion mode.
    
    Pass tests10.dat, test 16:
    <!DOCTYPE
    html><body><table><tr><td><select><svg><g>foo</g><g>bar</g><p>baz</table><p>quux
    
    | <!DOCTYPE html>
    | <html>
    |   <head>
    |   <body>
    |     <table>
    |       <tbody>
    |         <tr>
    |           <td>
    |             <select>
    |               "foobarbaz"
    |     <p>
    |       "quux"
    
    Also pass tests through test 21:
    <!DOCTYPE html><frameset></frameset><svg><g></g><g></g><p><span>
    
    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/5505069

変更の背景

HTML5の仕様は、ウェブブラウザがHTML文書をどのように解析し、DOM(Document Object Model)ツリーを構築するかについて厳密なルールを定めています。この解析プロセスは非常に複雑で、特に不正なマークアップや予期せぬ要素のネストをどのように扱うかについて詳細な規定があります。

<select>要素は、その内容(<option><optgroup>)に特定の制約があり、また<table>要素も、その内部構造(<thead>, <tbody>, <tr>, <td>など)に関して厳格なルールが適用されます。これら二つの要素が不適切に組み合わされた場合、例えば<table>の内部に<select>がネストされ、さらにその<select>の内部にHTML5の仕様では許可されていない要素(例: <svg>, <p>)が含まれるようなエッジケースでは、パーサーは仕様に準拠したDOMツリーを構築するために特別な処理を必要とします。

このコミットは、tests10.datのテスト16で示されるような特定のシナリオに対応するために導入されました。このテストケースは、<table>内に<select>があり、その<select>内にSVGP要素が不正にネストされている状況を扱っています。従来のパーサーでは、このような構造をHTML5仕様通りに解釈し、適切なDOMツリーを生成することができませんでした。この変更は、このような複雑なケースにおいてもGoのHTMLパーサーがHTML5の厳密な要件を満たし、堅牢なパース結果を提供できるようにするために行われました。

前提知識の解説

HTML5パーシングアルゴリズム

HTML5のパーシングアルゴリズムは、ウェブブラウザがHTMLソースコードを読み込み、それをウェブページとして表示するために必要なDOMツリーに変換する一連のプロセスです。このアルゴリズムは主に以下の2つの段階で構成されます。

  1. トークン化 (Tokenization): 入力されたHTML文字列を、意味のある単位である「トークン」に分解する段階です。トークンには、開始タグ(例: <div>)、終了タグ(例: </div>)、テキストデータ、コメント、DOCTYPE宣言などが含まれます。この段階では、HTMLの構文規則に基づいてトークンが識別されます。

  2. ツリー構築 (Tree Construction): トークン化されたストリームを受け取り、それらを基にDOMツリーを構築する段階です。この段階では、パーサーは「挿入モード」と呼ばれる状態機械に従って動作します。

挿入モード (Insertion Modes)

挿入モードは、ツリー構築段階におけるパーサーの現在の状態を示すものです。HTML文書の異なる部分(例: <head>内、<body>内、<table>内など)では、トークンの処理方法やDOMツリーへの要素の挿入方法が異なります。挿入モードは、これらのコンテキストに応じたルールを適用するために使用されます。パーサーは、特定のタグを検出したり、特定の条件が満たされたりすると、現在の挿入モードを動的に変更します。例えば、<html>タグを検出すると「before html」モードから「before head」モードへ、<head>タグを検出すると「in head」モードへ、<body>タグを検出すると「in body」モードへと遷移します。

主要な挿入モードには以下のようなものがあります。

  • initial
  • before html
  • before head
  • in head
  • in body
  • in table
  • in select
  • in foreign content (SVGやMathMLなどの外部コンテンツ用)

要素スタック (Stack of Open Elements)

要素スタックは、現在開いている(まだ閉じられていない)HTML要素を追跡するためにパーサーが使用するデータ構造です。新しい開始タグが検出されると、対応する要素がスタックにプッシュされます。終了タグが検出されると、スタックのトップにある要素がその終了タグに対応していれば、その要素はスタックからポップされます。このスタックは、HTMLのネスト構造を管理し、不正なネストや閉じ忘れを検出・修正するために不可欠です。

フォーサーペアレンティング (Foster Parenting)

フォーサーペアレンティング(里子に出すメカニズム)は、HTML5パーシングアルゴリズムにおける特殊なエラー処理メカニズムの一つです。これは、特にテーブル関連の要素(<table>, <tbody>, <tr>など)の内部に、HTML5の仕様では許可されていないコンテンツが不正に配置された場合に適用されます。

例えば、<table>の直下に<div>要素が置かれた場合、これはHTMLの構文規則に違反します。通常のDOM構築では、このような不正なネストはエラーを引き起こすか、予期せぬDOM構造を生成する可能性があります。フォーサーペアレンティングメカニズムは、このような不正に配置された要素を、テーブルの直前または直後の適切な親要素(例えば、テーブルの親要素や<body>要素)に「里子に出す」ように移動させます。これにより、不正なHTML構造であってもDOMツリーが崩壊することなく、可能な限り意味のある構造が維持されます。このコミットでは、<select>がテーブル内で不正にネストされた場合に、テーブル構造の整合性を維持するためにこのメカニズムが利用される可能性が示唆されています。

技術的詳細

in select in table 挿入モードの導入

このコミットの核心は、HTML5仕様のセクション12.2.5.4.17で定義されている「in select in table」挿入モードをGoのHTMLパーサーに実装したことです。

  • モードへの遷移: この挿入モードは、パーサーが既に<table>関連の挿入モード(例: in table, in caption, in cellなど)にある状態で、<select>開始タグを検出した場合に切り替わります。
  • 目的と挙動: このモードの主な目的は、テーブル構造の整合性を保ちつつ、<select>要素とその内部コンテンツを適切に処理することです。一般的なin selectモードと似ていますが、<table>関連のタグ(<caption>, <table>, <tbody>, <tfoot>, <thead>, <tr>, <td>, <th>)が検出された場合の挙動が異なります。
    • もしこれらのテーブル関連のタグが検出された場合、パーサーはまず現在の<select>要素を暗黙的に閉じます。これは、<select>要素がテーブル構造を破壊しないようにするためです。
    • その後、検出されたテーブル関連のマークアップを、適切な挿入モードで再処理します。これにより、たとえ<select>要素がテーブル内に不適切にネストされていても、テーブル構造が正しく維持されたDOMツリーが構築されます。
  • 不正なネストの処理: <select>要素の直接の子要素は、HTML5の仕様では<option>または<optgroup>に限定されています。しかし、現実のウェブページでは、開発者が誤って<select>内に他の要素(例: <div>, <p>, <svg>など)をネストさせることがあります。この「in select in table」モードは、このような不正なネストが発生した場合でも、ブラウザが可能な限りDOMツリーを構築しようとする挙動(エラー回復メカニズム)を模倣し、HTML5仕様に準拠した結果を生成することを目指します。

テストケース tests10.dat, test 16 の解析

コミットメッセージに記載されているテストケースは以下の通りです。

<!DOCTYPE html><body><table><tr><td><select><svg><g>foo</g><g>bar</g><p>baz</table><p>quux

このHTMLスニペットは、いくつかの重要なポイントを示しています。

  1. テーブル内の<select>: <table>要素の内部に<select>要素がネストされています。これはHTMLの一般的な使用パターンですが、その内部に不正な要素が含まれる場合に問題が生じます。
  2. <select>内の不正なコンテンツ: <select>要素の内部に<svg>, <g>, <p>といった要素がネストされています。これらはHTML5の仕様では<select>の直接の子要素として許可されていません。
  3. テーブルの閉じ忘れ: </table>タグが<select>の後に来ていますが、<select>の内部に不正な要素があるため、パーサーはこれをどのように解釈すべきか判断に迷う可能性があります。

このコミットが導入される前は、GoのHTMLパーサーはこの種の不正な構造をHTML5仕様通りに処理できず、期待されるDOMツリーを生成できませんでした。新しい「in select in table」挿入モードは、このようなエッジケースにおいて、<select>要素を適切に閉じ、その後のテーブル関連のマークアップを正しく処理することで、HTML5仕様に準拠したDOMツリーを構築することを可能にします。

期待されるDOMツリーは、<select>要素が閉じられ、その内部の不正なコンテンツがテキストノードとして扱われ、<table>要素が正しく閉じられた後に、<p>quuxが適切に配置される形になります。

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

このコミットでは、主にsrc/pkg/html/parse.gosrc/pkg/html/parse_test.goが変更されています。

src/pkg/html/parse.go

  • 新しい挿入モード inSelectInTableIM の追加: HTML5仕様の「in select in table」モードを処理するための新しい関数が追加されました。
  • 既存の挿入モードからの遷移ロジックの追加: inBodyIMinTableIMinCaptionIMinCellIMといった既存のテーブル関連の挿入モード関数内で、<select>タグが検出された際に、新しいinSelectInTableIMへパーサーの状態を遷移させるロジックが追加されました。
    // inBodyIM, inTableIM, inCaptionIM, inCellIM 内の変更例
    case "select":
        p.reconstructActiveFormattingElements()
        // inTableIM の場合、特定の親要素の下で select が開始された場合に fosterParenting を設定
        switch p.top().Data {
        case "table", "tbody", "tfoot", "thead", "tr":
            p.fosterParenting = true
        }
        p.addElement(p.tok.Data, p.tok.Attr)
        p.fosterParenting = false // fosterParenting は要素追加後にリセット
        p.framesetOK = false
        p.im = inSelectInTableIM // 新しい挿入モードへ遷移
        return true
    
  • endSelect ヘルパー関数の抽出: inSelectIM関数内にあった<select>要素を閉じるための共通ロジックが、endSelectという独立したヘルパー関数として抽出されました。これにより、inSelectIMと新しく追加されたinSelectInTableIMの両方からこのロジックを再利用できるようになりました。
    // inSelectIM から抽出された endSelect 関数
    func (p *parser) endSelect() {
        for i := len(p.oe) - 1; i >= 0; i-- {
            switch p.oe[i].Data {
            case "option", "optgroup":
                continue
            case "select":
                p.oe = p.oe[:i]
                p.resetInsertionMode()
            }
            return // select が見つかったらループを終了
        }
    }
    
  • inTableIMにおけるfosterParentingの設定: inTableIM内で<select>タグが検出され、かつその親要素がtable, tbody, tfoot, thead, trのいずれかである場合に、p.fosterParenting = trueが設定されるようになりました。これは、<select>要素の内部に不正なコンテンツが含まれる場合に、そのコンテンツがテーブル構造の外に「里子に出される」可能性があることをパーサーに伝えるための重要なフラグです。

src/pkg/html/parse_test.go

  • TestParser関数内のtests10.datの期待値が16から22に変更されました。これは、新しいパーシングロジックによって、tests10.dat内のより多くのテストケース(具体的にはテスト16からテスト21まで)が正しくパスするようになったことを示しています。

コアとなるコードの解説

inSelectInTableIM 関数

この関数は、in select in table挿入モードにおけるトークンの処理ロジックを実装しています。

// Section 12.2.5.4.17.
func inSelectInTableIM(p *parser) bool {
    switch p.tok.Type {
    case StartTagToken, EndTagToken:
        switch p.tok.Data {
        case "caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th":
            // テーブル関連のタグが検出された場合
            if p.tok.Type == StartTagToken || p.elementInScope(tableScopeStopTags, p.tok.Data) {
                // 開始タグであるか、または要素スタック内に対応する要素が存在する場合
                p.endSelect() // 現在の <select> 要素を閉じる
                return false  // 現在のトークンを再処理するために false を返す
            } else {
                // それ以外の場合(例: 閉じタグで、かつスコープ内に対応する要素がない場合)
                // トークンを無視する
                return true
            }
        }
    }
    // 上記の特殊なケースに該当しない場合、inSelectIM のロジックを呼び出す
    // これは、in select in table モードが in select モードの特殊なケースであることを示している
    return inSelectIM(p)
}
  • この関数は、StartTagTokenまたはEndTagTokenが検出された場合に、そのタグがcaption, table, tbody, tfoot, thead, tr, td, thのいずれかであるかをチェックします。
  • もしこれらのテーブル関連のタグが検出され、かつそれが開始タグであるか、または要素スタック内に対応する要素が存在する場合(p.elementInScope(tableScopeStopTags, p.tok.Data))、パーサーはまずp.endSelect()を呼び出して現在の<select>要素を閉じます。その後、falseを返すことで、現在のトークンを再処理するように指示します。これにより、テーブル構造の整合性が保たれ、不正なネストが修正されます。
  • 上記の特殊なケースに該当しない場合、この関数はinSelectIM(p)を呼び出します。これは、「in select in table」モードが、基本的な「in select」モードの動作を継承しつつ、テーブル関連のタグに対する特別な処理を追加したものであることを意味します。

endSelect ヘルパー関数

endSelect関数は、<select>要素を閉じるための共通ロジックを提供します。

func (p *parser) endSelect() {
    for i := len(p.oe) - 1; i >= 0; i-- {
        switch p.oe[i].Data {
        case "option", "optgroup":
            // <option> または <optgroup> 要素はスキップ
            continue
        case "select":
            // <select> 要素が見つかったら
            p.oe = p.oe[:i] // スタックから <select> 要素とその子孫を削除
            p.resetInsertionMode() // 挿入モードをリセット
        }
        return // <select> が見つかったらループを終了
    }
}
  • この関数は、要素スタック(p.oe)を逆順に走査します。
  • <option><optgroup>要素は、<select>の有効な子要素であるためスキップされます。
  • 最初の<select>要素が見つかると、その要素とそれ以降の要素をスタックから削除し、p.resetInsertionMode()を呼び出してパーサーの挿入モードをリセットします。これにより、<select>要素が適切に閉じられ、パーサーは次の適切な状態に移行できます。

inTableIM における fosterParenting の設定

inTableIM関数内で、<select>タグが検出され、かつその親要素がtable, tbody, tfoot, thead, trのいずれかである場合に、p.fosterParenting = trueが設定されるようになりました。

このfosterParentingフラグは、HTML5の「フォーサーペアレンティング」メカニズムをトリガーするために使用されます。<select>要素の内部にHTML5の仕様では許可されていないコンテンツ(例: tests10.dat<svg><p>)が含まれる場合、このフラグが設定されていると、パーサーはその不正なコンテンツをテーブル構造の外の適切な親要素に「里子に出す」ように処理します。これにより、テーブルの構造が破壊されることなく、不正なコンテンツもDOMツリーに組み込まれる(ただし、別の場所に)ことが保証され、HTML5仕様に準拠した堅牢なDOMツリー構築が促進されます。要素が追加された後、p.fosterParentingfalseにリセットされます。

関連リンク

参考にした情報源リンク