[インデックス 10087] ファイルの概要
このコミットは、Go言語の標準ライブラリである html
パッケージにおけるHTMLパーシングロジックの改善に関するものです。特に、リスト要素 (<li>
) と非順序リスト (<ul>
) の閉じタグの挙動を、HTML5の仕様に準拠するように修正しています。これにより、不正なHTML構造が与えられた場合でも、より正確なDOMツリーが構築されるようになります。
コミット
commit 05ed18f4f6c661bfe01db0d8c25e5d7b65658a54
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Wed Oct 26 14:02:30 2011 +1100
html: improve parsing of lists
Make a <li> tag close the previous <li> element.
Make a </ul> tag close <li> elements.
Pass tests1.dat, test 33:
<!DOCTYPE html><li>hello<li>world<ul>how<li>do</ul>you</body><!--do-->
| <!DOCTYPE html>
| <html>
| <head>
| <body>
| <li>
| "hello"
| <li>
| "world"
| <ul>
| "how"
| <li>
| "do"
| "you"
| <!-- do -->
R=nigeltao
CC=golang-dev
https://golang.org/cl/5321051
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/05ed18f4f6c661bfe01db0d8c25e5d7b65658a54
元コミット内容
このコミットは、Go言語の html
パッケージにおけるHTMLパーサーのリスト要素(<li>
)と非順序リスト(<ul>
)の処理を改善することを目的としています。具体的には、以下の2つの主要な変更が含まれています。
<li>
タグが前の<li>
要素を閉じるようにする: HTMLの仕様では、新しい<li>
タグが出現した場合、明示的に閉じられていない前の<li>
要素は自動的に閉じられるべきです。この変更は、この挙動をパーサーに実装します。</ul>
タグが<li>
要素を閉じるようにする:</ul>
(または<ol>
)の閉じタグが出現した場合、その中に含まれる開いたままの<li>
要素はすべて閉じられるべきです。この変更は、この規則をパーサーに適用します。
これらの変更により、<!DOCTYPE html><li>hello<li>world<ul>how<li>do</ul>you</body><!--do-->
のような、一部のタグが明示的に閉じられていないHTMLスニペットが、ブラウザの挙動により近づく形で正しくパースされるようになります。コミットメッセージには、このテストケースの期待されるDOM構造も示されています。
変更の背景
HTMLは非常に寛容な言語であり、多くのウェブページは厳密なXMLのような構造を持っていません。ブラウザは、不正なマークアップや省略されたタグに対しても、エラーを発生させることなく、一貫した方法でDOMツリーを構築する「エラー回復」メカニズムを持っています。このエラー回復の挙動は、HTML5の仕様で詳細に定義されており、すべてのHTMLパーサーはこれに準拠することが求められます。
このコミットが行われた2011年当時、Go言語の html
パッケージはまだ初期段階にあり、HTML5の複雑なパーシングルールを完全に実装しているわけではありませんでした。特に、リスト要素のような特定の要素は、その性質上、暗黙的な閉じタグの挙動が頻繁に発生します。例えば、<li>
タグは、別の <li>
タグや親リスト要素の閉じタグによって自動的に閉じられることが期待されます。
このコミットの背景には、このようなHTML5のパーシング仕様、特にリスト要素の暗黙的な閉じタグのルールにGoの html
パーサーをより厳密に準拠させるという目的があります。これにより、GoでHTMLをパースする際に、主要なブラウザと同じDOMツリーが生成されるようになり、ウェブスクレイピングやHTML処理の信頼性が向上します。
前提知識の解説
このコミットを理解するためには、以下のHTMLパーシングに関する前提知識が必要です。
-
HTML5パーシングアルゴリズム: HTML5の仕様は、ブラウザがHTMLドキュメントをどのようにパースし、DOMツリーを構築するかを詳細に定義しています。これは非常に複雑なステートマシンであり、様々な「挿入モード (Insertion Mode)」と「要素のスタック (Stack of Open Elements)」を管理しながらトークンを処理します。
- トークン化 (Tokenization): 入力されたHTML文字列を、タグ、属性、テキストなどの「トークン」に分解するプロセスです。
- ツリー構築 (Tree Construction): トークナイザーから受け取ったトークンに基づいて、DOMツリーを構築するプロセスです。
- 挿入モード (Insertion Mode): パーサーが現在どのHTML要素のコンテキストで動作しているかを示す状態です。例えば、
in body
モードは<body>
要素の内部でコンテンツを処理している状態を指します。各モードは、特定のトークンが検出されたときにどのようにDOMツリーを操作するかを決定します。 - 要素のスタック (Stack of Open Elements): 現在開いているHTML要素を追跡するためのスタックデータ構造です。開始タグが検出されると要素がスタックにプッシュされ、終了タグが検出されると対応する要素がポップされます。HTMLの柔軟性のため、このスタックの操作は単純なプッシュ/ポップだけではありません。
- 暗黙的な閉じタグ (Implicit Closures): HTMLでは、特定の要素が明示的に閉じられていなくても、別の要素の開始タグや終了タグによって自動的に閉じられることがあります。例えば、
<p>First paragraph<p>Second paragraph
のように書かれた場合、2番目の<p>
タグは最初の<p>
タグを自動的に閉じます。リスト要素 (<li>
) もこの暗黙的な閉じタグの挙動が頻繁に発生する要素の一つです。
-
HTMLのリスト要素の挙動:
<li>
の暗黙的な閉じ: HTML5のパーシングルールでは、in body
挿入モードで<li>
開始タグが検出された場合、要素のスタックを遡り、開いている<li>
要素があればそれを閉じます。これは、<li>
要素が兄弟要素として連続して出現する場合に、前の<li>
が自動的に閉じられることを保証するためです。- リスト要素のスコープ:
<li>
要素は、特定の「スコープ」内で閉じられるべき要素と見なされます。例えば、<ul>
や<ol>
の内部に存在します。</ul>
や<ol>
の終了タグが検出された場合、その内部で開いている<li>
要素はすべて閉じられる必要があります。
-
Go言語の
html
パッケージ: Goのhtml
パパッケージは、HTML5の仕様に準拠したHTMLパーサーを提供します。このパッケージは、ウェブスクレイピング、HTMLテンプレートの処理、HTMLのサニタイズなど、様々な用途で利用されます。内部的には、トークナイザーとツリーコンストラクタの概念に基づいて動作し、HTMLドキュメントをNode
のツリーとして表現します。
技術的詳細
このコミットの技術的詳細は、Go言語の html
パッケージ内の parse.go
ファイルにおける inBodyIM
関数(in body
挿入モードのハンドラ)の変更に集約されます。
inBodyIM
関数における <li>
開始タグの処理
変更前は、<li>
開始タグが検出された際の特別な処理は存在せず、一般的な要素として扱われていました。変更後、inBodyIM
関数内に case "li":
ブロックが追加され、以下のロジックが実装されました。
p.framesetOK = false
:frameset
要素の挿入が許可されないことを示します。これはHTML5パーシングアルゴリズムの標準的な挙動の一部です。- 開いている
<li>
要素の探索と閉じ:
このループは、要素のスタックfor i := len(p.oe) - 1; i >= 0; i-- { node := p.oe[i] switch node.Data { case "li": p.popUntil(listItemScopeStopTags, "li") case "address", "div", "p": continue default: if !isSpecialElement[node.Data] { continue } } break }
p.oe
を逆順(最も最近開かれた要素から)に走査します。- もし
<li>
要素が見つかった場合、p.popUntil(listItemScopeStopTags, "li")
が呼び出されます。これは、listItemScopeStopTags
で定義された要素(例えば、<ul>
,<ol>
,<li>
など)が見つかるか、またはターゲット要素である<li>
が見つかるまで、スタックから要素をポップする関数です。これにより、新しい<li>
が挿入される前に、開いている前の<li>
が適切に閉じられます。 address
,div
,p
のような特定の要素が見つかった場合、それらは<li>
の暗黙的な閉じを妨げないため、ループは続行されます。- その他の「特殊な要素」が見つかった場合、ループはそこで停止します。これは、これらの要素が
<li>
の暗黙的な閉じの境界となるためです。
- もし
p.popUntil(buttonScopeStopTags, "p")
: 上記の<li>
固有の処理の後、p
要素を閉じるための一般的なクリーンアップが行われます。これは、<li>
の前にp
要素が開いている場合に、HTML5のルールに従ってp
を閉じるためのものです。p.addElement("li", p.tok.Attr)
: 最後に、新しい<li>
要素が現在の属性 (p.tok.Attr
) と共にDOMツリーに追加されます。
inBodyIM
関数におけるブロックレベル要素の終了タグの処理
inBodyIM
関数内の終了タグを処理する switch
ステートメントに、新しい case
が追加されました。
case "address", "article", "aside", "blockquote", "button", "center", "details", "dir", "div", "dl", "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "listing", "menu", "nav", "ol", "pre", "section", "summary", "ul":
p.popUntil(defaultScopeStopTags, p.tok.Data)
この変更は、address
, article
, aside
, blockquote
, div
, ul
など、多くのブロックレベル要素の終了タグが検出された場合に適用されます。p.popUntil(defaultScopeStopTags, p.tok.Data)
が呼び出され、これは defaultScopeStopTags
で定義された要素が見つかるか、または現在の終了タグに対応する要素 (p.tok.Data
) が見つかるまで、要素のスタックから要素をポップします。これにより、例えば </ul>
タグが検出された際に、その内部で開いている <li>
要素が適切に閉じられるようになります。
テストファイルの変更
src/pkg/html/parse_test.go
ファイルでは、TestParser
関数のループ回数が i < 33
から i < 34
に変更されています。これは、コミットメッセージで言及されている新しいテストケース(tests1.dat
のテスト33)が追加または有効化されたことを示しています。このテストケースは、<li>
と <ul>
のパーシング改善を検証するために特別に設計されたものです。
コアとなるコードの変更箇所
src/pkg/html/parse.go
--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -576,6 +576,24 @@ func inBodyIM(p *parser) (insertionMode, bool) {
p.framesetOK = false
// TODO: detect <select> inside a table.
return inSelectIM, true
+ case "li":
+ p.framesetOK = false
+ for i := len(p.oe) - 1; i >= 0; i-- {
+ node := p.oe[i]
+ switch node.Data {
+ case "li":
+ p.popUntil(listItemScopeStopTags, "li")
+ case "address", "div", "p":
+ continue
+ default:
+ if !isSpecialElement[node.Data] {
+ continue
+ }
+ }
+ break
+ }
+ p.popUntil(buttonScopeStopTags, "p")
+ p.addElement("li", p.tok.Attr)
default:
// TODO.
p.addElement(p.tok.Data, p.tok.Attr)
@@ -592,6 +610,8 @@ func inBodyIM(p *parser) (insertionMode, bool) {
p.popUntil(buttonScopeStopTags, "p")
case "a", "b", "big", "code", "em", "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u":
p.inBodyEndTagFormatting(p.tok.Data)
+ case "address", "article", "aside", "blockquote", "button", "center", "details", "dir", "div", "dl", "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "listing", "menu", "nav", "ol", "pre", "section", "summary", "ul":
+ p.popUntil(defaultScopeStopTags, p.tok.Data)
default:
p.inBodyEndTagOther(p.tok.Data)
}
src/pkg/html/parse_test.go
--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -132,7 +132,7 @@ func TestParser(t *testing.T) {
rc := make(chan io.Reader)
go readDat(filename, rc)
// TODO(nigeltao): Process all test cases, not just a subset.
- for i := 0; i < 33; i++ {
+ for i := 0; i < 34; i++ {
// Parse the #data section.
b, err := ioutil.ReadAll(<-rc)
if err != nil {
コアとなるコードの解説
src/pkg/html/parse.go
の変更点
-
inBodyIM
関数内のcase "li":
ブロック: この新しいブロックは、HTMLパーサーが<body>
要素の内部で<li>
開始タグを検出したときに実行されるロジックを定義しています。p.framesetOK = false
:frameset
要素の挿入が許可されないことを示すフラグを設定します。これはHTML5パーシングアルゴリズムの標準的なステップです。for i := len(p.oe) - 1; i >= 0; i--
: このループは、パーサーの「開いている要素のスタック (p.oe
)」を逆順に(最も最近開かれた要素から)走査します。case "li": p.popUntil(listItemScopeStopTags, "li")
: もしスタックの途中で別の<li>
要素が見つかった場合、p.popUntil
関数が呼び出されます。この関数は、listItemScopeStopTags
で定義された要素(例えば、<ul>
,<ol>
,<li>
など)が見つかるか、またはターゲット要素である<li>
が見つかるまで、スタックから要素をポップします。これにより、新しい<li>
が挿入される前に、開いている前の<li>
がHTML5のルールに従って自動的に閉じられます。case "address", "div", "p": continue
:address
,div
,p
といった特定の要素は、<li>
の暗黙的な閉じを妨げないため、これらの要素が見つかってもループは続行されます。default: if !isSpecialElement[node.Data] { continue }
: その他の要素については、もしそれが「特殊な要素」(HTML5のパーシングアルゴリズムで特別な扱いを受ける要素)でない場合、ループは続行されます。特殊な要素である場合は、そこでループをbreak
します。これは、これらの要素が<li>
の暗黙的な閉じの境界となるためです。
p.popUntil(buttonScopeStopTags, "p")
:<li>
固有の処理の後、p
要素を閉じるための一般的なクリーンアップが行われます。これは、<li>
の前にp
要素が開いている場合に、HTML5のルールに従ってp
を閉じるためのものです。p.addElement("li", p.tok.Attr)
: 最後に、現在処理中の<li>
開始タグに対応する新しい<li>
要素が、その属性 (p.tok.Attr
) と共にDOMツリーに追加されます。
-
inBodyIM
関数内の終了タグ処理の拡張:inBodyIM
関数内の終了タグを処理するswitch
ステートメントに、新しいcase
が追加されました。case "address", "article", ..., "ul": p.popUntil(defaultScopeStopTags, p.tok.Data)
: この行は、address
,article
,aside
,blockquote
,div
,ul
など、多くのブロックレベル要素の終了タグが検出された場合に適用されます。p.popUntil(defaultScopeStopTags, p.tok.Data)
が呼び出され、これはdefaultScopeStopTags
で定義された要素が見つかるか、または現在の終了タグに対応する要素 (p.tok.Data
) が見つかるまで、要素のスタックから要素をポップします。これにより、例えば</ul>
タグが検出された際に、その内部で開いている<li>
要素が適切に閉じられるようになります。これは、HTML5のパーシングアルゴリズムにおける「特定の要素の終了タグが検出された場合の処理」の一部を実装しています。
src/pkg/html/parse_test.go
の変更点
for i := 0; i < 33; i++
がfor i := 0; i < 34; i++
に変更されました。これは、tests1.dat
ファイルに含まれるテストケースの総数を1つ増やし、コミットメッセージで言及されている新しいテストケース(テスト33)が実行されるようにするためのものです。このテストケースは、<li>
と<ul>
のパーシング改善が正しく機能するかを検証します。
これらの変更により、Goの html
パーサーは、HTML5の複雑なリスト要素のパーシングルールに、より厳密に準拠するようになりました。
関連リンク
- Go言語の
html
パッケージ ドキュメント: https://pkg.go.dev/golang.org/x/net/html (現在のパッケージはgolang.org/x/net/html
に移動しています) - HTML5仕様 - 8.2.5.4.7 The "in body" insertion mode: https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody (特に
li
要素とブロックレベル要素の終了タグの処理に関するセクション) - Go CL 5321051: https://golang.org/cl/5321051 (このコミットに対応するGoのコードレビューシステムのエントリ)
参考にした情報源リンク
- HTML Standard (current version): https://html.spec.whatwg.org/
- Go's HTML parser (golang.org/x/net/html): GoのHTMLパーサーの設計と実装に関する情報は、主にソースコードと関連するGo CL(Change List)から得られます。
- MDN Web Docs - HTML elements: https://developer.mozilla.org/en-US/docs/Web/HTML/Element (HTML要素の一般的な情報と挙動について)
- Stack Overflow / 関連する技術ブログ: HTMLパーシングの複雑な挙動やGoの
html
パッケージに関する議論は、Stack Overflowや技術ブログで多く見られます。
[インデックス 10087] ファイルの概要
このコミットは、Go言語の標準ライブラリである html
パッケージにおけるHTMLパーシングロジックの改善に関するものです。特に、リスト要素 (<li>
) と非順序リスト (<ul>
) の閉じタグの挙動を、HTML5の仕様に準拠するように修正しています。これにより、不正なHTML構造が与えられた場合でも、より正確なDOMツリーが構築されるようになります。
コミット
commit 05ed18f4f6c661bfe01db0d8c25e5d7b65658a54
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Wed Oct 26 14:02:30 2011 +1100
html: improve parsing of lists
Make a <li> tag close the previous <li> element.
Make a </ul> tag close <li> elements.
Pass tests1.dat, test 33:
<!DOCTYPE html><li>hello<li>world<ul>how<li>do</ul>you</body><!--do-->
| <!DOCTYPE html>
| <html>
| <head>
| <body>
| <li>
| "hello"
| <li>
| "world"
| <ul>
| "how"
| <li>
| "do"
| "you"
| <!-- do -->
R=nigeltao
CC=golang-dev
https://golang.org/cl/5321051
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/05ed18f4f6c661bfe01db0d8c25e5d7b65658a54
元コミット内容
このコミットは、Go言語の html
パッケージにおけるHTMLパーサーのリスト要素(<li>
)と非順序リスト(<ul>
)の処理を改善することを目的としています。具体的には、以下の2つの主要な変更が含まれています。
<li>
タグが前の<li>
要素を閉じるようにする: HTMLの仕様では、新しい<li>
タグが出現した場合、明示的に閉じられていない前の<li>
要素は自動的に閉じられるべきです。この変更は、この挙動をパーサーに実装します。</ul>
タグが<li>
要素を閉じるようにする:</ul>
(または<ol>
)の閉じタグが出現した場合、その中に含まれる開いたままの<li>
要素はすべて閉じられるべきです。この変更は、この規則をパーサーに適用します。
これらの変更により、<!DOCTYPE html><li>hello<li>world<ul>how<li>do</ul>you</body><!--do-->
のような、一部のタグが明示的に閉じられていないHTMLスニペットが、ブラウザの挙動により近づく形で正しくパースされるようになります。コミットメッセージには、このテストケースの期待されるDOM構造も示されています。
変更の背景
HTMLは非常に寛容な言語であり、多くのウェブページは厳密なXMLのような構造を持っていません。ブラウザは、不正なマークアップや省略されたタグに対しても、エラーを発生させることなく、一貫した方法でDOMツリーを構築する「エラー回復」メカニズムを持っています。このエラー回復の挙動は、HTML5の仕様で詳細に定義されており、すべてのHTMLパーサーはこれに準拠することが求められます。
このコミットが行われた2011年当時、Go言語の html
パッケージはまだ初期段階にあり、HTML5の複雑なパーシングルールを完全に実装しているわけではありませんでした。特に、リスト要素 (<li>
) のような特定の要素は、その性質上、暗黙的な閉じタグの挙動が頻繁に発生します。例えば、<li>
タグは、別の <li>
タグや親リスト要素の閉じタグによって自動的に閉じられることが期待されます。
このコミットの背景には、このようなHTML5のパーシング仕様、特にリスト要素の暗黙的な閉じタグのルールにGoの html
パーサーをより厳密に準拠させるという目的があります。これにより、GoでHTMLをパースする際に、主要なブラウザと同じDOMツリーが生成されるようになり、ウェブスクレイピングやHTML処理の信頼性が向上します。
前提知識の解説
このコミットを理解するためには、以下のHTMLパーシングに関する前提知識が必要です。
-
HTML5パーシングアルゴリズム: HTML5の仕様は、ブラウザがHTMLドキュメントをどのようにパースし、DOMツリーを構築するかを詳細に定義しています。これは非常に複雑なステートマシンであり、様々な「挿入モード (Insertion Mode)」と「要素のスタック (Stack of Open Elements)」を管理しながらトークンを処理します。
- トークン化 (Tokenization): 入力されたHTML文字列を、タグ、属性、テキストなどの「トークン」に分解するプロセスです。
- ツリー構築 (Tree Construction): トークナイザーから受け取ったトークンに基づいて、DOMツリーを構築するプロセスです。
- 挿入モード (Insertion Mode): パーサーが現在どのHTML要素のコンテキストで動作しているかを示す状態です。例えば、
in body
モードは<body>
要素の内部でコンテンツを処理している状態を指します。各モードは、特定のトークンが検出されたときにどのようにDOMツリーを操作するかを決定します。 - 要素のスタック (Stack of Open Elements): 現在開いているHTML要素を追跡するためのスタックデータ構造です。開始タグが検出されると要素がスタックにプッシュされ、終了タグが検出されると対応する要素がポップされます。HTMLの柔軟性のため、このスタックの操作は単純なプッシュ/ポップだけではありません。
- 暗黙的な閉じタグ (Implicit Closures): HTMLでは、特定の要素が明示的に閉じられていなくても、別の要素の開始タグや終了タグによって自動的に閉じられることがあります。例えば、
<p>First paragraph<p>Second paragraph
のように書かれた場合、2番目の<p>
タグは最初の<p>
タグを自動的に閉じます。リスト要素 (<li>
) もこの暗黙的な閉じタグの挙動が頻繁に発生する要素の一つです。
-
HTMLのリスト要素の挙動:
<li>
の暗黙的な閉じ: HTML5のパーシングルールでは、in body
挿入モードで<li>
開始タグが検出された場合、要素のスタックを遡り、開いている<li>
要素があればそれを閉じます。これは、<li>
要素が兄弟要素として連続して出現する場合に、前の<li>
が自動的に閉じられることを保証するためです。- リスト要素のスコープ:
<li>
要素は、特定の「スコープ」内で閉じられるべき要素と見なされます。例えば、<ul>
や<ol>
の内部に存在します。</ul>
や<ol>
の終了タグが検出された場合、その内部で開いている<li>
要素はすべて閉じられる必要があります。
-
Go言語の
html
パッケージ: Goのhtml
パッケージは、HTML5の仕様に準拠したHTMLパーサーを提供します。このパッケージは、ウェブスクレイピング、HTMLテンプレートの処理、HTMLのサニタイズなど、様々な用途で利用されます。内部的には、トークナイザーとツリーコンストラクタの概念に基づいて動作し、HTMLドキュメントをNode
のツリーとして表現します。
技術的詳細
このコミットの技術的詳細は、Go言語の html
パッケージ内の parse.go
ファイルにおける inBodyIM
関数(in body
挿入モードのハンドラ)の変更に集約されます。
inBodyIM
関数における <li>
開始タグの処理
変更前は、<li>
開始タグが検出された際の特別な処理は存在せず、一般的な要素として扱われていました。変更後、inBodyIM
関数内に case "li":
ブロックが追加され、以下のロジックが実装されました。
p.framesetOK = false
:frameset
要素の挿入が許可されないことを示します。これはHTML5パーシングアルゴリズムの標準的な挙動の一部です。- 開いている
<li>
要素の探索と閉じ:
このループは、要素のスタックfor i := len(p.oe) - 1; i >= 0; i-- { node := p.oe[i] switch node.Data { case "li": p.popUntil(listItemScopeStopTags, "li") case "address", "div", "p": continue default: if !isSpecialElement[node.Data] { continue } } break }
p.oe
を逆順(最も最近開かれた要素から)に走査します。- もし
<li>
要素が見つかった場合、p.popUntil(listItemScopeStopTags, "li")
が呼び出されます。これは、listItemScopeStopTags
で定義された要素(例えば、<ul>
,<ol>
,<li>
など)が見つかるか、またはターゲット要素である<li>
が見つかるまで、スタックから要素をポップする関数です。これにより、新しい<li>
が挿入される前に、開いている前の<li>
が適切に閉じられます。 address
,div
,p
のような特定の要素が見つかった場合、それらは<li>
の暗黙的な閉じを妨げないため、ループは続行されます。- その他の「特殊な要素」が見つかった場合、ループはそこで停止します。これは、これらの要素が
<li>
の暗黙的な閉じの境界となるためです。
- もし
p.popUntil(buttonScopeStopTags, "p")
: 上記の<li>
固有の処理の後、p
要素を閉じるための一般的なクリーンアップが行われます。これは、<li>
の前にp
要素が開いている場合に、HTML5のルールに従ってp
を閉じるためのものです。p.addElement("li", p.tok.Attr)
: 最後に、新しい<li>
要素が現在の属性 (p.tok.Attr
) と共にDOMツリーに追加されます。
inBodyIM
関数におけるブロックレベル要素の終了タグの処理
inBodyIM
関数内の終了タグを処理する switch
ステートメントに、新しい case
が追加されました。
case "address", "article", "aside", "blockquote", "button", "center", "details", "dir", "div", "dl", "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "listing", "menu", "nav", "ol", "pre", "section", "summary", "ul":
p.popUntil(defaultScopeStopTags, p.tok.Data)
この変更は、address
, article
, aside
, blockquote
, div
, ul
など、多くのブロックレベル要素の終了タグが検出された場合に適用されます。p.popUntil(defaultScopeStopTags, p.tok.Data)
が呼び出され、これは defaultScopeStopTags
で定義された要素が見つかるか、または現在の終了タグに対応する要素 (p.tok.Data
) が見つかるまで、要素のスタックから要素をポップします。これにより、例えば </ul>
タグが検出された際に、その内部で開いている <li>
要素が適切に閉じられるようになります。
テストファイルの変更
src/pkg/html/parse_test.go
ファイルでは、TestParser
関数のループ回数が i < 33
から i < 34
に変更されています。これは、コミットメッセージで言及されている新しいテストケース(tests1.dat
のテスト33)が追加または有効化されたことを示しています。このテストケースは、<li>
と <ul>
のパーシング改善を検証するために特別に設計されています。
コアとなるコードの変更箇所
src/pkg/html/parse.go
--- a/src/pkg/html/parse.go
+++ b/src/pkg/html/parse.go
@@ -576,6 +576,24 @@ func inBodyIM(p *parser) (insertionMode, bool) {
p.framesetOK = false
// TODO: detect <select> inside a table.
return inSelectIM, true
+ case "li":
+ p.framesetOK = false
+ for i := len(p.oe) - 1; i >= 0; i-- {
+ node := p.oe[i]
+ switch node.Data {
+ case "li":
+ p.popUntil(listItemScopeStopTags, "li")
+ case "address", "div", "p":
+ continue
+ default:
+ if !isSpecialElement[node.Data] {
+ continue
+ }
+ }
+ break
+ }
+ p.popUntil(buttonScopeStopTags, "p")
+ p.addElement("li", p.tok.Attr)
default:
// TODO.
p.addElement(p.tok.Data, p.tok.Attr)
@@ -592,6 +610,8 @@ func inBodyIM(p *parser) (insertionMode, bool) {
p.popUntil(buttonScopeStopTags, "p")
case "a", "b", "big", "code", "em", "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u":
p.inBodyEndTagFormatting(p.tok.Data)
+ case "address", "article", "aside", "blockquote", "button", "center", "details", "dir", "div", "dl", "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "listing", "menu", "nav", "ol", "pre", "section", "summary", "ul":
+ p.popUntil(defaultScopeStopTags, p.tok.Data)
default:
p.inBodyEndTagOther(p.tok.Data)
}
src/pkg/html/parse_test.go
--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -132,7 +132,7 @@ func TestParser(t *testing.T) {
rc := make(chan io.Reader)
go readDat(filename, rc)
// TODO(nigeltao): Process all test cases, not just a subset.
- for i := 0; i < 33; i++ {
+ for i := 0; i < 34; i++ {
// Parse the #data section.
b, err := ioutil.ReadAll(<-rc)
if err != nil {
コアとなるコードの解説
src/pkg/html/parse.go
の変更点
-
inBodyIM
関数内のcase "li":
ブロック: この新しいブロックは、HTMLパーサーが<body>
要素の内部で<li>
開始タグを検出したときに実行されるロジックを定義しています。p.framesetOK = false
:frameset
要素の挿入が許可されないことを示すフラグを設定します。これはHTML5パーシングアルゴリズムの標準的なステップです。for i := len(p.oe) - 1; i >= 0; i--
: このループは、パーサーの「開いている要素のスタック (p.oe
)」を逆順に(最も最近開かれた要素から)走査します。case "li": p.popUntil(listItemScopeStopTags, "li")
: もしスタックの途中で別の<li>
要素が見つかった場合、p.popUntil
関数が呼び出されます。この関数は、listItemScopeStopTags
で定義された要素(例えば、<ul>
,<ol>
,<li>
など)が見つかるか、またはターゲット要素である<li>
が見つかるまで、スタックから要素をポップします。これにより、新しい<li>
が挿入される前に、開いている前の<li>
がHTML5のルールに従って自動的に閉じられます。case "address", "div", "p": continue
:address
,div
,p
といった特定の要素は、<li>
の暗黙的な閉じを妨げないため、これらの要素が見つかってもループは続行されます。default: if !isSpecialElement[node.Data] { continue }
: その他の要素については、もしそれが「特殊な要素」(HTML5のパーシングアルゴリズムで特別な扱いを受ける要素)でない場合、ループは続行されます。特殊な要素である場合は、そこでループをbreak
します。これは、これらの要素が<li>
の暗黙的な閉じの境界となるためです。
p.popUntil(buttonScopeStopTags, "p")
:<li>
固有の処理の後、p
要素を閉じるための一般的なクリーンアップが行われます。これは、<li>
の前にp
要素が開いている場合に、HTML5のルールに従ってp
を閉じるためのものです。p.addElement("li", p.tok.Attr)
: 最後に、現在処理中の<li>
開始タグに対応する新しい<li>
要素が、その属性 (p.tok.Attr
) と共にDOMツリーに追加されます。
-
inBodyIM
関数内の終了タグ処理の拡張:inBodyIM
関数内の終了タグを処理するswitch
ステートメントに、新しいcase
が追加されました。case "address", "article", ..., "ul": p.popUntil(defaultScopeStopTags, p.tok.Data)
: この行は、address
,article
,aside
,blockquote
,div
,ul
など、多くのブロックレベル要素の終了タグが検出された場合に適用されます。p.popUntil(defaultScopeStopTags, p.tok.Data)
が呼び出され、これはdefaultScopeStopTags
で定義された要素が見つかるか、または現在の終了タグに対応する要素 (p.tok.Data
) が見つかるまで、要素のスタックから要素をポップします。これにより、例えば</ul>
タグが検出された際に、その内部で開いている<li>
要素が適切に閉じられるようになります。これは、HTML5のパーシングアルゴリズムにおける「特定の要素の終了タグが検出された場合の処理」の一部を実装しています。
src/pkg/html/parse_test.go
の変更点
for i := 0; i < 33; i++
がfor i := 0; i < 34; i++
に変更されました。これは、tests1.dat
ファイルに含まれるテストケースの総数を1つ増やし、コミットメッセージで言及されている新しいテストケース(テスト33)が実行されるようにするためのものです。このテストケースは、<li>
と<ul>
のパーシング改善が正しく機能するかを検証します。
これらの変更により、Goの html
パーサーは、HTML5の複雑なリスト要素のパーシングルールに、より厳密に準拠するようになりました。
関連リンク
- Go言語の
html
パッケージ ドキュメント: https://pkg.go.dev/golang.org/x/net/html (現在のパッケージはgolang.org/x/net/html
に移動しています) - HTML5仕様 - 8.2.5.4.7 The "in body" insertion mode: https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody (特に
li
要素とブロックレベル要素の終了タグの処理に関するセクション) - Go CL 5321051: https://golang.org/cl/5321051 (このコミットに対応するGoのコードレビューシステムのエントリ)
参考にした情報源リンク
- HTML Standard (current version): https://html.spec.whatwg.org/
- Go's HTML parser (golang.org/x/net/html): GoのHTMLパーサーの設計と実装に関する情報は、主にソースコードと関連するGo CL(Change List)から得られます。
- MDN Web Docs - HTML elements: https://developer.mozilla.org/en-US/docs/Web/HTML/Element (HTML要素の一般的な情報と挙動について)
- HTML5 parsing algorithm li closing rules: https://www.w3.org/TR/html5/syntax.html#parsing-main-inbody (Web検索で得られたHTML5仕様の関連情報)
- Stack Overflow / 関連する技術ブログ: HTMLパーシングの複雑な挙動やGoの
html
パッケージに関する議論は、Stack Overflowや技術ブログで多く見られます。