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

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

このコミットは、Go言語の実験的なHTMLパーサーライブラリである exp/html パッケージ内の parse.go ファイルに対する変更です。parse.go は、HTMLドキュメントを解析し、トークン化されたストリームを処理してDOMツリーを構築する主要なロジックを含んでいます。具体的には、HTMLの構文解析、要素スタックの管理、トークン処理、エラーハンドリングなど、HTMLパーサーの中核的な機能が実装されています。

コミット

  • コミットハッシュ: fa0e9cd2792dd81acc7ddec5bba271d778231ad4
  • Author: Nigel Tao nigeltao@golang.org
  • Date: Tue Aug 21 20:59:02 2012 +1000

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

https://github.com/golang/go/commit/fa0e9cd2792dd81acc7ddec5bba271d778231ad4

元コミット内容

    exp/html: refactor the parser.read method.
    
    R=andybalholm
    CC=golang-dev
    https://golang.org/cl/6463070

変更の背景

このコミットの背景は、exp/html パッケージのパーサーにおける parser.read メソッドのリファクタリングです。コードレビューの議論によると、この変更は特定の機能追加やバグ修正を目的としたものではなく、「Just general clean-up.」(単なる一般的なクリーンアップ)として行われました。

元の実装では、parser.read メソッドがトークナイザーから次のトークンを読み取り、エラーを処理し、CDATAセクションの許可/不許可を制御するロジックをカプセル化していました。しかし、このメソッドは parser.parse メソッド内のループからのみ呼び出されており、そのロジックが非常にシンプルであったため、独立したメソッドとして存在させる必要性が低いと判断された可能性があります。

コードをより直接的で読みやすくするために、parser.read メソッドのロジックを呼び出し元である parser.parse メソッドのループ内にインライン化することで、関数呼び出しのオーバーヘッドを削減し、コードのフローをより明確にすることが意図されたと考えられます。これにより、パーサーの主要なループ内でトークン読み取りと処理のステップが連続して記述され、コードの局所性が向上します。

前提知識の解説

このコミットを理解するためには、以下の前提知識が必要です。

  1. HTMLパーシングの基本:

    • トークン化 (Tokenization): HTMLドキュメントを、タグ、属性、テキストなどの意味のある最小単位(トークン)に分解するプロセス。HTMLパーサーの最初の段階。
    • ツリー構築 (Tree Construction): トークンストリームを処理し、HTMLドキュメントの構造を表すDOM(Document Object Model)ツリーを構築するプロセス。
    • パーサー (Parser): トークン化されたHTMLを読み込み、DOMツリーを構築するロジックを実装したコンポーネント。
    • トークナイザー (Tokenizer): 入力ストリームからトークンを生成するコンポーネント。
    • 要素スタック (Open Elements Stack): HTMLパーシング中に現在開いている要素(タグ)を追跡するために使用されるスタック。これにより、要素のネスト関係が管理される。
  2. CDATAセクション:

    • XMLやHTMLにおいて、マークアップとして解釈されるべきではないテキストブロックを指定するための特殊なセクション。
    • <![CDATA[...]]> の形式で記述され、内部のコンテンツはパーサーによって通常の文字データとして扱われ、マークアップとして解析されない。
    • HTML5の仕様では、CDATAセクションは外部コンテンツ(SVGやMathMLなど)でのみ許可されており、通常のHTMLコンテンツ内では許可されていない。これは、HTMLパーサーがCDATAセクションをどのように扱うべきかを決定する上で重要となる。
  3. Go言語の exp/html パッケージ:

    • Go言語の標準ライブラリの一部ではないが、HTML5の仕様に準拠したHTMLパーサーを提供する実験的なパッケージ。
    • html.Tokenizer はHTMLのトークン化を担当し、html.Parser はトークンストリームを処理してDOMツリーを構築する。
    • Token 構造体は、トークンの種類 (Type)、データ (Data)、属性 (Attr) など、トークンの情報を保持する。
    • ErrorToken は、トークン化中にエラーが発生したことを示すトークンタイプ。
    • io.EOF は、入力の終端(End Of File)を示すエラー。
  4. リファクタリング:

    • ソフトウェアの外部的な振る舞いを変更せずに、内部構造を改善するプロセス。
    • コードの可読性、保守性、効率性を向上させることを目的とする。
    • このコミットでは、parser.read メソッドのインライン化がリファクタリングの一例。

技術的詳細

このコミットの技術的詳細は、parser.read メソッドの削除と、そのロジックが parser.parse メソッドのメインループにインライン化された点に集約されます。

変更前: parser.read メソッドは以下の役割を担っていました。

  1. CDATAセクションの許可制御: p.tokenizer.AllowCDATA(n != nil && n.Namespace != "") を呼び出し、現在の要素スタックのトップにある要素が外部コンテンツ(n.Namespace != "")である場合にのみCDATAセクションを許可していました。これはHTML5の仕様に準拠するための重要なロジックです。
  2. 次のトークンの読み取り: p.tokenizer.Next() を呼び出してトークナイザーに次のトークンを生成させ、p.tok = p.tokenizer.Token() でそのトークンを取得していました。
  3. エラーハンドリング: 読み取ったトークンのタイプが ErrorToken であった場合、p.tokenizer.Err() を呼び出して実際のエラーを取得し、それを返していました。io.EOF は特別なケースとして扱われ、エラーとは見なされませんでした。

parser.parse メソッドは、メインループ内で p.read() を呼び出し、その結果に基づいて処理を進めていました。

変更後: parser.read メソッドは完全に削除されました。その代わりに、parser.read が行っていたすべてのロジックが parser.parse メソッドの for ループ内に直接記述されました。

具体的には、parser.parse メソッドのループの先頭で、以下の処理が直接実行されるようになりました。

  1. n := p.oe.top() で現在の要素スタックのトップ要素を取得。
  2. p.tokenizer.AllowCDATA(n != nil && n.Namespace != "") でCDATAセクションの許可を制御。
  3. p.tokenizer.Next() で次のトークンを読み込み。
  4. p.tok = p.tokenizer.Token() でトークンを取得。
  5. if p.tok.Type == ErrorToken ブロック内でエラーハンドリングを実行。ここでも p.tokenizer.Err() を呼び出し、io.EOF 以外のエラーであれば即座に return err しています。

この変更により、parser.read メソッドへの関数呼び出しが不要になり、コードの実行パスがより直接的になりました。また、トークン読み取りとエラー処理のロジックが parse メソッドのコンテキスト内に直接配置されることで、コードの局所性が高まり、parse メソッドの動作を理解するために別のメソッド定義を参照する必要がなくなりました。これは、コードの可読性と保守性の向上に寄与します。

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

--- a/src/pkg/exp/html/parse.go
+++ b/src/pkg/exp/html/parse.go
@@ -398,20 +398,6 @@ func (p *parser) reconstructActiveFormattingElements() {
 	}\n}\n\n-// read reads the next token from the tokenizer.\n-func (p *parser) read() error {\n-\t// CDATA sections are allowed only in foreign content.\n-\tn := p.oe.top()\n-\tp.tokenizer.AllowCDATA(n != nil && n.Namespace != "")\n-\n-\tp.tokenizer.Next()\n-\tp.tok = p.tokenizer.Token()\n-\tif p.tok.Type == ErrorToken {\n-\t\treturn p.tokenizer.Err()\n-\t}\n-\treturn nil\n-}\n-\n // Section 12.2.4.\n func (p *parser) acknowledgeSelfClosingTag() {\n \tp.hasSelfClosingToken = false\n@@ -2014,9 +2000,17 @@ func (p *parser) parse() error {\n \t// Iterate until EOF. Any other error will cause an early return.\n \tvar err error\n \tfor err != io.EOF {\n-\t\terr = p.read()\n-\t\tif err != nil && err != io.EOF {\n-\t\t\treturn err\n+\t\t// CDATA sections are allowed only in foreign content.\n+\t\tn := p.oe.top()\n+\t\tp.tokenizer.AllowCDATA(n != nil && n.Namespace != "")\n+\t\t// Read and parse the next token.\n+\t\tp.tokenizer.Next()\n+\t\tp.tok = p.tokenizer.Token()\n+\t\tif p.tok.Type == ErrorToken {\n+\t\t\terr = p.tokenizer.Err()\n+\t\t\tif err != nil && err != io.EOF {\n+\t\t\t\treturn err\n+\t\t\t}\n \t\t}\n \t\tp.parseCurrentToken()\n \t}\n```

## コアとなるコードの解説

このコミットのコアとなる変更は、`parser.read` メソッドの削除と、その機能が `parser.parse` メソッドのメインループに直接組み込まれたことです。

1.  **`parser.read` メソッドの削除**:
    *   元のコードでは、`func (p *parser) read() error` というメソッドが存在し、トークナイザーから次のトークンを読み取り、CDATAセクションの許可を制御し、エラーを処理していました。
    *   このコミットでは、このメソッドが完全に削除されています。これにより、コードベースから一つの関数が減り、全体的な構造が簡素化されました。

2.  **`parser.parse` メソッドへのロジックのインライン化**:
    *   `parser.parse` メソッドは、HTMLドキュメントの解析を制御する主要なループを含んでいます。
    *   変更前は、このループ内で `err = p.read()` を呼び出して次のトークンを取得していました。
    *   変更後、`p.read()` の内部ロジック(CDATAセクションの許可制御、トークンの読み取り、エラーハンドリング)が、`parser.parse` メソッドの `for` ループの先頭に直接コピー&ペーストされました。

    具体的には、以下の行が `for err != io.EOF` ループの内部に移動しました。
    ```go
    // CDATA sections are allowed only in foreign content.
    n := p.oe.top()
    p.tokenizer.AllowCDATA(n != nil && n.Namespace != "")
    // Read and parse the next token.
    p.tokenizer.Next()
    p.tok = p.tokenizer.Token()
    if p.tok.Type == ErrorToken {
        err = p.tokenizer.Err()
        if err != nil && err != io.EOF {
            return err
        }
    }
    ```
    この変更により、`parser.parse` メソッドは、トークンの読み取りと処理の全体的なフローを単一の場所で管理するようになりました。これは、コードの局所性を高め、特定のトークンがどのように読み取られ、処理されるかを理解するために複数の関数定義を追跡する必要をなくすため、可読性の向上に貢献します。また、関数呼び出しのオーバーヘッドがなくなるため、わずかながらパフォーマンスの改善にもつながる可能性があります。

## 関連リンク

*   Go Code Review: `exp/html: refactor the parser.read method.` - [https://golang.org/cl/6463070](https://golang.org/cl/6463070)

## 参考にした情報源リンク

*   GitHub Commit: `fa0e9cd2792dd81acc7ddec5bba271d778231ad4` - [https://github.com/golang/go/commit/fa0e9cd2792dd81acc7ddec5bba271d778231ad4](https://github.com/golang/go/commit/fa0e9cd2792dd81acc7ddec5bba271d778231ad4)
*   Go Code Review: `exp/html: refactor the parser.read method.` - [https://golang.org/cl/6463070](https://golang.org/cl/6463070)
*   HTML5仕様 (W3C): [https://www.w3.org/TR/html5/](https://www.w3.org/TR/html5/) (特にパーシングに関するセクション)
*   Go言語の `html` パッケージドキュメント (もしあれば、`exp/html` の後継): [https://pkg.go.dev/golang.org/x/net/html](https://pkg.go.dev/golang.org/x/net/html) (現在の `html` パッケージは `x/net/html` にあります)