[インデックス 13207] ファイルの概要
このコミットは、Go言語の実験的なHTMLパーサーライブラリであるexp/html
パッケージにおいて、改行コードの処理とキャリッジリターン(\r
)文字のエスケープに関する改善を導入しています。具体的には、トークナイズ(字句解析)の段階で\r
および\r\n
を標準的な\n
に正規化し、HTMLレンダリング時に\r
をHTMLエンティティ
として適切にエスケープするように変更されました。これにより、HTMLの仕様に準拠し、異なるプラットフォームからの入力や、<pre>
タグ内のテキスト表示における互換性と正確性が向上しています。
コミット
commit 4e0749a47805912a528326e3a63e5f0342b19b59
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Wed May 30 15:50:12 2012 +1000
exp/html: Convert \r and \r\n to \n when tokenizing
Also escape "\r" as " " when rendering HTML.
Pass 2 additional tests.
R=nigeltao
CC=golang-dev
https://golang.org/cl/6260046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4e0749a47805912a528326e3a63e5f0342b19b59
元コミット内容
exp/html
パッケージにおいて、トークナイズ時に\r
および\r\n
を\n
に変換する。
また、HTMLレンダリング時に\r
を
としてエスケープする。
これにより、2つの追加テストがパスするようになった。
変更の背景
HTMLの仕様、特にHTML5のパースアルゴリズムでは、入力ストリーム中の改行コードの扱いについて明確なルールが定められています。ウェブコンテンツは様々なオペレーティングシステム(Windows, macOS, Linuxなど)で作成され、それぞれ異なる改行コード(\r\n
, \r
, \n
)を使用する可能性があります。しかし、HTMLパーサーはこれらの違いを吸収し、一貫した内部表現に正規化する必要があります。
このコミット以前のexp/html
パッケージは、トークナイズの段階でこれらの改行コードの正規化が不十分であった可能性があります。特に、\r
(キャリッジリターン)単独、または\r\n
(キャリッジリターンとラインフィード)の組み合わせが、テキストコンテンツや属性値として適切に処理されない場合がありました。これにより、パーサーが生成するDOMツリーが期待通りにならなかったり、レンダリング結果がブラウザの挙動と異なる可能性がありました。
また、HTMLコンテンツを生成する際に、特殊文字をHTMLエンティティとしてエスケープすることは、セキュリティと表示の正確性の両面で重要です。\r
のような制御文字は、そのまま出力されるとブラウザによって無視されたり、予期せぬレイアウトの崩れを引き起こすことがあります。特に<pre>
タグのような整形済みテキストを表示する要素内では、改行コードの正確な表現が不可欠です。このコミットは、\r
をHTMLエンティティ
としてエスケープすることで、この問題を解決し、より堅牢なHTML生成を可能にしています。
これらの変更は、HTMLパーサーの堅牢性を高め、HTML5の仕様への準拠を強化し、異なる環境間での互換性を向上させることを目的としています。
前提知識の解説
HTMLの改行コード
- LF (
\n
, Line Feed): UNIX系システムで主に使われる改行コード。 - CR (
\r
, Carriage Return): 古いMac OSで使われていた改行コード。 - CRLF (
\r\n
, Carriage Return + Line Feed): Windows系システムで主に使われる改行コード。
HTMLの仕様では、これらの異なる改行コードが入力された場合でも、パーサーはそれらを標準的なLF (\n
) に正規化して処理することが推奨されています。これにより、ソースコードの改行コード形式に依存せず、一貫したDOMツリーが構築されます。
HTMLパーサーとトークナイザー
- HTMLパーサー: HTMLドキュメントを読み込み、その構造を解析して、ブラウザがレンダリングできるような内部表現(通常はDOMツリー)に変換するソフトウェアコンポーネントです。
- トークナイザー(字句解析器): パーサーの最初の段階であり、入力されたHTML文字列を、意味を持つ最小単位である「トークン」(例: 開始タグ、終了タグ、属性名、属性値、テキスト、コメントなど)のシーケンスに分割する役割を担います。このコミットの変更は、主にこのトークナイズの段階での改行コード処理に焦点を当てています。
HTMLエンティティ
HTMLエンティティは、HTMLドキュメント内で特殊文字(例: <
、>
、&
、"
)や、キーボードから直接入力できない文字(例: 著作権記号©、登録商標®)を表現するための仕組みです。これらは通常、&
で始まり、セミコロン(;
)で終わる形式(例: <
、&
)か、数値参照(例: "
は"
、
は\r
)で表現されます。
は、キャリッジリターン(\r
)文字の数値文字参照です。HTMLの文脈では、通常\r
は空白文字として扱われるか、無視されることが多いため、明示的にその存在を示す必要がある場合に用いられます。
Go言語の exp/html
パッケージ
exp/html
は、Go言語の標準ライブラリの一部として提供されているHTMLパーサーパッケージです。元々は実験的な(exp
)パッケージとして開発されましたが、後にgolang.org/x/net/html
として独立し、Goのウェブアプリケーション開発において広く利用されています。このパッケージは、HTML5のパースアルゴリズムに準拠することを目指しており、ウェブスクレイピングやHTMLコンテンツの生成・変換などに使用されます。
技術的詳細
このコミットの技術的な核心は、HTMLのトークナイズとレンダリングにおける改行コードの厳密な処理にあります。
-
改行コードの正規化(トークナイズ時): HTML5の仕様では、入力ストリーム中の
\r
および\r\n
のシーケンスは、トークナイズの初期段階で単一の\n
に変換されるべきであると規定されています。これは、異なるOSで作成されたHTMLファイルが、パーサーによって一貫して扱われることを保証するためです。このコミットでは、token.go
にconvertNewlines
関数を導入し、テキストノードの内容や属性値に含まれるこれらの改行コードをインプレースで\n
に変換することで、この仕様に準拠しています。これにより、パーサーの内部で扱われるテキストデータは常にLF (\n
) 形式の改行コードを持つことになります。 -
\r
のエスケープ(レンダリング時): HTMLコンテンツを生成する際、特にテキストとして表示されるべき部分(例:<pre>
タグ内)で\r
文字を保持する必要がある場合、それをHTMLエンティティとしてエスケープすることが重要です。ブラウザは通常、HTMLソース内の\r
を無視するか、空白として扱うため、
として明示的にエスケープすることで、その文字の存在をブラウザに正確に伝えることができます。このコミットでは、escape.go
のescape
関数に\r
のエスケープルールを追加し、
に変換するようにしています。これにより、生成されるHTMLが、元の入力に含まれる\r
文字の意図を正確に反映できるようになります。
これらの変更は、HTMLパーサーがより堅牢になり、異なる入力形式や表示要件に対して正確に対応できるようになることを意味します。特に、<pre>
タグのような整形済みテキストを扱う際に、改行コードの表示が期待通りになることが保証されます。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は以下のファイルに集中しています。
-
src/pkg/exp/html/escape.go
escapedChars
定数に\r
が追加されました。これにより、\r
もエスケープ対象の文字として認識されます。escape
関数内のswitch
文にcase '\r': esc = " "
が追加され、\r
文字がHTMLエンティティ
としてエスケープされるようになりました。
--- a/src/pkg/exp/html/escape.go +++ b/src/pkg/exp/html/escape.go @@ -192,7 +192,7 @@ func lower(b []byte) []byte { return b } -const escapedChars = `&\'<>\"` +const escapedChars = "&\'<>\"\\r" func escape(w writer, s string) error { i := strings.IndexAny(s, escapedChars) @@ -214,6 +214,8 @@ func escape(w writer, s string) error { case '"': // """ is shorter than """. esc = """ + case '\r': + esc = " " default: panic("unrecognized escape character") }
-
src/pkg/exp/html/token.go
- 新しい関数
convertNewlines(s []byte) []byte
が追加されました。この関数は、バイトスライスs
内の\r
および\r\n
をインプレースで\n
に変換します。 Tokenizer.Text()
メソッド内で、テキストコンテンツを返す前にconvertNewlines
が呼び出されるようになりました。Tokenizer.TagAttr()
メソッド内で、属性値を返す前にconvertNewlines
が呼び出されるようになりました。
--- a/src/pkg/exp/html/token.go +++ b/src/pkg/exp/html/token.go @@ -696,6 +696,38 @@ func (z *Tokenizer) Raw() []byte { return z.buf[z.raw.start:z.raw.end] } +// convertNewlines converts "\r" and "\r\n" in s to "\n". +// The conversion happens in place, but the resulting slice may be shorter. +func convertNewlines(s []byte) []byte { + for i, c := range s { + if c != '\r' { + continue + } + + src := i + 1 + if src >= len(s) || s[src] != '\n' { + s[i] = '\n' + continue + } + + dst := i + for src < len(s) { + if s[src] == '\r' { + if src+1 < len(s) && s[src+1] == '\n' { + src++ + } + s[dst] = '\n' + } else { + s[dst] = s[src] + } + src++ + dst++ + } + return s[:dst] + } + return s +} + // Text returns the unescaped text of a text, comment or doctype token. The // contents of the returned slice may change on the next call to Next. func (z *Tokenizer) Text() []byte { @@ -704,6 +736,7 @@ func (z *Tokenizer) Text() []byte { s := z.buf[z.data.start:z.data.end] z.data.start = z.raw.end z.data.end = z.raw.end + s = convertNewlines(s) if !z.textIsRaw { s = unescape(s) } @@ -739,7 +772,7 @@ func (z *Tokenizer) TagAttr() (key, val []byte, moreAttr bool) { z.nAttrReturned++ key = z.buf[x[0].start:x[0].end] val = z.buf[x[1].start:x[1].end] - return lower(key), unescape(val), z.nAttrReturned < len(z.attr) + return lower(key), unescape(convertNewlines(val)), z.nAttrReturned < len(z.attr) } return nil, nil, false
- 新しい関数
-
src/pkg/exp/html/testlogs/plain-text-unsafe.dat.log
- 以前は
FAIL
だった2つのテストケースがPASS
に変更されました。これらのテストは、<pre>
タグ内の\r\n
や\r\r
といった改行シーケンスの処理に関するもので、今回の変更によって正しく扱われるようになったことを示しています。
--- a/src/pkg/exp/html/testlogs/plain-text-unsafe.dat.log +++ b/src/pkg/exp/html/testlogs/plain-text-unsafe.dat.log @@ -21,8 +21,8 @@ PASS "<svg>\x00 </svg><frameset>" FAIL "<svg>\x00a</svg><frameset>" PASS "<svg><path></path></svg><frameset>" PASS "<svg><p><frameset>" -FAIL "<!DOCTYPE html><pre>\r\n\r\nA</pre>" -FAIL "<!DOCTYPE html><pre>\r\rA</pre>" +PASS "<!DOCTYPE html><pre>\r\n\r\nA</pre>" +PASS "<!DOCTYPE html><pre>\r\rA</pre>" PASS "<!DOCTYPE html><pre>\rA</pre>" PASS "<!DOCTYPE html><table><tr><td><math><mtext>\x00a" PASS "<!DOCTYPE html><table><tr><td><svg><foreignObject>\x00a"
- 以前は
-
src/pkg/exp/html/token_test.go
- 新しく追加された
convertNewlines
関数の単体テストであるTestConvertNewlines
が追加されました。様々な改行コードの組み合わせ(\r
,\n
,\r\n
,\r\r
など)が正しく\n
に変換されることを検証しています。
--- a/src/pkg/exp/html/token_test.go +++ b/src/pkg/exp/html/token_test.go @@ -592,6 +592,33 @@ loop: } } +func TestConvertNewlines(t *testing.T) { + testCases := map[string]string{ + "Mac\rDOS\r\nUnix\n": "Mac\nDOS\nUnix\n", + "Unix\nMac\rDOS\r\n": "Unix\nMac\nDOS\n", + "DOS\r\nDOS\r\nDOS\r\n": "DOS\nDOS\nDOS\n", + "": "", + "\n": "\n", + "\n\r": "\n\n", + "\r": "\n", + "\r\n": "\n", + "\r\n\n": "\n\n", + "\r\n\r": "\n\n", + "\r\n\r\n": "\n\n", + "\r\r": "\n\n", + "\r\r\n": "\n\n", + "\r\r\n\n": "\n\n\n", + "\r\r\r\n": "\n\n\n", + "\r \n": "\n \n", + "xyz": "xyz", + } + for in, want := range testCases { + if got := string(convertNewlines([]byte(in))); got != want { + t.Errorf("input %q: got %q, want %q", in, got, want) + } + } +} + const ( rawLevel = iota lowLevel
- 新しく追加された
コアとなるコードの解説
convertNewlines
関数 (src/pkg/exp/html/token.go
)
この関数は、バイトスライスs
を受け取り、その中の\r
および\r\n
のシーケンスを\n
に変換します。変換はインプレースで行われ、結果としてスライスの長さが短くなる可能性があります。
func convertNewlines(s []byte) []byte {
for i, c := range s {
if c != '\r' {
continue // \r 以外の文字はスキップ
}
src := i + 1
// \r の次が \n でない場合(つまり \r 単独の場合)
if src >= len(s) || s[src] != '\n' {
s[i] = '\n' // \r を \n に変換
continue
}
// \r\n のシーケンスが見つかった場合
dst := i // 書き込み先のインデックス
for src < len(s) {
if s[src] == '\r' {
if src+1 < len(s) && s[src+1] == '\n' {
src++ // \r\n の \n をスキップ
}
s[dst] = '\n' // \r または \r\n を \n に変換
} else {
s[dst] = s[src] // その他の文字はそのままコピー
}
src++
dst++
}
return s[:dst] // 短くなったスライスを返す
}
return s // \r が見つからなかった場合は元のスライスを返す
}
この関数は、まず入力スライスを走査し、最初の\r
文字を見つけます。
- もし
\r
の次に\n
が続かない場合(つまり\r
単独の場合)、その\r
を\n
に変換し、次の文字の処理に進みます。 - もし
\r
の次に\n
が続く場合(つまり\r\n
の場合)、またはそれ以降に複数の改行シーケンスが続く場合、インプレースでのコピー処理を開始します。src
ポインタは読み込み元、dst
ポインタは書き込み先を示します。\r
または\r\n
が見つかるたびに、s[dst]
に\n
を書き込み、src
を適切に進めます。これにより、元のスライス内で改行コードが正規化され、不要な文字が上書きされます。最終的に、短くなった可能性のあるスライスを返します。
このconvertNewlines
関数は、Tokenizer.Text()
(テキストノードの内容を取得する際)とTokenizer.TagAttr()
(タグの属性値を取得する際)の両方で呼び出されるようになりました。これにより、HTMLパーサーが扱うすべてのテキストデータが、改行コードに関してHTML5の仕様に準拠した形式に正規化されることが保証されます。
escape
関数 (src/pkg/exp/html/escape.go
)
escape
関数は、HTML出力時に特定の特殊文字をHTMLエンティティに変換する役割を担っています。このコミットでは、escapedChars
定数に\r
が追加され、escape
関数内で\r
が検出された場合に
としてエスケープされるようになりました。
これは、HTMLレンダリングの段階で、\r
文字がブラウザによって誤って解釈されたり、無視されたりするのを防ぐために重要です。特に<pre>
タグのような整形済みテキストを表示する要素内では、\r
の存在がレイアウトに影響を与える可能性があるため、明示的に
としてエスケープすることで、その意図を正確に伝えることができます。
これらの変更により、exp/html
パッケージは、入力されたHTMLの改行コードを適切に正規化し、また出力されるHTMLにおいても\r
文字を正確に表現できるようになり、より堅牢で互換性の高いHTML処理を実現しています。
関連リンク
- Go Gerrit Code Review: https://golang.org/cl/6260046
参考にした情報源リンク
- HTML Standard - 13.2.5.4. Common parsing idioms: https://html.spec.whatwg.org/multipage/parsing.html#common-parsing-idioms (特に "Newline normalisation" のセクション)
- HTML Standard - 13.2.5. The tokenizer: https://html.spec.whatwg.org/multipage/parsing.html#the-tokenizer
- HTML Standard - 13.2.5.1. Data state: https://html.spec.whatwg.org/multipage/parsing.html#data-state
- HTML Entities - W3Schools: https://www.w3schools.com/html/html_entities.asp
- Character entity references in HTML: https://developer.mozilla.org/en-US/docs/Glossary/Character_entity_reference
- GoDoc - golang.org/x/net/html: https://pkg.go.dev/golang.org/x/net/html (現在のパッケージドキュメント)
- Go言語における改行コードの扱い: https://go.dev/blog/strings (Go言語の文字列とバイトスライスに関する一般的な情報)