[インデックス 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つのカテゴリに分類しています:
- Void elements (空要素):
<br>
,<img>
など - Template element:
<template>
- Raw text elements (Raw Textエレメント):
<script>
,<style>
- Escapable raw text elements (RCDATA/エスケープ可能Raw Textエレメント):
<textarea>
,<title>
- Foreign elements: SVG、MathMLエレメント
- Normal elements: その他の通常のエレメント
Raw Text Elements vs RCDATA Elements
Raw Text Elements (<script>
, <style>
):
- 内容は単純なテキストとして扱われる
- HTMLエンティティ(文字参照)は展開されない
- 唯一の制限は、対応する終了タグ(例:
</script>
)を含むことができない
RCDATA Elements (<textarea>
, <title>
):
- 内容は基本的にテキストとして扱われる
- HTMLエンティティ(文字参照)は展開される(例:
&
→&
) - 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/>
のような自己閉じタグ
技術的詳細
パーサーの状態管理
このコミットでは、パーサーの状態管理を強化するために以下の機能を追加しました:
- originalIM フィールド: text挿入モードまたはinTableText挿入モードが完了した後に戻る挿入モードを保存
- setOriginalIM メソッド: originalIMを設定し、二重設定を防ぐ検証機能
- textIM関数: text挿入モードの実装(HTML5仕様のSection 11.2.5.4.8に準拠)
トークナイザーの拡張
トークナイザーに以下の機能を追加しました:
- rawTag フィールド: 現在のRaw TextまたはRCDATAエレメントの終了タグを記録
- textIsRaw フィールド: 現在のテキストトークンがエスケープされていないかどうかを示す
- 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>
エレメントの内容を適切に処理できませんでした。
状態遷移の流れ:
inHeadIM
で<script>
または<title>
タグを検出setOriginalIM(inHeadIM)
で現在の挿入モードを保存textIM
に遷移してテキスト内容を処理- 終了タグを検出したら、保存された挿入モードに戻る
トークナイザーの改良
最適化されたタグ検出:
- タグ名の長さでフィルタリング(5-8文字)
- 最初の文字で事前選別('n', 's', 't'とその大文字)
- 小文字変換後の完全一致検証
Raw/RCDATAの区別:
z.textIsRaw = z.rawTag != "textarea" && z.rawTag != "title"
この実装により、<textarea>
と<title>
ではHTMLエンティティが展開され、<script>
と<style>
では展開されません。
終了タグの検出アルゴリズム
readRawOrRCDATA
関数は、効率的に終了タグを検出します:
<
文字を検索- 続く
/
文字を確認 - タグ名を大文字小文字を区別せずに比較
- 適切な区切り文字(空白、
>
、/
など)を確認
レンダリングの最適化
従来の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
}
}
関連リンク
- HTML5 Living Standard - The HTML syntax
- HTML5 Specification - Parsing HTML documents
- Go HTML package documentation
- html5lib test suite