[インデックス 12913] ファイルの概要
このコミットは、Go言語の実験的なHTMLパーサーパッケージ exp/html
において、パーサーに parseImpliedToken
メソッドを追加するものです。この変更により、暗黙的に挿入されるトークン(例えば、HTMLの仕様に基づいて自動的に補完される <html>
や <head>
タグなど)の処理が明示的になり、関連するロジックの重複が解消されます。
コミット
commit c88ca5906cbf022e5b63bddae44c0722054466c9
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Thu Apr 19 11:48:17 2012 +1000
exp/html: add parseImpliedToken method to parser
This method will allow us to be explicit about what we're doing when
we insert an implied token, and avoid repeating the logic involved in
multiple places.
R=nigeltao
CC=golang-dev
https://golang.org/cl/6060048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c88ca5906cbf022e5b63bddae44c0722054466c9
元コミット内容
このコミットの目的は、Go言語の exp/html
パッケージのパーサーに parseImpliedToken
という新しいメソッドを導入することです。このメソッドの導入により、HTMLドキュメントのパース中に暗黙的に(つまり、ソースコードには明示的に存在しないが、HTMLの仕様に基づいてパーサーが自動的に生成する)トークンを挿入する際の処理がより明確になります。また、これまで複数の場所で重複していた同様のロジックをこの新しいメソッドに集約することで、コードの重複を避け、保守性を向上させることを目指しています。
変更の背景
HTMLのパースは、単にテキストを読み込むだけでなく、HTML5の仕様に厳密に従って、欠落している要素を補完したり、不正なマークアップを修正したりする複雑なプロセスを含みます。特に、<html>
、<head>
、<body>
といった要素は、たとえHTMLソースに明示的に記述されていなくても、パーサーによって暗黙的に生成されることがあります。
このコミット以前の exp/html
パーサーでは、このような暗黙的なトークンの挿入ロジックが、パーサーの異なる状態(例えば、beforeHTMLIM
や beforeHeadIM
といった、HTMLの特定の部分をパースしている状態を表す関数)に散在していました。これにより、同じような処理が何度も記述され、コードの重複と理解のしにくさを招いていました。
parseImpliedToken
メソッドの導入は、この問題を解決するためのものです。このメソッドは、暗黙的なトークンを生成し、それをあたかも入力ストリームから読み込まれたかのようにパーサーに処理させる一元的なメカニズムを提供します。これにより、パーサーの各状態関数は、暗黙的なトークンを挿入する必要がある場合に、この共通のメソッドを呼び出すだけでよくなり、コードの重複が解消され、パーサーのロジックがより明確で理解しやすくなります。
前提知識の解説
HTMLパーシング
HTMLパーシングとは、HTMLドキュメントのテキストデータを読み込み、それをブラウザがレンダリングできるような構造化された表現(通常はDOMツリー)に変換するプロセスです。HTMLは非常に寛容な言語であり、多くのブラウザは不正なマークアップに対してもエラーを出さずに表示しようとします。このため、HTMLパーサーは単なる構文解析器ではなく、エラー回復や欠落要素の補完といった複雑なロジックを内包しています。
HTML5パーシングアルゴリズム
HTML5の仕様は、ブラウザがHTMLドキュメントをどのようにパースすべきかについて、非常に詳細なアルゴリズムを定義しています。このアルゴリズムは、トークン化フェーズとツリー構築フェーズの2つの主要なフェーズに分かれています。
- トークン化フェーズ (Tokenization Phase): 入力ストリームを読み込み、個々のトークン(開始タグ、終了タグ、テキスト、コメントなど)に分解します。
- ツリー構築フェーズ (Tree Construction Phase): トークン化フェーズで生成されたトークンを受け取り、それらを使用してDOMツリーを構築します。このフェーズでは、パーサーは特定の状態機械に従って動作し、現在の状態と入力トークンに基づいて次のアクションを決定します。
暗黙的なトークン (Implied Tokens)
HTML5のパーシングアルゴリズムでは、特定の状況下で、HTMLソースコードには明示的に存在しないが、仕様上存在するとみなされるべき要素(タグ)をパーサーが「暗黙的に」生成し、DOMツリーに挿入することがあります。これらが「暗黙的なトークン」です。
例えば、多くのHTMLドキュメントでは、ルート要素である <html>
タグや、メタデータを含む <head>
タグ、コンテンツを含む <body>
タグが省略されることがあります。しかし、ブラウザはこれらの要素が常に存在することを前提としてDOMツリーを構築します。したがって、パーサーはこれらのタグが欠落している場合、自動的にそれらを生成し、あたかもソースに存在したかのように処理します。
自己終了タグ (Self-Closing Tags)
XMLやXHTMLでは、<br/>
のように要素が内容を持たず、開始タグと終了タグが一体となっている形式を自己終了タグと呼びます。HTML5では、一部の要素(<img>
, <br>
, <input>
など)は内容を持たない「空要素」として定義されており、終了タグを必要としません。HTML5のパーサーは、これらの要素を適切に処理し、DOMツリーに正しく表現する必要があります。
exp/html
パッケージ
exp/html
は、Go言語の標準ライブラリの一部として提供されている html
パッケージの実験的な前身、または関連するパッケージを指す可能性があります。Go言語では、x/
で始まるパス(例: golang.org/x/net/html
)で実験的または補助的なパッケージが提供されることがよくあります。このコミットの文脈では、HTML5の仕様に準拠したパーサーの実装を目指していることが示唆されます。
技術的詳細
このコミットの主要な変更点は、parser
構造体に parseImpliedToken
メソッドを追加し、既存のパーシングロジックを修正してこの新しいメソッドを利用するようにしたことです。
parseImpliedToken
メソッドの追加
// parseImpliedToken parses a token as though it had appeared in the parser's
// input.
func (p *parser) parseImpliedToken(t TokenType, data string, attr []Attribute) {
realToken, selfClosing := p.tok, p.hasSelfClosingToken
p.tok = Token{
Type: t,
Data: data,
Attr: attr,
}
p.hasSelfClosingToken = false
p.parseCurrentToken()
p.tok, p.hasSelfClosingToken = realToken, selfClosing
}
このメソッドは、指定された TokenType
、data
(タグ名など)、および attr
(属性)を持つトークンを、あたかもパーサーの入力ストリームから読み込まれたかのように処理します。
- 現在のトークンの保存:
realToken
とselfClosing
に、現在のパーサーの状態(p.tok
とp.hasSelfClosingToken
)を一時的に保存します。これは、暗黙的なトークンの処理が完了した後に、元のパーサーの状態を復元するためです。 - 暗黙的なトークンの設定:
p.tok
を、引数で渡された暗黙的なトークンの情報で上書きします。p.hasSelfClosingToken
はfalse
に設定されます。これは、暗黙的なトークンが自己終了タグとして扱われるべきではないことを保証するためです。 - トークンのパース:
p.parseCurrentToken()
を呼び出します。この関数は、現在p.tok
に設定されているトークンを、パーサーの現在の状態機械 (p.im
) に従って処理します。 - 元のトークンの復元:
p.tok
とp.hasSelfClosingToken
を、ステップ1で保存した元の値に戻します。これにより、parseImpliedToken
の呼び出し元は、暗黙的なトークンの処理が完了した後も、中断されることなく元のパースを続行できます。
parseCurrentToken
メソッドの追加
// parseCurrentToken runs the current token through the parsing routines
// until it is consumed.
func (p *parser) parseCurrentToken() {
if p.tok.Type == SelfClosingTagToken {
p.hasSelfClosingToken = true
p.tok.Type = StartTagToken
}
consumed := false
for !consumed {
if p.inForeignContent() {
consumed = parseForeignContent(p)
} else {
consumed = p.im(p)
}
}
if p.hasSelfClosingToken {
p.hasSelfClosingToken = false
p.parseImpliedToken(EndTagToken, p.tok.Data, nil)
}
}
このメソッドは、p.tok
に設定されている現在のトークンを、それが完全に消費されるまでパーシングルーチンに通します。
- 自己終了タグの処理: もし現在のトークンが
SelfClosingTagToken
であれば、p.hasSelfClosingToken
をtrue
に設定し、トークンタイプをStartTagToken
に変更します。これは、HTML5のパーシングにおいて、自己終了タグが開始タグとして扱われ、その後に暗黙的な終了タグが続くという挙動を模倣するためです。 - トークンの消費:
for !consumed
ループ内で、トークンが消費されるまでパーシングロジックを実行します。p.inForeignContent()
: 現在のコンテキストがSVGやMathMLのような外部コンテンツ内であるかをチェックします。もしそうであれば、parseForeignContent
が呼び出されます。p.im(p)
: 現在の挿入モード(p.im
)に対応する関数を呼び出します。この関数がトークンを処理し、true
を返せばトークンは消費されたとみなされます。
- 自己終了タグの暗黙的な終了タグ: もし
p.hasSelfClosingToken
がtrue
であれば(つまり、元のトークンが自己終了タグだった場合)、p.hasSelfClosingToken
をfalse
にリセットし、対応する終了タグをparseImpliedToken
を使って暗黙的に挿入します。これにより、自己終了タグが正しくDOMツリーに反映されます。
read()
メソッドの変更
read()
メソッドは、トークナイザーから次のトークンを読み込む役割を担っています。このコミットでは、read()
から自己終了タグの処理ロジックが削除されました。以前は read()
が自己終了タグを StartTagToken
に変換し、p.hasSelfClosingToken
を設定していましたが、このロジックは parseCurrentToken
に移管されました。これにより、read()
は純粋にトークナイザーからの読み込みに専念し、パーシングロジックは parseCurrentToken
と parseImpliedToken
に集約されます。
parse()
メソッドの変更
parse()
メソッドは、パーシングプロセスのメインループです。変更前は、read()
の結果と consumed
フラグに基づいて複雑なループを回していました。変更後は、read()
で次のトークンを読み込み、エラーがなければ p.parseCurrentToken()
を呼び出すというシンプルな構造になりました。これにより、パーシングのフローがより明確になります。
挿入モード関数の変更 (beforeHTMLIM
, beforeHeadIM
)
beforeHTMLIM
や beforeHeadIM
といった挿入モード関数は、HTMLの特定のセクション(例えば、<html>
タグの前や <head>
タグの前)でパーサーがどのように振る舞うかを定義します。これらの関数では、以前は暗黙的な <html>
や <head>
タグを生成するために p.addElement
を直接呼び出していました。
このコミットでは、これらの直接的な p.addElement
の呼び出しが p.parseImpliedToken(StartTagToken, "html", nil)
や p.parseImpliedToken(StartTagToken, "head", nil)
といった形式に置き換えられました。これにより、暗黙的なタグの挿入ロジックが一元化され、各挿入モード関数は parseImpliedToken
を呼び出すだけでよくなりました。
コアとなるコードの変更箇所
src/pkg/exp/html/parse.go
read()
メソッドから自己終了タグの処理ロジックを削除。parseImpliedToken
メソッドを新規追加。parseCurrentToken
メソッドを新規追加。parse()
メソッドのメインループを簡素化し、parseCurrentToken
を呼び出すように変更。beforeHTMLIM
関数内で、暗黙的な<html>
タグの生成をp.parseImpliedToken
を使用するように変更。beforeHeadIM
関数内で、暗黙的な<head>
タグの生成をp.parseImpliedToken
を使用するように変更。
コアとなるコードの解説
このコミットの核心は、HTMLパーサーが暗黙的なトークン(HTMLの仕様に基づいて自動的に補完されるタグなど)を処理する方法を体系化することにあります。
新しい parseImpliedToken
メソッドは、特定のタイプのトークン(例: 開始タグ、終了タグ)を、あたかもそれが実際の入力ストリームから読み込まれたかのようにパーサーに「注入」する役割を担います。これにより、パーサーの異なる部分で散発的に行われていた暗黙的なタグの生成ロジックが、この一箇所に集約されます。このメソッドは、現在のパーサーの状態を一時的に保存し、暗黙的なトークンを処理した後で元の状態に戻すことで、パーシングの連続性を保ちます。
parseCurrentToken
メソッドは、parseImpliedToken
から呼び出される補助的なメソッドであり、現在パーサーが保持しているトークンを、それが完全に消費されるまで処理する責任を持ちます。これには、自己終了タグの特別な処理(開始タグとして扱い、後で暗黙的な終了タグを生成する)や、外部コンテンツ(SVGやMathMLなど)の処理、そして現在の挿入モードに応じたトークンの処理が含まれます。
これらの変更により、パーサーの read()
メソッドは純粋にトークナイザーからの読み込みに特化し、パーシングのメインループである parse()
は read()
と parseCurrentToken
を呼び出すだけのシンプルな構造になります。結果として、HTMLパーシングのロジックはよりモジュール化され、理解しやすく、保守しやすくなりました。特に、HTML5の複雑なエラー回復と要素補完のルールを実装する上で、このような一元化されたアプローチは非常に有効です。
関連リンク
- https://golang.org/cl/6060048
参考にした情報源リンク
- HTML5 Parsing Algorithm: https://html.spec.whatwg.org/multipage/parsing.html
- Go
x/net/html
package documentation (general context for Go's HTML parsing): https://pkg.go.dev/golang.org/x/net/html - Implied tags in HTML parsing: https://www.w3.org/TR/html5/syntax.html#parsing-main-inbody (and related sections on parsing states)
- Self-closing tags in HTML: https://html.spec.whatwg.org/multipage/syntax.html#void-elements
- Go language
exp
packages (general understanding of experimental packages): https://go.dev/doc/go1.1#exp (or similar documentation onx/
packages) - General HTML parsing concepts.I have generated the commit explanation as requested. It is output to standard output.