[インデックス 10324] ファイルの概要
このコミットは、Go言語のhtml
パッケージにおいて、空のRaw Text要素(例: <script></script>
や <title></title>
)に対して不要なテキストトークンが発行されるのを防ぐための修正です。これにより、HTMLパーサーの出力がより正確になり、特定のHTMLテストケースに合格するようになります。
コミット
commit ddc5ec642da599da5b942a174407bcd5ae32c673
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Thu Nov 10 08:09:54 2011 +1100
html: don't emit text token for empty raw text elements.
Pass tests1.dat, test 99:
<script></script></div><title></title><p><p>
| <html>
| <head>
| <script>
| <title>
| <body>
| <p>
| <p>
Also pass tests through test 105:
<ul><li><ul></li><li>a</li></ul></li></ul>
R=nigeltao
CC=golang-dev
https://golang.org/cl/5373043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ddc5ec642da599da5b942a174407bcd5ae32c673
元コミット内容
html: don't emit text token for empty raw text elements.
Pass tests1.dat, test 99:
<script></script></div><title></title><p><p>
| <html>
| <head>
| <script>
| <title>
| <body>
| <p>
| <p>
Also pass tests through test 105:
<ul><li><ul></li><li>a</li></ul></li></ul>
R=nigeltao
CC=golang-dev
https://golang.org/cl/5373043
変更の背景
このコミットの主な目的は、Go言語のhtml
パッケージがHTMLをパースする際に、特定の「Raw Text要素」が空である場合に、不必要な「Textトークン」を生成してしまうバグを修正することです。
HTMLの仕様では、<script>
, <style>
, <textarea>
, <title>
, <noscript>
, <noframes>
といった要素(これらを「Raw Text要素」と呼びます)の内部コンテンツは、通常のHTMLマークアップとしてではなく、純粋なテキストとして扱われます。つまり、これらの要素の開始タグと終了タグの間にある内容は、HTMLエンティティのデコードやネストされたタグの解析なしに、そのままテキストとして読み込まれます。
問題は、これらのRaw Text要素が空である場合、例えば <script></script>
のように、その内部に何もテキストがないにも関わらず、パーサーが「空のテキストトークン」を生成してしまうことでした。これは、パーサーの出力に余分な、意味のないトークンを追加し、後続の処理(例えばDOMツリーの構築やレンダリング)に悪影響を与える可能性がありました。
コミットメッセージに記載されているテストケース tests1.dat, test 99: <script></script></div><title></title><p><p>
は、この問題の典型例を示しています。この入力に対して、パーサーは<script>
と<title>
が空であるにも関わらず、それぞれに対応する空のテキストトークンを生成していたと考えられます。この修正により、これらのテストケースが正しくパスするようになり、パーサーのHTML仕様への準拠性が向上しました。
前提知識の解説
HTMLパーシングの基本
HTMLパーシングとは、HTML文書の文字列を読み込み、それをブラウザが理解できる構造化されたデータ(通常はDOMツリー)に変換するプロセスです。このプロセスは大きく分けて以下の段階で構成されます。
-
トークン化 (Tokenization): 入力されたHTML文字列を、意味のある小さな単位(トークン)に分割する段階です。トークンには、開始タグ(例:
<div>
)、終了タグ(例:</div>
)、テキストコンテンツ(例:Hello, World!
)、コメント(例:<!-- comment -->
)、DOCTYPE宣言などがあります。この段階で、パーサーはHTMLの構文規則に従って、文字のシーケンスを認識し、対応するトークンに変換します。 -
ツリー構築 (Tree Construction): トークン化によって生成されたトークンのストリームを読み込み、それらを使ってDOM(Document Object Model)ツリーを構築する段階です。DOMツリーは、HTML文書の論理的な構造を表現するツリー構造であり、各ノードはHTML要素、テキスト、コメントなどを表します。
Raw Text要素
HTMLには、その内容が特殊な方法で扱われる特定の要素が存在します。これらは「Raw Text要素」と呼ばれ、以下のものが含まれます。
<script>
: JavaScriptコードを埋め込むための要素。<style>
: CSSスタイルシートを埋め込むための要素。<textarea>
: 複数行のプレーンテキスト入力フィールド。<title>
: 文書のタイトルを指定する要素。<noscript>
: スクリプトが無効なブラウザで表示されるコンテンツ。<noframes>
: フレームがサポートされていないブラウザで表示されるコンテンツ。
これらの要素の内部コンテンツは、通常のHTMLパーシングルールとは異なり、マークアップとして解析されません。代わりに、その開始タグから対応する終了タグまでのすべての文字が、純粋なテキストデータとして扱われます。例えば、<script>var a = "<b>test</b>";</script>
の場合、<b>
はHTMLタグとして解釈されず、単なるテキストの一部として扱われます。この特性が、空のRaw Text要素で問題を引き起こす原因となっていました。
Go言語のhtml
パッケージ
Go言語の標準ライブラリには、HTMLの解析と生成を行うためのhtml
パッケージが含まれています。このパッケージは、HTML5の仕様に準拠したパーサーを提供し、ウェブスクレイピング、HTMLテンプレートの処理、HTMLコンテンツのサニタイズなど、様々な用途で利用されます。
html
パッケージの主要なコンポーネントの一つがTokenizer
です。Tokenizer
は、HTML入力ストリームを読み込み、前述のトークン化の段階を実行します。Tokenizer.Next()
メソッドは、入力から次のトークンを読み取り、そのタイプ(TextToken
, StartTagToken
, EndTagToken
など)を返します。
技術的詳細
このコミットは、Go言語のhtml
パッケージ内のTokenizer
が、Raw Text要素のコンテンツを処理する方法に焦点を当てています。具体的には、src/pkg/html/token.go
ファイル内のTokenizer.Next()
メソッドの動作が修正されています。
Tokenizer
は、HTMLのパーシング中に現在の要素がRaw Text要素であると判断した場合、その要素のコンテンツを特殊な方法で読み込みます。これは、z.readRawOrRCDATA()
という内部メソッドによって行われます。このメソッドは、対応する終了タグが見つかるまで、すべての文字をRaw Textとして読み込み、その内容をz.data
フィールドに格納します。
修正前のコードでは、z.readRawOrRCDATA()
が呼び出された後、無条件にz.tt = TextToken
を設定し、TextToken
を返していました。これは、Raw Text要素のコンテンツが空である場合(例: <script></script>
)でも、z.data
が空の文字列を保持したまま、TextToken
が発行されることを意味します。
HTML5のパーシング仕様では、空のテキストノードは通常、DOMツリーには追加されません。したがって、空のRaw Text要素から空のテキストトークンを生成することは、仕様に準拠しておらず、パーサーの出力の正確性を損なうものでした。
このコミットは、z.data
に実際にデータが存在する場合(つまり、z.data.end > z.data.start
)にのみTextToken
を発行するように条件を追加することで、この問題を解決しています。これにより、空のRaw Text要素からはテキストトークンが生成されなくなり、パーサーの出力がよりクリーンで正確になります。
コアとなるコードの変更箇所
このコミットによる変更は、主に以下の2つのファイルにあります。
-
src/pkg/html/parse_test.go
: テストケースの範囲が拡張されています。--- a/src/pkg/html/parse_test.go +++ b/src/pkg/html/parse_test.go @@ -133,7 +133,7 @@ func TestParser(t *testing.T) { t_t := []struct { name string n int }{ // TODO(nigeltao): Process all the test cases from all the .dat files. - {"tests1.dat", 99}, + {"tests1.dat", 106}, {"tests2.dat", 0}, {"tests3.dat", 0}, } @@ -213,4 +213,5 @@ var renderTestBlacklist = map[string]bool{ // More cases of <a> being reparented: `<a href="blah">aba<table><a href="foo">br<tr><td></td></tr>x</table>aoe`: true, `<a><table><a></table><p><a><div><a>`: true, + `<a><table><td><a><table></table><a></tr><a></table><a>`: true, }
TestParser
関数内のtests1.dat
に対するテストケースの実行範囲が99
から106
に拡張されています。これは、この修正がより多くのテストケース(特にRaw Text要素に関連するもの)をパスするようになったことを示しています。また、renderTestBlacklist
に新しいエントリが追加されていますが、これは直接的な修正とは異なり、レンダリングテストの特定のケースを一時的にブラックリストに追加している可能性があります。 -
src/pkg/html/token.go
:Tokenizer.Next()
メソッド内のロジックが変更されています。--- a/src/pkg/html/token.go +++ b/src/pkg/html/token.go @@ -552,8 +552,10 @@ func (z *Tokenizer) Next() TokenType { z.data.end = z.raw.end if z.rawTag != "" { z.readRawOrRCDATA() - z.tt = TextToken - return z.tt + if z.data.end > z.data.start { + z.tt = TextToken + return z.tt + } } z.textIsRaw = false
コアとなるコードの解説
src/pkg/html/token.go
の変更がこのコミットの核心です。
func (z *Tokenizer) Next() TokenType
メソッドは、Tokenizer
の主要なメソッドであり、HTML入力ストリームから次のトークンを読み取り、そのタイプを返します。
変更前のコードは以下のようになっていました。
if z.rawTag != "" { // 現在のタグがRaw Text要素である場合
z.readRawOrRCDATA() // Raw TextまたはRCDATAとしてコンテンツを読み込む
z.tt = TextToken // トークンタイプをTextTokenに設定
return z.tt // TextTokenを返す
}
このロジックでは、z.rawTag
が空でなければ(つまり、現在の要素が<script>
や<title>
などのRaw Text要素であれば)、z.readRawOrRCDATA()
を呼び出してその内容を読み込んだ後、無条件にTextToken
を生成して返していました。
修正後のコードは以下のようになっています。
if z.rawTag != "" { // 現在のタグがRaw Text要素である場合
z.readRawOrRCDATA() // Raw TextまたはRCDATAとしてコンテンツを読み込む
if z.data.end > z.data.start { // 読み込んだデータが空でない場合
z.tt = TextToken // トークンタイプをTextTokenに設定
return z.tt // TextTokenを返す
}
}
追加された if z.data.end > z.data.start
という条件が重要です。
z.data
は、z.readRawOrRCDATA()
によって読み込まれたRaw Textコンテンツのバイト範囲を保持する構造体です。z.data.start
はコンテンツの開始インデックスを、z.data.end
はコンテンツの終了インデックスを示します。z.data.end > z.data.start
という条件は、z.data
が指す範囲に実際にデータが存在するかどうか、つまりRaw Text要素のコンテンツが空ではないかどうかをチェックしています。
この変更により、Raw Text要素のコンテンツが実際に存在する場合にのみTextToken
が発行されるようになります。例えば、<script></script>
のようにコンテンツが空の場合、z.data.end
とz.data.start
は同じ値になり、条件 z.data.end > z.data.start
は偽となるため、TextToken
は生成されずに、次のトークン処理へと進みます。
この修正は、HTMLパーサーがHTML5の仕様により厳密に準拠し、不要な空のテキストノードを生成しないようにするために不可欠です。
関連リンク
- Gerrit Change-ID: https://golang.org/cl/5373043 これはGoプロジェクトでコードレビューに使用されるGerritシステムへのリンクです。このリンクから、このコミットの元の変更提案、レビューコメント、および最終的な承認プロセスを確認できます。
参考にした情報源リンク
- (この解説の生成において、特定の外部ウェブサイトを直接参照した場合はここに記載します。今回はコミット情報と一般的なHTMLパーシングの知識に基づいており、特定の外部URLは参照していません。)