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

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

このコミットは、Go言語の標準ライブラリ html パッケージにおけるHTMLパーサーの改善に関するものです。具体的には、非推奨ながらも一部のHTMLコンテンツに存在する <xmp> タグのパースとレンダリングのサポートを追加しています。これにより、パーサーが <xmp> タグ内のコンテンツを正しく「生テキスト(raw text)」として扱い、その内部のHTMLマークアップを解釈せずにそのまま表示できるようになります。

コミット

commit 3b3922771a1ace2e4781f7e53a16cf566f2c27bf
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Wed Nov 30 15:37:41 2011 +1100

    html: parse <xmp> tags
    
    Pass tests5.dat, test 10:
    <p><xmp></xmp>
    
    | <html>
    |   <head>
    |   <body>
    |     <p>
    |     <xmp>
    
    Also pass the remaining tests in tests5.dat.
    
    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/5440062

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

https://github.com/golang/go/commit/3b3922771a1ace2e4781f7e53a16cf566f2c27bf

元コミット内容

このコミットの目的は、Go言語の html パッケージが <xmp> タグを正しくパースできるようにすることです。これにより、tests5.dat のテスト10(<p><xmp></xmp> のような構造)を含む、tests5.dat 内の残りのテストケースもパスするようになります。これは、HTMLパーサーが <xmp> タグの特殊な性質(内部コンテンツをマークアップとして解釈しない)を認識し、適切に処理することを意味します。

変更の背景

HTMLには、特定の要素(例: <script>, <style>, <textarea>, <plaintext>, <xmp>) の内部コンテンツを「生テキスト(raw text)」として扱うという特殊なルールがあります。これらの要素の内部では、通常のHTMLパースルールが適用されず、タグやエンティティがそのままの文字列として扱われます。

<xmp> タグは、HTML 2.0で導入された非推奨の要素であり、その内部のテキストを整形済みテキストとして表示するために使用されました。これは、現在の <pre> タグと似ていますが、<xmp> は内部のHTMLマークアップをエスケープせずにそのまま表示するという点で異なります。例えば、<xmp><b>bold</b></xmp> はブラウザ上で「bold」と表示され、<b> タグは解釈されません。

Go言語の html パッケージは、HTML5のパースアルゴリズムに準拠することを目指しています。このアルゴリズムでは、<xmp> のような「生テキスト要素」の特殊なパースルールが定義されています。このコミット以前は、html パッケージが <xmp> タグを正しく処理できていなかったため、関連するテストケースが失敗していました。この変更は、パーサーの堅牢性を高め、より広範なHTMLコンテンツを正確に処理できるようにするためのものです。

前提知識の解説

  1. HTMLパースアルゴリズム: HTMLのパースは、非常に複雑なプロセスです。ブラウザは、HTML5仕様で定義された詳細なアルゴリズムに従ってHTMLドキュメントを解析し、DOMツリーを構築します。このアルゴリズムには、様々な「挿入モード(insertion mode)」や「トークナイザーの状態(tokenizer states)」があり、現在のコンテキストに基づいて次のトークンをどのように解釈するかを決定します。

  2. 生テキスト要素 (Raw Text Elements): HTMLには、その内容が通常のHTMLとしてパースされない特殊な要素が存在します。これらは「生テキスト要素」と呼ばれ、<script>, <style>, <textarea>, <title>, <noembed>, <noframes>, <noscript>, <plaintext>, <xmp> などが含まれます。これらの要素の内部では、終了タグが見つかるまで、すべての文字が生のテキストデータとして扱われます。例えば、<script>var a = "<b>test</b>";</script><b> はタグとして解釈されず、単なる文字列の一部として扱われます。

  3. トークナイザー (Tokenizer): HTMLパースの最初の段階はトークナイザーです。トークナイザーは、入力されたHTML文字列を、タグ、属性、テキストデータなどの意味のある「トークン」のストリームに変換します。生テキスト要素の場合、トークナイザーは特殊な状態に入り、終了タグ以外のすべての文字をテキストトークンとして出力します。

  4. パーサー (Parser): トークナイザーによって生成されたトークンのストリームは、パーサーに渡されます。パーサーはこれらのトークンを使用して、DOMツリーを構築します。パーサーは、現在の挿入モードに基づいて、どの要素が許可され、どのようにネストされるべきかを決定します。生テキスト要素の場合、パーサーはトークナイザーが生テキストモードになっていることを認識し、その要素の終了タグが見つかるまで、子ノードとしてテキストノードのみを受け入れます。

  5. tests5.dat: Go言語の html パッケージのテストスイートには、WebKitプロジェクトから派生したHTMLパースのテストデータが含まれています。tests5.dat はそのうちの一つで、様々なエッジケースや特殊なHTML構造のパースを検証するために使用されます。このコミットで tests5.dat のテストがパスするようになったということは、パーサーがより多くの標準的なHTML構造を正しく処理できるようになったことを意味します。

技術的詳細

このコミットは、Go言語の html パッケージ内の以下の主要なコンポーネントに変更を加えています。

  1. src/pkg/html/parse.go (パーサー):

    • inBodyIM 関数は、HTMLパースアルゴリズムの「in body」挿入モードにおけるトークンの処理を定義しています。
    • <xmp> タグが開始タグとして現れた場合、パーサーは特定の処理を行います。
      • p.popUntil(buttonScopeStopTags, "p"): これは、特定のスコープ(ここでは buttonScopeStopTags)内の要素、または <p> 要素が見つかるまで、アクティブな要素スタックから要素をポップする処理です。これは、<xmp> が特定のコンテキストでどのようにネストされるべきかを制御します。
      • p.reconstructActiveFormattingElements(): アクティブなフォーマット要素のリストを再構築します。これは、HTMLパースアルゴリズムにおける複雑なステップの一つで、要素のネストが正しく行われるようにします。
      • p.framesetOK = false: framesetOK フラグを false に設定します。これは、<frameset> 要素が許可されるかどうかを制御するフラグで、<xmp> のような特定の要素がパースされた後に変更されることがあります。
      • p.addElement(p.tok.Data, p.tok.Attr): <xmp> 要素をDOMツリーに追加します。
  2. src/pkg/html/render.go (レンダラー):

    • render1 関数は、DOMノードをHTML文字列にレンダリングする際に使用されます。
    • switch n.Data 文に "xmp" が追加されました。これは、<xmp><iframe, noembed, noframes, noscript, plaintext, script, style と同様に「生テキスト要素」として扱われることを意味します。
    • 生テキスト要素の場合、その子ノードは TextNode であることが期待されます。もし非テキストの子ノードが見つかった場合、エラーが返されます。これは、<xmp> の内部コンテンツがHTMLとして再パースされるべきではないというルールを強制します。
  3. src/pkg/html/token.go (トークナイザー):

    • readStartTag 関数は、開始タグを読み取る際にトークナイザーの状態を決定します。
    • z.data.end - z.data.start はタグ名の長さを表します。以前は [5, 9] の範囲で特殊なタグをチェックしていましたが、<xmp> (長さ3) を含めるために [3, 9] に変更されました。
    • switch z.buf[z.data.start] 文に 'x' (for xmp) が追加されました。これにより、タグ名の最初の文字が 'x' の場合も特殊なタグとして考慮されるようになります。
    • switch s := strings.ToLower(string(z.buf[z.data.start:z.data.end])) 文に "xmp" が追加されました。これにより、トークナイザーは <xmp> を認識し、z.rawTag"xmp" に設定します。z.rawTag が設定されると、トークナイザーは生テキストモードに切り替わり、対応する終了タグが見つかるまで、すべての文字をテキストデータとして扱います。
  4. src/pkg/html/parse_test.go (テスト):

    • TestParser 関数内の testFiles スライスで、tests5.dat のテストケースの実行方法が変更されました。
    • 以前は {"tests5.dat", 10} となっており、tests5.dat の最初の10個のテストのみが実行されていました。
    • 変更後は {"tests5.dat", -1} となり、tests5.dat 内のすべてのテストケースが実行されるようになりました。これは、<xmp> のパースに関する修正により、すべてのテストがパスするようになったことを示しています。

これらの変更により、Goの html パッケージは、HTML5の仕様に準拠し、<xmp> タグの特殊なセマンティクスを正しく処理できるようになりました。

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

src/pkg/html/parse.go

--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -770,6 +770,11 @@ func inBodyIM(p *parser) bool {
 		p.oe.pop()
 		p.oe.pop()
 		p.form = nil
+		case "xmp":
+			p.popUntil(buttonScopeStopTags, "p")
+			p.reconstructActiveFormattingElements()
+			p.framesetOK = false
+			p.addElement(p.tok.Data, p.tok.Attr)
 		case "caption", "col", "colgroup", "frame", "head", "tbody", "td", "tfoot", "th", "thead", "tr":
 			// Ignore the token.
 		default:

src/pkg/html/parse_test.go

--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -154,7 +154,7 @@ func TestParser(t *testing.T) {
 		{"tests2.dat", -1},
 		{"tests3.dat", -1},
 		// tests4.dat is fragment cases.
-		{"tests5.dat", 10},
+		{"tests5.dat", -1},
 	}
 	for _, tf := range testFiles {
 		f, err := os.Open("testdata/webkit/" + tf.filename)

src/pkg/html/render.go

--- a/src/pkg/html/render.go
+++ b/src/pkg/html/render.go
@@ -185,7 +185,7 @@ func render1(w writer, n *Node) error {
 
 	// Render any child nodes.
 	switch n.Data {
-	case "iframe", "noembed", "noframes", "noscript", "plaintext", "script", "style":
+	case "iframe", "noembed", "noframes", "noscript", "plaintext", "script", "style", "xmp":
 		for _, c := range n.Child {
 			if c.Type != TextNode {
 				return fmt.Errorf("html: raw text element <%s> has non-text child node", n.Data)

src/pkg/html/token.go

--- a/src/pkg/html/token.go
+++ b/src/pkg/html/token.go
@@ -406,12 +406,12 @@ func (z *Tokenizer) readStartTag() TokenType {
 		}
 	}
 	// Several tags flag the tokenizer's next token as raw.
-	// The tag name lengths of these special cases ranges in [5, 9].
-	if x := z.data.end - z.data.start; 5 <= x && x <= 9 {
+	// The tag name lengths of these special cases ranges in [3, 9].
+	if x := z.data.end - z.data.start; 3 <= x && x <= 9 {
 		switch z.buf[z.data.start] {
-		case 'i', 'n', 'p', 's', 't', 'I', 'N', 'P', 'S', 'T':
+		case 'i', 'n', 'p', 's', 't', 'x', 'I', 'N', 'P', 'S', 'T', 'X':
 		switch s := strings.ToLower(string(z.buf[z.data.start:z.data.end])); s {
-		case "iframe", "noembed", "noframes", "noscript", "plaintext", "script", "style", "textarea", "title":
+		case "iframe", "noembed", "noframes", "noscript", "plaintext", "script", "style", "textarea", "title", "xmp":
 			z.rawTag = s
 			}
 		}

コアとなるコードの解説

  • src/pkg/html/parse.go:

    • inBodyIM 関数内の switch ステートメントに case "xmp": が追加されました。これは、パーサーが <body> 要素の内部で <xmp> 開始タグを検出した際の具体的な処理フローを定義しています。
    • p.popUntil(buttonScopeStopTags, "p"): これは、<xmp> が挿入される前に、特定の要素(例えば <p>)が適切に閉じられていることを保証するための処理です。HTMLのパースでは、要素のネストルールが厳密に定められており、不適切なネストは自動的に修正されます。
    • p.reconstructActiveFormattingElements(): アクティブなフォーマット要素のリストは、HTMLのパースにおいて、例えば <b><i> のような要素が適切に適用されるようにするために重要です。<xmp> のような特殊な要素が挿入される際には、このリストの再構築が必要になる場合があります。
    • p.framesetOK = false: framesetOK フラグは、<frameset> 要素がドキュメント内で許可されるかどうかを追跡します。通常、<body> 要素がパースされると framesetOKfalse に設定されますが、<xmp> のような要素の挿入もこのフラグに影響を与える可能性があります。
    • p.addElement(p.tok.Data, p.tok.Attr): 最後に、パースされた <xmp> 要素がDOMツリーに追加されます。
  • src/pkg/html/parse_test.go:

    • tests5.dat のテスト実行範囲が 10 から -1 に変更されました。これは、このコミットによって <xmp> のパースが正しく行われるようになり、tests5.dat 内のすべてのテストケース(特に <xmp> に関連するもの)がパスするようになったことを示しています。これにより、テストカバレッジが向上し、パーサーの堅牢性が確認されます。
  • src/pkg/html/render.go:

    • render1 関数内の switch n.Data ステートメントに "xmp" が追加されました。これは、レンダリング時に <xmp> 要素が他の生テキスト要素(iframe, noembed, noframes, noscript, plaintext, script, style)と同様に扱われることを意味します。
    • 生テキスト要素の子ノードは TextNode であることが期待されるため、もし非テキストの子ノードが見つかった場合はエラーが報告されます。これは、<xmp> の内部コンテンツがHTMLとして解釈されずにそのまま出力されるというHTMLのルールをレンダリング時にも適用するための重要な変更です。
  • src/pkg/html/token.go:

    • readStartTag 関数では、タグ名の長さをチェックする条件が [5, 9] から [3, 9] に変更されました。これは、タグ名が3文字の <xmp> をこの特殊な処理の対象に含めるためです。
    • タグ名の最初の文字をチェックする switch ステートメントに 'x''X' が追加されました。これにより、<xmp> の開始タグが検出された際に、トークナイザーが適切な処理を開始できるようになります。
    • タグ名を小文字に変換してチェックする switch ステートメントに "xmp" が追加されました。これにより、トークナイザーは <xmp> を生テキスト要素として認識し、z.rawTag"xmp" に設定します。z.rawTag が設定されると、トークナイザーは生テキストモードに移行し、対応する終了タグが見つかるまで、その後のすべての文字をテキストデータとして扱います。

これらの変更は、HTML5のパースおよびレンダリングの仕様に厳密に準拠し、Goの html パッケージが <xmp> のような特殊な要素を正確に処理できるようにするために不可欠です。

関連リンク

参考にした情報源リンク