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

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

このコミットは、Go言語の実験的なHTMLパーサーパッケージ exp/html における重要な修正を導入しています。具体的には、SVG (Scalable Vector Graphics) の <title> 要素が、HTMLの <title> 要素と同じようにRCDATA(Raw Character Data)として扱われるという誤った挙動を修正しています。これにより、SVGの仕様に準拠し、<title> 要素の内部にタグが記述された場合でも正しくパースされるようになります。

コミット

commit 74db9d298b1b5aedd1d63f0a7257e4b51e21ca0c
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Sun Aug 5 22:32:35 2012 +1000

    exp/html: don't treat SVG <title> like HTML <title>
    
    The content of an HTML <title> element is RCDATA, but the content of an SVG
    <title> element is parsed as tags. Now the parser doesn't go into RCDATA
    mode in foreign content.
    
    Pass 4 additional tests.
    
    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/6448111

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

https://github.com/golang/go/commit/74db9d298b1b5aedd1d63f0a7257e4b51e21ca0c

元コミット内容

exp/html: don't treat SVG <title> like HTML <title>

HTMLの <title> 要素の内容はRCDATAとして扱われますが、SVGの <title> 要素の内容はタグとしてパースされます。今回の変更により、外部コンテンツ(foreign content)内でパーサーがRCDATAモードに入らないようになりました。

これにより、4つの追加テストがパスするようになりました。

変更の背景

このコミットの背景には、HTMLとSVGの仕様における <title> 要素の扱いの違いがあります。

HTMLの <title> 要素は、その内容を「RCDATA(Raw Character Data)」として扱います。これは、<title> タグの開始タグと終了タグの間にあるすべての文字が、マークアップとしてではなく、純粋なテキストデータとして解釈されることを意味します。例えば、<title>My <b>Bold</b> Title</title> の場合、ブラウザは「My Bold Title」という文字列をそのまま表示し、「」をHTMLタグとして解釈してテキストを太字にすることはありません。これは、HTMLの <textarea> 要素や <style> 要素などにも適用される特性です。

一方、SVGの <title> 要素は、その内容をXML(またはSVG)の要素としてパースします。SVGはXMLベースのマークアップ言語であり、その内部の要素はXMLのパースルールに従います。したがって、SVGの <title> 要素内には、他のSVG要素(例えば <desc><text> など)を含めることができ、それらは通常のXML要素として解釈されます。

Go言語の exp/html パッケージのパーサーは、このHTMLとSVGの <title> 要素のセマンティクスの違いを正しく処理できていませんでした。特に、HTMLドキュメント内に埋め込まれたSVGコンテンツ(「foreign content」と呼ばれる)をパースする際に、SVGの <title> をHTMLの <title> と同様にRCDATAとして扱ってしまっていたため、SVGの仕様に違反し、SVG <title> 内のタグが正しく解釈されないというバグがありました。このバグにより、関連するテストが失敗していました。

このコミットは、この不整合を解消し、SVGの <title> 要素がその仕様通りにパースされるようにすることで、HTMLパーサーの堅牢性と正確性を向上させることを目的としています。

前提知識の解説

1. HTMLパーシングとトークナイザー

HTMLパーシングは、HTMLドキュメントを読み込み、それをブラウザが理解できる構造(DOMツリー)に変換するプロセスです。このプロセスは通常、以下の2つの主要なフェーズに分かれます。

  • トークナイゼーション(Tokenization): 入力されたHTML文字列を、意味のある小さな単位(トークン)に分割するフェーズです。トークンには、開始タグ、終了タグ、テキスト、コメント、DOCTYPEなどが含まれます。
  • ツリー構築(Tree Construction): トークナイザーによって生成されたトークンを基に、DOMツリーを構築するフェーズです。

2. RCDATA (Raw Character Data)

RCDATAは、HTMLの特定の要素(例: <title>, <textarea>, <style>, <xmp>, <iframe>, <noembed>, <noframes>) の内容のパースモードを指します。RCDATAモードでは、要素の開始タグと終了タグの間にあるコンテンツは、ほとんどの場合、純粋なテキストとして扱われます。つまり、その中に含まれる <& などの文字は、HTMLエンティティやタグの開始として解釈されず、文字通りの意味で扱われます。唯一の例外は、対応する終了タグのシーケンス(例: </title>) と、文字参照(例: &amp;)です。

3. Foreign Content (外部コンテンツ)

HTML5の仕様では、「foreign content」という概念が導入されました。これは、HTMLドキュメント内に埋め込まれた、HTML以外のXML名前空間に属するコンテンツを指します。最も一般的な例は、SVG (Scalable Vector Graphics) と MathML (Mathematical Markup Language) です。

HTMLパーサーは、HTML要素をパースする通常のモードとは異なるルールセットを使用して、foreign contentをパースする必要があります。例えば、HTMLでは大文字・小文字を区別しないタグ名が一般的ですが、SVGやMathMLではXMLのルールに従い、タグ名は大文字・小文字を区別します。また、自己終了タグの扱いなども異なります。

4. SVG (Scalable Vector Graphics)

SVGは、XMLベースの2次元ベクターグラフィックス記述言語です。Web上でインタラクティブなグラフィックスを表示するために使用されます。SVGはXMLのサブセットであるため、その要素や属性はXMLのパースルールに従います。

5. Go言語の exp/html パッケージ

exp/html は、Go言語の標準ライブラリの一部として提供されているHTMLパーサーおよびレンダラーの実験的なパッケージです。このパッケージは、WHATWG (Web Hypertext Application Technology Working Group) のHTML Living Standardに準拠したHTML5のパースルールを実装することを目指しています。ウェブブラウザがHTMLをどのように解釈するかを模倣しており、堅牢なHTML処理を可能にします。

技術的詳細

このコミットの技術的な核心は、HTMLパーサーが「foreign content」を処理する際の挙動を修正することにあります。

  1. parse.go における rawTag のクリア: HTMLパーサーのトークナイザーは、<title><textarea> のようなRCDATA要素を検出すると、その内容をRCDATAとして処理するために内部的に rawTag という状態を設定します。この状態が設定されている間、トークナイザーは終了タグ以外のマークアップを無視し、すべてをテキストとして扱います。 しかし、SVGの <title> 要素はRCDATAではありません。その内容はXMLとしてパースされるべきです。 このコミットでは、parseForeignContent 関数(foreign contentを処理する部分)に以下のロジックが追加されました。

    if namespace != "" {
        // Don't let the tokenizer go into raw text mode in foreign content
        // (e.g. in an SVG <title> tag).
        p.tokenizer.rawTag = ""
    }
    

    namespace != "" は、現在の要素がHTML名前空間以外の(つまりSVGやMathMLのような)foreign contentであることを示します。この条件が真の場合、p.tokenizer.rawTag = "" が実行され、トークナイザーのRCDATAモードが強制的に解除されます。これにより、SVG <title> の内部でトークナイザーがRCDATAモードに入ることがなくなり、その内容が通常のタグとしてパースされるようになります。

  2. render.go における名前空間の考慮: HTMLの <title> 要素や <textarea> 要素はRCDATAであるため、その子ノードはテキストノードである必要があります。もし非テキストノード(例: <div>)が含まれている場合、それはHTMLの仕様に違反し、レンダリング時にエラーとなるべきです。 しかし、SVGの <title> 要素は非テキストノードを子に持つことができます。 このコミットでは、render.gorender1 関数内のRCDATA要素のレンダリングロジックが修正されました。

    // 変更前:
    // if c.Type != TextNode {
    //     return fmt.Errorf("html: RCDATA element <%s> has non-text child node", n.Data)
    // }
    // 変更後:
    if c.Type != TextNode && n.Namespace == "" {
        return fmt.Errorf("html: RCDATA element <%s> has non-text child node", n.Data)
    }
    

    n.Namespace == "" という条件が追加されました。これは、現在のノードがHTML名前空間に属している場合にのみ、非テキスト子ノードのチェックを行うことを意味します。もしノードがforeign content(例: SVG <title>)であれば、n.Namespace は空ではないため、このエラーチェックはスキップされ、非テキスト子ノードが存在してもエラーにはなりません。これにより、パース時の修正とレンダリング時の挙動が整合性を持ち、SVGの仕様に準拠したレンダリングが可能になります。

これらの変更により、Goの exp/html パーサーは、HTMLとSVGの <title> 要素のセマンティクスの違いを正確に区別し、それぞれの仕様に従ってコンテンツを処理できるようになりました。

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

src/pkg/exp/html/parse.go

--- a/src/pkg/exp/html/parse.go
+++ b/src/pkg/exp/html/parse.go
@@ -1902,6 +1902,11 @@ func parseForeignContent(p *parser) bool {
 		namespace := p.top().Namespace
 		p.addElement()
 		p.top().Namespace = namespace
+		if namespace != "" {
+			// Don't let the tokenizer go into raw text mode in foreign content
+			// (e.g. in an SVG <title> tag).
+			p.tokenizer.rawTag = ""
+		}
 		if p.hasSelfClosingToken {
 			p.oe.pop()
 			p.acknowledgeSelfClosingTag()

src/pkg/exp/html/render.go

--- a/src/pkg/exp/html/render.go
+++ b/src/pkg/exp/html/render.go
@@ -209,7 +209,7 @@ func render1(w writer, n *Node) error {
 		}
 	case "textarea", "title":
 		for _, c := range n.Child {
-			if c.Type != TextNode {
+			if c.Type != TextNode && n.Namespace == "" {
 				return fmt.Errorf("html: RCDATA element <%s> has non-text child node", n.Data)
 			}
 			if err := render1(w, c); err != nil {

コアとなるコードの解説

parse.go の変更

parseForeignContent 関数は、HTMLパーサーがSVGやMathMLなどの外部コンテンツを処理する際に呼び出されます。この関数内で、新しい要素が追加される直前に、現在の要素がHTML名前空間以外の名前空間(namespace != "")に属しているかどうかがチェックされます。

もし外部コンテンツであれば、p.tokenizer.rawTag = "" が実行されます。

  • p.tokenizer は、HTML文字列をトークンに分割する役割を担うトークナイザーのインスタンスです。
  • rawTag は、トークナイザーがRCDATAモードで動作しているかどうかを示す内部フラグ(または状態変数)です。HTMLの <title><textarea> のような要素をパースする際に、このフラグが設定され、内部のコンテンツが純粋なテキストとして扱われるようになります。

この行を追加することで、SVGの <title> 要素のような外部コンテンツの内部では、トークナイザーがRCDATAモードに強制的に入ることを防ぎます。これにより、SVG <title> 内の <p><div> といったタグが、RCDATAとしてではなく、通常のXML要素として正しくパースされるようになります。

render.go の変更

render1 関数は、HTMLノードツリーをHTML文字列としてレンダリングする役割を担っています。case "textarea", "title": のブロックは、<textarea><title> 要素のレンダリングロジックを処理します。

変更前のコード if c.Type != TextNode は、これらの要素の子ノードがテキストノードでない場合にエラーを返していました。これはHTMLの <title><textarea> がRCDATA要素であり、その子ノードはテキストであるべきというHTMLの仕様に準拠した挙動です。

変更後のコード if c.Type != TextNode && n.Namespace == "" は、このエラーチェックに n.Namespace == "" という条件を追加しています。

  • n.Namespace は、現在レンダリング中のノードが属するXML名前空間を示します。HTML要素の場合、この値は空文字列 "" です。SVG要素の場合、SVGの名前空間URI(例: "http://www.w3.org/2000/svg")が設定されます。

この変更により、非テキスト子ノードのチェックは、ノードがHTML名前空間に属している場合にのみ行われるようになります。つまり、SVGの <title> 要素(n.Namespace が空ではない)の場合、このエラーチェックはスキップされ、SVGの仕様通りに非テキスト子ノードを持つことが許容されるようになります。これにより、パース時の修正とレンダリング時の挙動が整合性を持ち、SVGの <title> が正しくレンダリングされるようになります。

関連リンク