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

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

GoのHTMLパーサーに対するRaw TextおよびRCDATAエレメント(<script><title>など)の解析機能の実装

コミット

  • Author: Nigel Tao nigeltao@golang.org
  • Date: Wed Oct 19 08:03:30 2011 +1100
  • Commit Hash: b1fd528db5305d85c6dfabd8ff7d0656c7f97a39
  • Review: R=andybalholm, CC=golang-dev
  • Change-Id: https://golang.org/cl/5301042

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

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

元コミット内容

html: parse raw text and RCDATA elements, such as <script> and <title>.

Pass tests1.dat, test 26:
#data
<script><div></script></div><title><p></title><p><p>
#document
| <html>
|   <head>
|     <script>
|       "<div>"
|     <title>
|       "<p>"
|   <body>
|     <p>
|     <p>

Thanks to Andy Balholm for driving this change.

変更の背景

このコミットは、Go言語のHTMLパーサーにおいて、Raw TextおよびRCDATAエレメントの適切な解析を実装するために作成されました。2011年時点で、Go言語のHTMLパーサーは初期段階にあり、HTML5仕様に準拠した完全な実装を目指していました。

背景の詳細

  • HTML5仕様への準拠: HTML5仕様では、異なるタイプのエレメントに対して異なる解析ルールが定義されています
  • テストケース合格: このコミットにより、html5lib/webkit test suiteのtest 26が合格するようになりました
  • 実世界での使用: <script><title><textarea>などのエレメントは実際のWebページで非常に頻繁に使用されるため、正しい解析が必要でした
  • コミュニティからの貢献: Andy Balholm氏が主導した変更で、Go言語のHTMLパーサーの発展に大きく貢献しました

前提知識の解説

HTMLエレメントの分類

HTML5仕様では、エレメントを以下の6つのカテゴリに分類しています:

  1. Void elements (空要素): <br>, <img>など
  2. Template element: <template>
  3. Raw text elements (Raw Textエレメント): <script>, <style>
  4. Escapable raw text elements (RCDATA/エスケープ可能Raw Textエレメント): <textarea>, <title>
  5. Foreign elements: SVG、MathMLエレメント
  6. Normal elements: その他の通常のエレメント

Raw Text Elements vs RCDATA Elements

Raw Text Elements (<script>, <style>):

  • 内容は単純なテキストとして扱われる
  • HTMLエンティティ(文字参照)は展開されない
  • 唯一の制限は、対応する終了タグ(例:</script>)を含むことができない

RCDATA Elements (<textarea>, <title>):

  • 内容は基本的にテキストとして扱われる
  • HTMLエンティティ(文字参照)は展開される(例:&amp;&
  • Raw Text Elementsと同様に、対応する終了タグを含むことができない

HTMLパーサーの挿入モード

HTML5パーサーは状態機械として動作し、以下のような挿入モードを持ちます:

  • initial: 初期状態
  • before html: <html>タグの前
  • before head: <head>タグの前
  • in head: <head>タグ内
  • after head: <head>タグの後
  • in body: <body>タグ内
  • text: Raw TextまたはRCDATAエレメント内
  • in table: テーブル内
  • その他多数

text挿入モードは、<script><style><textarea><title>などのエレメントの内容を処理するときに使用されます。

トークナイザーの動作

HTMLトークナイザーは、入力されたHTMLテキストを以下のようなトークンに分割します:

  • StartTagToken: <div>のような開始タグ
  • EndTagToken: </div>のような終了タグ
  • TextToken: テキストデータ
  • CommentToken: <!-- comment -->のようなコメント
  • DoctypeToken: <!DOCTYPE html>のような文書型宣言
  • SelfClosingTagToken: <br/>のような自己閉じタグ

技術的詳細

パーサーの状態管理

このコミットでは、パーサーの状態管理を強化するために以下の機能を追加しました:

  1. originalIM フィールド: text挿入モードまたはinTableText挿入モードが完了した後に戻る挿入モードを保存
  2. setOriginalIM メソッド: originalIMを設定し、二重設定を防ぐ検証機能
  3. textIM関数: text挿入モードの実装(HTML5仕様のSection 11.2.5.4.8に準拠)

トークナイザーの拡張

トークナイザーに以下の機能を追加しました:

  1. rawTag フィールド: 現在のRaw TextまたはRCDATAエレメントの終了タグを記録
  2. textIsRaw フィールド: 現在のテキストトークンがエスケープされていないかどうかを示す
  3. readRawOrRCDATA メソッド: 対応する終了タグまでの内容を読み取る

レンダリングの改善

レンダリング部分では、エレメントタイプに応じた適切な処理を実装:

  • Raw text elements: 子ノードの内容をそのまま出力(エスケープなし)
  • RCDATA elements: 子ノードをエスケープして出力
  • 通常のエレメント: 通常のレンダリング処理

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

1. パーサーの状態管理 (parse.go:42-44)

// originalIM is the insertion mode to go back to after completing a text
// or inTableText insertion mode.
originalIM insertionMode

2. text挿入モード処理 (parse.go:78-81)

case "script", "title":
    p.addElement(p.tok.Data, p.tok.Attr)
    p.setOriginalIM(inHeadIM)
    return textIM, true

3. textIM関数の実装 (parse.go:90-101)

func textIM(p *parser) (insertionMode, bool) {
    switch p.tok.Type {
    case TextToken:
        p.addText(p.tok.Data)
        return textIM, true
    case EndTagToken:
        p.oe.pop()
    }
    o := p.originalIM
    p.originalIM = nil
    return o, p.tok.Type == EndTagToken
}

4. Raw/RCDATAタグの検出 (token.go:277-288)

// Any "<noembed>", "<noframes>", "<noscript>", "<script>", "<style>",
// "<textarea>" or "<title>" tag flags the tokenizer's next token as raw.
// The tag name lengths of these special cases ranges in [5, 8].
if x := z.data.end - z.data.start; 5 <= x && x <= 8 {
    switch z.buf[z.data.start] {
    case 'n', 's', 't', 'N', 'S', 'T':
        switch s := strings.ToLower(string(z.buf[z.data.start:z.data.end])); s {
        case "noembed", "noframes", "noscript", "script", "style", "textarea", "title":
            z.rawTag = s
        }
    }
}

5. readRawOrRCDATA関数 (token.go:224-268)

func (z *Tokenizer) readRawOrRCDATA() {
loop:
    for {
        c := z.readByte()
        if z.err != nil {
            break loop
        }
        if c != '<' {
            continue loop
        }
        // 終了タグの検出と処理
        // ...
    }
    // RCDATA要素(textareaとtitle)では文字参照が有効
    z.textIsRaw = z.rawTag != "textarea" && z.rawTag != "title"
    z.rawTag = ""
}

コアとなるコードの解説

パーサーの状態管理

このコミットの核心は、HTML5仕様に準拠したtext挿入モードの実装です。従来のパーサーでは、<script><title>エレメントの内容を適切に処理できませんでした。

状態遷移の流れ

  1. inHeadIM<script>または<title>タグを検出
  2. setOriginalIM(inHeadIM)で現在の挿入モードを保存
  3. textIMに遷移してテキスト内容を処理
  4. 終了タグを検出したら、保存された挿入モードに戻る

トークナイザーの改良

最適化されたタグ検出

  • タグ名の長さでフィルタリング(5-8文字)
  • 最初の文字で事前選別('n', 's', 't'とその大文字)
  • 小文字変換後の完全一致検証

Raw/RCDATAの区別

z.textIsRaw = z.rawTag != "textarea" && z.rawTag != "title"

この実装により、<textarea><title>ではHTMLエンティティが展開され、<script><style>では展開されません。

終了タグの検出アルゴリズム

readRawOrRCDATA関数は、効率的に終了タグを検出します:

  1. <文字を検索
  2. 続く/文字を確認
  3. タグ名を大文字小文字を区別せずに比較
  4. 適切な区切り文字(空白、>/など)を確認

レンダリングの最適化

従来のTODOコメントを削除し、実際の実装に置き換えました:

switch n.Data {
case "noembed", "noframes", "noscript", "script", "style":
    // Raw text elements: エスケープなしで出力
    for _, c := range n.Child {
        if c.Type != TextNode {
            return fmt.Errorf("html: raw text element <%s> has non-text child node", n.Data)
        }
        if _, err := w.WriteString(c.Data); err != nil {
            return err
        }
    }
case "textarea", "title":
    // RCDATA elements: エスケープして出力
    for _, c := range n.Child {
        if c.Type != TextNode {
            return fmt.Errorf("html: RCDATA element <%s> has non-text child node", n.Data)
        }
        if err := render(w, c); err != nil {
            return err
        }
    }

関連リンク

参考にした情報源リンク