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

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

このコミットは、Go言語の標準ライブラリである html パッケージにおけるHTMLパーサーの挙動を修正し、特に <pre><listing><textarea> 要素の先頭にある改行文字の扱いをHTMLの仕様に準拠させるための変更です。これにより、これらの要素内のコンテンツがブラウザのレンダリングと一致するように調整されます。

コミット

commit af081cd43ee3a69f89c5a00ab830111cae99d94a
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Thu Nov 24 13:15:09 2011 +1100

    html: ingore newline at the start of a <pre> block
    
    Pass tests3.dat, test 4:
    <!DOCTYPE html><html><head></head><body><pre>\n</pre></body></html>
    
    | <!DOCTYPE html>
    | <html>
    |   <head>
    |   <body>
    |     <pre>
    
    Also pass tests through test 11:
    <!DOCTYPE html><pre>&#x0a;&#x0a;A</pre>
    
    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/5437051

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

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

元コミット内容

このコミットの目的は、<pre> ブロックの先頭にある改行を無視することです。これにより、tests3.dat のテスト4(<!DOCTYPE html><html><head></head><body><pre>\n</pre></body></html>)がパスするようになります。また、テスト11までの他のテストもパスするようになります。

変更の背景

HTMLの <pre> (preformatted text) 要素は、その内部のテキストを整形済みテキストとして表示します。つまり、空白文字(スペース、タブ、改行)がそのまま表示される特性を持っています。しかし、HTMLの仕様では、<pre> 要素の直後に改行文字がある場合、その改行は無視されるという特殊なルールが存在します。これは、HTMLソースコードの可読性を高めるために、開発者が開始タグの直後に改行を入れても、それが余分な空白としてレンダリングされないようにするためです。

Go言語の html パッケージは、HTMLのパースとレンダリングを行うためのライブラリであり、ウェブブラウザの挙動を正確に模倣することが求められます。このコミット以前は、GoのHTMLパーサーがこの「<pre> 要素直後の改行無視」ルールを適切に実装していなかったため、一部のHTMLドキュメントがブラウザと異なる形でパース・レンダリングされる可能性がありました。特に、WebKitベースのテストデータ (tests3.dat) でこの問題が顕在化し、テストが失敗していました。

この変更は、Goの html パッケージがより標準に準拠し、ウェブコンテンツの正確な処理を保証するために不可欠でした。

前提知識の解説

HTMLの <pre> 要素

<pre> 要素は、HTML文書内で整形済みのテキストを表示するために使用されます。この要素内のテキストは、通常、等幅フォントで表示され、空白文字(スペース、タブ、改行)がそのままの形で保持されます。これにより、コードスニペット、アスキーアート、またはその他の整形済みテキストをウェブページに表示する際に便利です。

<pre> 要素と改行の特殊な扱い

HTMLの仕様(特にHTML5のパースアルゴリズム)では、<pre><listing><textarea> 要素の開始タグの直後に改行文字(LF: \n または CR+LF: \r\n)がある場合、その改行文字は要素のコンテンツとしては扱われず、無視されるという特殊なルールがあります。

例:

<pre>
Hello World
</pre>

この場合、<pre> タグの直後の改行は無視され、レンダリング結果は「Hello World」が先頭から始まる形になります。もしこのルールがなければ、先頭に余分な改行が入ってしまいます。

Go言語の html パッケージ

src/pkg/html (現在の golang.org/x/net/html) は、Go言語でHTMLドキュメントをパースし、DOMツリーを構築するためのパッケージです。また、DOMツリーをHTML文字列にレンダリングする機能も提供します。このパッケージは、ウェブスクレイピング、HTMLテンプレート処理、HTMLのサニタイズなど、様々な用途で利用されます。

HTMLパーサーのステートマシン

HTMLのパースは、複雑なステートマシンによって行われます。入力ストリームから文字を読み込み、現在の状態と読み込んだ文字に基づいて次の状態に遷移し、トークンを生成します。このトークンがDOMツリーの構築に使用されます。<pre> のような特殊な要素の処理は、このステートマシン内で特定のルールとして組み込まれています。

技術的詳細

このコミットは、Go言語の html パッケージ内の以下の3つのファイルに影響を与えています。

  1. src/pkg/html/parse.go: HTMLのパースロジックを定義するファイル。
  2. src/pkg/html/parse_test.go: パーサーのテストケースを定義するファイル。
  3. src/pkg/html/render.go: DOMツリーをHTML文字列にレンダリングするロジックを定義するファイル。

parse.go の変更点

parse.goinBodyIM 関数(inBody インサーションモード)は、HTMLドキュメントの <body> 要素内のコンテンツをパースする際の主要なロジックを含んでいます。この関数内で、TextToken が処理される際に、現在の要素が <pre><listing>、または <textarea> であるかどうかがチェックされます。

変更前は、これらの要素の先頭にある改行がそのままテキストノードとして追加されていました。変更後は、以下のロジックが追加されました。

		switch n := p.oe.top(); n.Data {
		case "pre", "listing", "textarea":
			if len(n.Child) == 0 {
				// Ignore a newline at the start of a <pre> block.
				d := p.tok.Data
				if d != "" && d[0] == '\r' {
					d = d[1:]
				}
				if d != "" && d[0] == '\n' {
					d = d[1:]
				}
				if d == "" {
					return true
				}
				p.tok.Data = d
			}
		}

このコードブロックは、以下の処理を行います。

  • 現在処理中の要素 (n) が <pre><listing>、または <textarea> であるかを確認します。
  • len(n.Child) == 0 は、その要素がまだ子ノードを持っていない、つまり、その要素のコンテンツの「先頭」である場合にのみこのロジックを適用することを示します。
  • p.tok.Data は現在のテキストトークンのデータです。
  • もしデータが空でなく、最初の文字が \r (キャリッジリターン) であれば、それを削除します。
  • もしデータが空でなく、最初の文字が \n (ラインフィード) であれば、それを削除します。
  • \r\n のシーケンスに対応するため、\r の後に \n が続く場合も正しく処理されます。
  • 改行を削除した結果、テキストトークンが空になった場合 (d == "") は、そのトークンを完全に無視して true を返します。
  • そうでなければ、改行が削除された後のテキストデータ (d) を p.tok.Data に再割り当てし、パースを続行します。

これにより、<pre> などの要素の直後に存在する改行文字が、パース時に適切に無視されるようになります。

parse_test.go の変更点

parse_test.go では、TestParser 関数内の testFiles スライスが更新されています。

--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -152,7 +152,7 @@ func TestParser(t *testing.T) {
 	\t\t{\"doctype01.dat\", -1},\n \t\t{\"tests1.dat\", -1},\n \t\t{\"tests2.dat\", -1},\n-\t\t{\"tests3.dat\", 0},\n+\t\t{\"tests3.dat\", 12},\n \t}\n \tfor _, tf := range testFiles {

tests3.dat の期待される結果が 0 から 12 に変更されています。これは、tests3.dat 内の特定のテストケース(コミットメッセージによるとテスト4)が、この改行無視の変更によって異なるパース結果を生成するようになったことを示しています。具体的には、以前は失敗していたテストが、この変更によってパスするようになったことを意味します。

render.go の変更点

render.gorender1 関数は、DOMノードをHTML文字列に変換する役割を担っています。このファイルには、パース時の改行無視とは逆の、レンダリング時の改行追加ロジックが追加されています。

	// Add initial newline where there is danger of a newline beging ignored.
	if len(n.Child) > 0 && n.Child[0].Type == TextNode && strings.HasPrefix(n.Child[0].Data, "\n") {
		switch n.Data {
		case "pre", "listing", "textarea":
			if err := w.WriteByte('\n'); err != nil {
				return err
			}
		}
	}

このコードブロックは、以下の処理を行います。

  • レンダリング中のノード (n) が子ノードを持ち、その最初の子ノードがテキストノードであり、かつそのテキストノードのデータが改行 (\n) で始まっている場合をチェックします。
  • さらに、そのノードが <pre><listing>、または <textarea> である場合にのみ適用されます。
  • これらの条件が満たされた場合、レンダリング出力に明示的に改行文字 (\n) を追加します。

このレンダリング時の改行追加は、パース時に先頭の改行が無視されたとしても、レンダリング時にその改行が「意図されたもの」として再挿入されることを保証するためのものです。これは、HTMLの仕様において、<pre> 要素のコンテンツが「整形済み」であることを維持しつつ、ブラウザがレンダリングする際の視覚的な整合性を保つための重要な側面です。例えば、ソースコードで <pre>\nfoo</pre> と書かれた場合、パース時には先頭の \n が無視されますが、レンダリング時には foo の前に改行が挿入され、視覚的には foo が次の行から始まるように見えます。このレンダリング側の変更は、その挙動を再現するためのものです。

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

src/pkg/html/parse.go (抜粋)

 func inBodyIM(p *parser) bool {
 	switch p.tok.Type {
 	case TextToken:
+		switch n := p.oe.top(); n.Data {
+		case "pre", "listing", "textarea":
+			if len(n.Child) == 0 {
+				// Ignore a newline at the start of a <pre> block.
+				d := p.tok.Data
+				if d != "" && d[0] == '\r' {
+					d = d[1:]
+				}
+				if d != "" && d[0] == '\n' {
+					d = d[1:]
+				}
+				if d == "" {
+					return true
+				}
+				p.tok.Data = d
+			}
+		}
 		p.reconstructActiveFormattingElements()
 		p.addText(p.tok.Data)
 		p.framesetOK = false

src/pkg/html/render.go (抜粋)

 func render1(w writer, n *Node) error {
 	// ... (既存のコード) ...
 
+	// Add initial newline where there is danger of a newline beging ignored.
+	if len(n.Child) > 0 && n.Child[0].Type == TextNode && strings.HasPrefix(n.Child[0].Data, "\n") {
+		switch n.Data {
+		case "pre", "listing", "textarea":
+			if err := w.WriteByte('\n'); err != nil {
+				return err
+			}
+		}
+	}
+
 	// Render any child nodes.
 	switch n.Data {
 	case "noembed", "noframes", "noscript", "plaintext", "script", "style":

コアとなるコードの解説

parse.go の変更は、HTMLパーサーが <pre><listing><textarea> 要素の開始タグ直後の改行文字を、その要素のコンテンツの一部として扱わないようにするためのものです。これは、HTMLの仕様で定められた「改行無視」ルールを実装しています。具体的には、テキストトークンがこれらの要素の最初の子ノードとして現れる場合、そのトークンの先頭にある \r\n を削除します。これにより、パースされたDOMツリーには余分な改行ノードが含まれなくなります。

一方、render.go の変更は、パース時に無視された改行が、レンダリング時に視覚的に再現されるようにするためのものです。もし <pre> 要素のコンテンツが改行で始まる場合、レンダリング時に明示的に改行文字を書き出すことで、ブラウザがその改行を「整形済みテキストの一部」として表示する挙動を模倣します。これは、パースとレンダリングの両方でHTMLの仕様に準拠し、一貫した挙動を提供するために重要です。

これらの変更は、Goの html パッケージがより堅牢で、標準に準拠したHTML処理を提供するための改善であり、特にウェブコンテンツの正確な表示において重要な役割を果たします。

関連リンク

参考にした情報源リンク

  • HTML Living Standard (WHATWG): https://html.spec.whatwg.org/
  • Go言語の公式ドキュメント: https://go.dev/
  • WebKitのテストデータ (このコミットで参照されている tests3.dat などは、ブラウザの互換性テストのために使用されることが多い): https://github.com/WebKit/WebKit/tree/main/LayoutTests/fast/html (直接のリンクではありませんが、WebKitのテストデータがHTMLパーサーのテストに利用されることの背景情報として)
  • Stack OverflowやMDN Web Docsなどのウェブ開発コミュニティの議論(<pre> 要素の改行に関する一般的な知識)