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

[インデックス 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ツリー)に変換するプロセスです。このプロセスは大きく分けて以下の段階で構成されます。

  1. トークン化 (Tokenization): 入力されたHTML文字列を、意味のある小さな単位(トークン)に分割する段階です。トークンには、開始タグ(例: <div>)、終了タグ(例: </div>)、テキストコンテンツ(例: Hello, World!)、コメント(例: <!-- comment -->)、DOCTYPE宣言などがあります。この段階で、パーサーはHTMLの構文規則に従って、文字のシーケンスを認識し、対応するトークンに変換します。

  2. ツリー構築 (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つのファイルにあります。

  1. 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に新しいエントリが追加されていますが、これは直接的な修正とは異なり、レンダリングテストの特定のケースを一時的にブラックリストに追加している可能性があります。

  2. 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.endz.data.startは同じ値になり、条件 z.data.end > z.data.start は偽となるため、TextTokenは生成されずに、次のトークン処理へと進みます。

この修正は、HTMLパーサーがHTML5の仕様により厳密に準拠し、不要な空のテキストノードを生成しないようにするために不可欠です。

関連リンク

  • Gerrit Change-ID: https://golang.org/cl/5373043 これはGoプロジェクトでコードレビューに使用されるGerritシステムへのリンクです。このリンクから、このコミットの元の変更提案、レビューコメント、および最終的な承認プロセスを確認できます。

参考にした情報源リンク

  • (この解説の生成において、特定の外部ウェブサイトを直接参照した場合はここに記載します。今回はコミット情報と一般的なHTMLパーシングの知識に基づいており、特定の外部URLは参照していません。)