[インデックス 10172] ファイルの概要
このコミットは、Go言語の標準ライブラリであるhtml
パッケージにおける、HTMLのレンダリングと再パースに関するテストの改善と、関連するドキュメントの明確化を目的としています。特に、「レンダリング後に再パースしても元のツリーと同一になるか」を検証するテストにおいて、特定の「整形式ではない」HTML入力に対するブラックリストの管理方法がリファクタリングされました。これにより、テストの意図がより明確になり、コードの可読性と保守性が向上しています。
コミット
commit 90b76c0f3e3356e17c03baae3e20a4a11c2a6f10
Author: Nigel Tao <nigeltao@golang.org>
Date: Wed Nov 2 09:42:25 2011 +1100
html: refactor the blacklist for the "render and re-parse" test.
R=andybalholm
CC=golang-dev, mikesamuel
https://golang.org/cl/5331056
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/90b76c0f3e3356e17c03baae3e20a4a11c2a6f10
元コミット内容
このコミットの元のメッセージは以下の通りです。
html: refactor the blacklist for the "render and re-parse" test.
R=andybalholm
CC=golang-dev, mikesamuel
https://golang.org/cl/5331056
これは、「レンダリングと再パース」テストのためのブラックリストをリファクタリングしたことを示しています。
変更の背景
Go言語のhtml
パッケージは、HTML5の仕様に準拠したHTMLのパース(解析)とレンダリング(生成)機能を提供します。HTMLのパースは、非常に柔軟なエラー回復メカニズムを持つため、不正なマークアップであっても何らかのDOMツリーを生成します。しかし、その生成されたツリーが「整形式(well-formed)」であるとは限りません。
「レンダリングと再パース」テストは、HTMLツリーを文字列にレンダリングし、その文字列を再度パースした結果のツリーが、元のツリーと同一(または意味的に同等)であることを確認するための重要なテストです。これは、HTMLの「ラウンドトリップ」が正しく機能するかを検証するものです。
しかし、HTML5のパースアルゴリズムは非常に複雑であり、特にエラー回復の挙動によって、パースされたツリーがHTMLの構造的な制約(例えば、<a>
要素の中に別の<a>
要素は入れられない、<table>
の中に直接<a>
要素は入れられないなど)を満たさない「整形式ではない」状態になることがあります。このような「整形式ではない」ツリーをレンダリングし、再度パースすると、元のツリーとは異なるツリーが生成される可能性があります。これは、レンダリングされたHTMLが、HTML5のパースアルゴリズムによって「修正」されるためです。
以前のコードでは、このような特定のテストケース(tests1.dat
の30番と77番)がハードコードでスキップされていました。このコミットの背景には、これらの例外をより明確かつ保守しやすい方法で管理し、なぜそれらがスキップされるのかという理由をコードとドキュメントの両方で明確にする必要があったことが挙げられます。
前提知識の解説
HTMLのパースとDOMツリー
HTMLのパースとは、HTMLのテキストデータを読み込み、それをコンピュータが扱いやすい構造化されたデータ、すなわちDOM(Document Object Model)ツリーに変換するプロセスです。DOMツリーは、HTML文書の論理的な構造を表現するツリー構造であり、各HTML要素、属性、テキストなどがノードとして表現されます。
HTMLのレンダリング
HTMLのレンダリングとは、DOMツリーを元のHTMLテキスト形式に戻すプロセスです。これは、パースの逆の操作であり、ツリー構造をたどってHTMLタグやテキストを文字列として出力します。
「整形式(Well-formed)」HTMLとHTML5のパース
XMLのような厳格なマークアップ言語では、「整形式」とは構文規則に完全に準拠していることを意味し、少しでも違反があればパースエラーとなります。しかし、HTML5のパースアルゴリズムは、ウェブ上の既存の膨大な量の不正なHTMLに対応するため、非常に寛容で堅牢なエラー回復メカニズムを持っています。
このエラー回復メカニズムにより、たとえ不正なマークアップであっても、ブラウザは常に何らかのDOMツリーを構築しようとします。しかし、その結果生成されるDOMツリーが、HTMLの論理的な構造規則(例えば、要素の入れ子のルール)に完全に準拠しているとは限りません。
例えば、HTML5の仕様では、<a>
(アンカー)要素の中に別の<a>
要素を直接入れ子にすることはできません。また、<table>
要素の直接の子として<a>
要素を置くこともできません。しかし、不正なHTMLとして<a href="#"><a>Nested Link</a></a>
や<table><a href="#">Link in Table</a></table>
のようなマークアップがあった場合、HTML5のパースアルゴリズムはエラーを報告するのではなく、これらの要素を「再親付け(reparenting)」して、仕様に準拠した(またはそれに近い)ツリー構造に修正しようとします。この修正の結果、元のマークアップからは想像しにくいツリーが生成されることがあります。
このコミットで言及されている「整形式ではない」ツリーとは、このようなHTML5のパースアルゴリズムによって生成されたツリーのうち、HTMLの構造的な制約(例えば、<a>
要素の入れ子禁止)に違反している状態のものを指します。
「レンダリングと再パース」テストの重要性
このテストは、HTMLパーサーとレンダラーの堅牢性を保証するために不可欠です。もし、レンダリングと再パースの過程でツリーが変化してしまうと、アプリケーションがHTMLを処理する際に予期せぬ挙動を引き起こす可能性があります。例えば、HTMLエディタがHTMLを読み込み、編集し、保存する際に、元の構造が失われたり、意図しない変更が加えられたりする事態を防ぐために重要です。
技術的詳細
このコミットの主要な技術的変更点は、parse_test.go
における「レンダリングと再パース」テストのブラックリストの実装方法です。
以前は、特定のテストケースのインデックス(tests1.dat
の30番と77番)をハードコードでチェックし、スキップしていました。これは、テストデータが変更された場合にインデックスがずれる可能性があり、また、なぜそのテストがスキップされるのかという理由がコードからは直接読み取れないという問題がありました。
このコミットでは、renderTestBlacklist
というmap[string]bool
型の変数を導入しました。このマップのキーは、スキップすべきHTML入力文字列そのものであり、値はtrue
です。これにより、テストコードは入力文字列をキーとしてマップを検索し、ブラックリストに登録されている場合はテストをスキップするようになります。
この変更の利点は以下の通りです。
- 明確性: スキップされるテストケースが、その入力文字列によって直接識別されるため、どの入力が問題を引き起こすのかが一目でわかります。
- 保守性: テストデータが変更されても、ブラックリストのキーが入力文字列であるため、インデックスのずれによる影響を受けません。新しい問題のある入力が見つかった場合も、マップにエントリを追加するだけで済みます。
- 説明性: ブラックリストの定義箇所にコメントを追加することで、なぜその特定の入力がブラックリストに登録されているのかという技術的な理由を詳細に記述できるようになりました。
また、render.go
のRender
関数のコメントが大幅に更新され、HTML5のパースとレンダリングにおける「整形式」の概念がより詳細に説明されています。特に、Parse
関数がエラーを返さなくても「整形式ではない」ツリーを生成する可能性があること、そしてそのようなツリーをレンダリングして再パースしても元のツリーと同一にならない具体的な例(<a>
の入れ子、<table>
内の<a>
の再親付け)が追加されました。これにより、開発者がhtml
パッケージの挙動をより深く理解できるようになっています。
コアとなるコードの変更箇所
src/pkg/html/parse_test.go
--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -160,14 +160,10 @@ func TestParser(t *testing.T) {
t.Errorf("%s test #%d %q, got vs want:\n----\n%s----\n%s----", filename, i, text, got, want)
continue
}
- // Check that rendering and re-parsing results in an identical tree.
- if filename == "tests1.dat" && (i == 30 || i == 77) {
- // Some tests in tests1.dat have such messed-up markup that a correct parse
- // results in a non-conforming tree (one <a> element nested inside another).
- // Therefore when it is rendered and re-parsed, it isn't the same.
- // So we skip rendering on that test.
+ if renderTestBlacklist[text] {
continue
}
+ // Check that rendering and re-parsing results in an identical tree.
pr, pw := io.Pipe()
go func() {
pw.CloseWithError(Render(pw, doc))
@@ -187,3 +183,15 @@ func TestParser(t *testing.T) {
}
}
}
+
+// Some test input result in parse trees are not 'well-formed' despite
+// following the HTML5 recovery algorithms. Rendering and re-parsing such a
+// tree will not result in an exact clone of that tree. We blacklist such
+// inputs from the render test.
+var renderTestBlacklist = map[string]bool{
+ // The second <a> will be reparented to the first <table>'s parent. This
+ // results in an <a> whose parent is an <a>, which is not 'well-formed'.
+ `<a><table><td><a><table></table><a></tr><a></table><b>X</b>C<a>Y`: true,
+ // The second <a> will be reparented, similar to the case above.
+ `<a href="blah">aba<table><a href="foo">br<tr><td></td></tr>x</table>aoe`: true,
+}
src/pkg/html/render.go
--- a/src/pkg/html/render.go
+++ b/src/pkg/html/render.go
@@ -19,17 +19,28 @@ type writer interface {
// Render renders the parse tree n to the given writer.
//
-// For 'well-formed' parse trees, calling Parse on the output of Render will
-// result in a clone of the original tree.
+// Rendering is done on a 'best effort' basis: calling Parse on the output of
+// Render will always result in something similar to the original tree, but it
+// is not necessarily an exact clone unless the original tree was 'well-formed'.
+// 'Well-formed' is not easily specified; the HTML5 specification is
+// complicated.
//
-// 'Well-formed' is not formally specified, but calling Parse on arbitrary
-// input results in a 'well-formed' parse tree if Parse does not return an
-// error. Programmatically constructed trees are typically also 'well-formed',
-// but it is possible to construct a tree that, when rendered and re-parsed,
-// results in a different tree. A simple example is that a solitary text node
-// would become a tree containing <html>, <head> and <body> elements. Another
-// example is that the programmatic equivalent of "a<head>b</head>c" becomes
-// "<html><head><head/><body>abc</body></html>".
+// Calling Parse on arbitrary input typically results in a 'well-formed' parse
+// tree. However, it is possible for Parse to yield a 'badly-formed' parse tree.
+// For example, in a 'well-formed' parse tree, no <a> element is a child of
+// another <a> element: parsing "<a><a>" results in two sibling elements.
+// Similarly, in a 'well-formed' parse tree, no <a> element is a child of a
+// <table> element: parsing "<p><table><a>" results in a <p> with two sibling
+// children; the <a> is reparented to the <table>'s parent. However, calling
+// Parse on "<a><table><a>" does not return an error, but the result has an <a>
+// element with an <a> child, and is therefore not 'well-formed'.
+//
+// Programmatically constructed trees are typically also 'well-formed', but it
+// is possible to construct a tree that looks innocuous but, when rendered and
+// re-parsed, results in a different tree. A simple example is that a solitary
+// text node would become a tree containing <html>, <head> and <body> elements.
+// Another example is that the programmatic equivalent of "a<head>b</head>c"
+// becomes "<html><head><head/><body>abc</body></html>".
func Render(w io.Writer, n *Node) os.Error {
if x, ok := w.(writer); ok {
return render(x, n)
コアとなるコードの解説
parse_test.go
の変更
-
変更前:
if filename == "tests1.dat" && (i == 30 || i == 77) { // Some tests in tests1.dat have such messed-up markup that a correct parse // results in a non-conforming tree (one <a> element nested inside another). // Therefore when it is rendered and re-parsed, it isn't the same. // So we skip rendering on that test. continue }
この部分では、
tests1.dat
という特定のテストファイル内の、インデックス30番と77番のテストケースをハードコードでスキップしていました。コメントには、これらのテストケースが「整形式ではない」ツリーを生成するため、レンダリングと再パースで同一にならないことが説明されています。 -
変更後:
if renderTestBlacklist[text] { continue } // ... var renderTestBlacklist = map[string]bool{ // The second <a> will be reparented to the first <table>'s parent. This // results in an <a> whose parent is an <a>, which is not 'well-formed'. `<a><table><td><a><table></table><a></tr><a></table><b>X</b>C<a>Y`: true, // The second <a> will be reparented, similar to the case above. `<a href="blah">aba<table><a href="foo">br<tr><td></td></tr>x</table>aoe`: true, }
変更後では、
renderTestBlacklist
というマップが導入され、スキップすべきHTML入力文字列そのものがキーとして登録されています。テストコードは、現在のテストの入力文字列(text
変数)がこのマップに存在するかどうかをチェックし、存在すればスキップします。 マップのコメントには、なぜこれらの入力がブラックリストに登録されているのか、具体的な理由が記述されています。例えば、最初の例では、<a>
要素が<table>
要素の子として現れることで、パース時に<a>
が<table>
の親に再親付けされ、結果として<a>
の中に<a>
が入れ子になるという「整形式ではない」ツリーが生成されることが説明されています。
render.go
の変更
-
変更前:
Render
関数のコメントは、「整形式」のパースツリーであれば、レンダリングと再パースで元のツリーのクローンが得られると述べていました。また、「整形式」は形式的に定義されていないが、Parse
がエラーを返さなければ整形式のツリーが得られると示唆していました。 -
変更後:
Render
関数のコメントは、より詳細かつ正確な説明に更新されました。- レンダリングは「ベストエフォート」であり、再パースの結果は元のツリーに「似ている」が、元のツリーが「整形式」でない限り「正確なクローン」であるとは限らないことが明確にされました。
- 「整形式」の定義が難しいこと、HTML5の仕様が複雑であることが強調されています。
Parse
関数がエラーを返さなくても「整形式ではない」ツリーを生成する具体的な例が追加されました。- 例1:
<a><a>
をパースすると、2つの兄弟要素になる(整形式の場合)。しかし、<a>
要素が別の<a>
要素の子になることは整形式ではない。 - 例2:
<table>
要素の子として<a>
要素をパースすると、<a>
は<table>
の親に再親付けされる(整形式の場合)。しかし、<a><table><a>
のような入力はエラーを返さないが、結果として<a>
要素が<a>
の子を持つ「整形式ではない」ツリーになる。
- 例1:
- プログラム的に構築されたツリーでも、レンダリングと再パースで異なる結果になる可能性がある例(単一のテキストノードが
<html>
,<head>
,<body>
を持つツリーになる、a<head>b</head>c
が<html><head><head/><body>abc</body></html>
になる)が引き続き説明されています。
これらの変更により、html
パッケージのユーザーは、HTMLのパースとレンダリングの複雑さ、特に「整形式」の概念と、それがラウンドトリップテストに与える影響について、より深い理解を得られるようになりました。
関連リンク
- Go言語の
html
パッケージのドキュメント: https://pkg.go.dev/golang.org/x/net/html (現在のパッケージはgolang.org/x/net/html
に移動しています) - このコミットが参照しているGoの変更リスト (CL): https://golang.org/cl/5331056
参考にした情報源リンク
- HTML5仕様 (W3C勧告): https://www.w3.org/TR/html5/ (特にパースアルゴリズムに関する章)
- HTMLのDOM(Document Object Model)に関する一般的な情報
- Go言語のテストとマップの利用に関する一般的な情報