[インデックス 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コンテンツを正確に処理できるようにするためのものです。
前提知識の解説
-
HTMLパースアルゴリズム: HTMLのパースは、非常に複雑なプロセスです。ブラウザは、HTML5仕様で定義された詳細なアルゴリズムに従ってHTMLドキュメントを解析し、DOMツリーを構築します。このアルゴリズムには、様々な「挿入モード(insertion mode)」や「トークナイザーの状態(tokenizer states)」があり、現在のコンテキストに基づいて次のトークンをどのように解釈するかを決定します。
-
生テキスト要素 (Raw Text Elements): HTMLには、その内容が通常のHTMLとしてパースされない特殊な要素が存在します。これらは「生テキスト要素」と呼ばれ、
<script>
,<style>
,<textarea>
,<title>
,<noembed>
,<noframes>
,<noscript>
,<plaintext>
,<xmp>
などが含まれます。これらの要素の内部では、終了タグが見つかるまで、すべての文字が生のテキストデータとして扱われます。例えば、<script>var a = "<b>test</b>";</script>
の<b>
はタグとして解釈されず、単なる文字列の一部として扱われます。 -
トークナイザー (Tokenizer): HTMLパースの最初の段階はトークナイザーです。トークナイザーは、入力されたHTML文字列を、タグ、属性、テキストデータなどの意味のある「トークン」のストリームに変換します。生テキスト要素の場合、トークナイザーは特殊な状態に入り、終了タグ以外のすべての文字をテキストトークンとして出力します。
-
パーサー (Parser): トークナイザーによって生成されたトークンのストリームは、パーサーに渡されます。パーサーはこれらのトークンを使用して、DOMツリーを構築します。パーサーは、現在の挿入モードに基づいて、どの要素が許可され、どのようにネストされるべきかを決定します。生テキスト要素の場合、パーサーはトークナイザーが生テキストモードになっていることを認識し、その要素の終了タグが見つかるまで、子ノードとしてテキストノードのみを受け入れます。
-
tests5.dat
: Go言語のhtml
パッケージのテストスイートには、WebKitプロジェクトから派生したHTMLパースのテストデータが含まれています。tests5.dat
はそのうちの一つで、様々なエッジケースや特殊なHTML構造のパースを検証するために使用されます。このコミットでtests5.dat
のテストがパスするようになったということは、パーサーがより多くの標準的なHTML構造を正しく処理できるようになったことを意味します。
技術的詳細
このコミットは、Go言語の html
パッケージ内の以下の主要なコンポーネントに変更を加えています。
-
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ツリーに追加します。
-
src/pkg/html/render.go
(レンダラー):render1
関数は、DOMノードをHTML文字列にレンダリングする際に使用されます。switch n.Data
文に"xmp"
が追加されました。これは、<xmp>
が<iframe
,noembed
,noframes
,noscript
,plaintext
,script
,style
と同様に「生テキスト要素」として扱われることを意味します。- 生テキスト要素の場合、その子ノードは
TextNode
であることが期待されます。もし非テキストの子ノードが見つかった場合、エラーが返されます。これは、<xmp>
の内部コンテンツがHTMLとして再パースされるべきではないというルールを強制します。
-
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'
(forxmp
) が追加されました。これにより、タグ名の最初の文字が'x'
の場合も特殊なタグとして考慮されるようになります。switch s := strings.ToLower(string(z.buf[z.data.start:z.data.end]))
文に"xmp"
が追加されました。これにより、トークナイザーは<xmp>
を認識し、z.rawTag
を"xmp"
に設定します。z.rawTag
が設定されると、トークナイザーは生テキストモードに切り替わり、対応する終了タグが見つかるまで、すべての文字をテキストデータとして扱います。
-
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>
要素がパースされるとframesetOK
はfalse
に設定されますが、<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>
のような特殊な要素を正確に処理できるようにするために不可欠です。
関連リンク
- HTML5仕様 - 12.2.5.1 "The rules for parsing tokens in HTML" (Raw text elements): https://html.spec.whatwg.org/multipage/parsing.html#raw-text-elements
- HTML5仕様 - 12.2.6.4.5 "The "in body" insertion mode": https://html.spec.whatwg.org/multipage/parsing.html#the-in-body-insertion-mode
- Go言語の
html
パッケージのドキュメント: https://pkg.go.dev/golang.org/x/net/html (Go 1.0以降はgolang.org/x/net/html
に移動) - Gerrit Change-ID for this commit: https://golang.org/cl/5440062
参考にした情報源リンク
- HTML
<xmp>
tag: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/xmp - HTML
<pre>
tag: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/pre - HTML parsing: https://www.html5rocks.com/en/tutorials/internals/howbrowserswork/#The_parsing_algorithm
- WebKit HTML Test Suite: https://github.com/WebKit/WebKit/tree/main/Source/WebCore/html/parser/tests (Goの
html
パッケージのテストデータの一部はWebKitから派生しています)