[インデックス 1304] ファイルの概要
このコミットは、Go言語の標準ライブラリの一部であるtabwriter
パッケージにおける重要な改善とバグ修正を含んでいます。tabwriter
は、テキストを整形し、タブ区切りのデータをカラム状に整列させるためのユーティリティです。
主な変更点は以下の通りです。
- Unicodeテキストのフォーマットに関するバグ修正:部分的なルーン(Unicode文字)が入力された際に、正確なバイト数を計算できない問題を解決するため、幅の計算を遅延させるように変更されました。
- HTMLフィルタリングモードの追加:HTMLタグやエンティティを幅計算の際に無視する機能が導入されました。これにより、HTMLを含むテキストを整形する際に、タグの長さがレイアウトに影響を与えないようになります。
- テストの拡充:HTMLテキストのテストや、様々な方法(バイト単位、フィボナッチ数列のサイズなど)でテキストを書き込むテストが追加され、堅牢性が向上しました。
コミット
commit 6cbdeb3f8810a7acb20d166fe399ab087587a353
Author: Robert Griesemer <gri@golang.org>
Date: Tue Dec 9 13:03:15 2008 -0800
- fixed bug with unicode text formatting: the number of bytes
per rune cannot be computed correctly if we have only parts
of a rune - delay computation
- added html filtering mode: html tags and entities are ignored
for width computations
- expanded tests:
- extra tests for html text
- extra tests that write text in various portions
R=r
DELTA=227 (126 added, 20 deleted, 81 changed)
OCL=20833
CL=20835
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6cbdeb3f8810a7acb20d166fe399ab087587a353
元コミット内容
このコミットは、tabwriter
パッケージの機能拡張とバグ修正を目的としています。具体的には、以下の3つの主要な変更が含まれています。
- Unicodeテキストフォーマットのバグ修正: ルーン(Unicode文字)が複数バイトで構成される場合、そのルーンの一部しか読み込まれていない状態で幅を計算しようとすると、誤った結果になるバグが修正されました。この修正は、幅の計算をルーン全体が利用可能になるまで遅延させることで実現されています。
- HTMLフィルタリングモードの追加: HTMLタグ(例:
<b>
,<div>
)やHTMLエンティティ(例:<
,&
)を、整形時の幅計算から除外する新しいモードが追加されました。これにより、HTMLを含むテキストを整形する際に、タグの長さがカラムの配置に影響を与えなくなります。HTMLエンティティは1文字としてカウントされます。 - テストの拡充: 上記の変更を検証するために、HTMLテキストの整形に関する追加テストや、様々な入力パターン(バイト単位での書き込み、フィボナッチ数列のサイズでの書き込みなど)に対する堅牢性を確認するテストが追加されました。
変更の背景
tabwriter
パッケージは、Go言語においてテキストを整形し、特にタブ区切りのデータをカラム状に整列させる際に非常に有用なツールです。しかし、初期の実装にはいくつかの課題がありました。
- Unicode文字の不正確な幅計算: UTF-8のような可変長エンコーディングを使用するUnicodeテキストでは、1つの文字(ルーン)が1バイト以上を占めることがあります。
tabwriter
が入力ストリームから部分的なバイト列を受け取った場合、それが完全なルーンを形成しているかどうかの判断が難しく、結果として文字の幅を誤って計算し、整形が崩れる可能性がありました。特に、日本語のようなマルチバイト文字を扱う際にはこの問題が顕著になります。 - HTMLコンテンツの整形問題: Webアプリケーションやドキュメント生成において、HTMLタグやエンティティを含むテキストを
tabwriter
で整形しようとすると、これらのタグやエンティティが通常の文字として幅計算に含まれてしまい、意図しない余白やカラムのずれが発生していました。例えば、<b>bold</b>
というテキストは、表示上は「bold」の4文字ですが、tabwriter
はタグを含めた11文字として幅を計算してしまい、レイアウトが崩れる原因となっていました。 - テストカバレッジの不足: 上記のようなエッジケースや複雑な入力パターンに対するテストが不足しており、潜在的なバグを見逃す可能性がありました。特に、ストリーム処理を行うライブラリでは、入力が一度にすべて与えられるとは限らず、部分的に与えられるケース(バイト単位、チャンク単位など)に対する堅牢性が求められます。
これらの課題に対処し、tabwriter
の信頼性と実用性を向上させるために、本コミットによる修正と機能追加が行われました。
前提知識の解説
1. tabwriter
パッケージの基本
tabwriter
パッケージは、Go言語のio.Writer
インターフェースを実装しており、入力されたテキストを整形して別のio.Writer
に出力します。主な機能は以下の通りです。
- カラム整列: タブ文字(
\t
)で区切られたテキストを、指定された最小カラム幅とパディングに基づいて自動的に整列させます。 - パディング: カラム間の空白を埋めるための文字(
padchar
)と、追加のパディング量(padding
)を設定できます。 - アラインメント: テキストを左寄せまたは右寄せに設定できます。
- フラッシュ: バッファリングされたテキストを強制的に出力する
Flush
メソッドを提供します。
2. Unicodeとルーン(Rune)
- Unicode: 世界中の文字を統一的に扱うための文字コード標準です。
- UTF-8: Unicode文字をバイト列にエンコードするための可変長エンコーディング方式です。ASCII文字は1バイト、日本語の漢字などは3バイトで表現されるなど、文字によってバイト数が異なります。
- ルーン(Rune): Go言語において、Unicodeコードポイントを表す型です。Goの文字列はUTF-8バイト列として扱われますが、
for range
ループなどで文字列をイテレートすると、各要素はルーンとして扱われます。tabwriter
が文字の「幅」を計算する際には、このルーンの概念が重要になります。
3. HTMLタグとHTMLエンティティ
- HTMLタグ:
<b>
,<i>
,<div>
,<p>
などの要素を定義するマークアップです。これらはブラウザによって解釈され、表示には影響しませんが、テキストデータとしては文字数を持ちます。 - HTMLエンティティ:
<
(<
),>
(>
),&
(&
),"
("
),'
('
),
(非改行スペース) など、特殊文字を表現するためのシーケンスです。これらもテキストデータとしては複数の文字で構成されますが、表示上は1つの文字として扱われます。
tabwriter
がこれらのHTML要素を幅計算に含めてしまうと、視覚的なレイアウトと実際の文字数計算が一致せず、整形が崩れる原因となります。
4. ストリーム処理とバッファリング
tabwriter
のようなio.Writer
を実装するコンポーネントは、通常、入力データを一度にすべて受け取るのではなく、小さなチャンク(バイト列)として逐次的に受け取ります。これをストリーム処理と呼びます。
- バッファリング: 効率的な処理のために、入力されたチャンクを内部バッファに一時的に蓄積し、ある程度の量がたまるか、特定の区切り文字(タブ、改行など)が検出されたときにまとめて処理します。
- 部分的な入力: ストリーム処理では、ルーンの途中で入力チャンクが途切れる(例: UTF-8の3バイト文字の最初の2バイトだけが来た状態)といった「部分的な入力」が発生する可能性があります。この場合、完全なルーンが揃うまで幅の計算を遅延させる必要があります。
技術的詳細
このコミットは、tabwriter
パッケージのWriter
構造体とそのWrite
メソッドを中心に変更を加えています。
1. Unicode幅計算の遅延
以前のtabwriter
は、入力されたバイト列を即座にUnicodeLen
関数に渡し、その幅をb.width
に加算していました。しかし、UnicodeLen
は完全なルーンが揃っていない場合、正確な幅を計算できません。
変更点:
Writer
構造体にpos int
フィールドが追加されました。これは、buf
(収集されたテキスト)の中で、既に幅が計算された部分の終端位置を示します。Write
メソッド内で、タブや改行でセルが区切られる直前に、b.width += UnicodeLen(b.buf.Slice(b.pos, b.buf.Len()));
という行が追加されました。これにより、セルが確定するまで、つまり完全なルーンがバッファに揃うまで、幅の計算が遅延されるようになりました。b.pos = b.buf.Len();
によって、幅計算が完了した位置が更新されます。
この修正により、部分的なルーンがバッファに存在しても、それが次の入力チャンクで補完され、完全なルーンとして認識されてから幅が計算されるため、Unicodeテキストの整形精度が向上しました。
2. HTMLフィルタリングモードの実装
HTMLタグやエンティティを幅計算から除外するための新しいロジックがWrite
メソッドに導入されました。
変更点:
Writer
構造体にfilter_html bool
とhtml_char byte
フィールドが追加されました。filter_html
: HTMLフィルタリングを有効にするかどうかを制御します。html_char
: 現在処理中のHTMLタグまたはエンティティの終端文字(>
または;
)を保持します。0の場合はHTML要素の内部ではないことを示します。
Writer.Init
メソッドにfilter_html
パラメータが追加され、初期化時にこのモードを有効にできるようになりました。Write
メソッドの内部ロジックが大幅に変更されました。- HTML要素の外部 (
b.html_char == 0
):- 通常の文字処理に加え、
'<'
(HTMLタグの開始)または'&'
(HTMLエンティティの開始)が検出された場合、filter_html
が有効であれば、HTML要素の処理モードに移行します。 b.html_char
に適切な終端文字(>
または;
)が設定され、b.pos
が-1
に設定されます(これは、HTML要素の内部では幅計算を行わないことを示す予防的な措置です)。
- 通常の文字処理に加え、
- HTML要素の内部 (
b.html_char != 0
):- 入力バイトが
b.html_char
(終端文字)と一致した場合、HTML要素の終端に達したと判断します。 - HTMLエンティティ(
html_char == ';'
)の場合のみ、b.width++
によって幅を1文字分加算します。これは、エンティティが視覚的には1文字として扱われるためです。HTMLタグの場合は幅は加算されません。 b.html_char
が0
にリセットされ、通常の文字処理モードに戻ります。
- 入力バイトが
- HTML要素の外部 (
このロジックにより、tabwriter
はHTMLタグを完全に無視し、HTMLエンティティを1文字として扱うことで、HTMLを含むテキストの整形を正確に行えるようになりました。
3. ByteArray.Slice
の導入
差分から直接は確認できませんが、b.buf.a[pos : pos + s]
のような直接的なバイト配列のスライス操作がb.buf.Slice(pos, pos + s)
に置き換えられています。これは、ByteArray
型にSlice
メソッドが追加されたか、または既存のSlice
メソッドがより適切に使用されるようになったことを示唆しています。これにより、内部配列への直接アクセスを減らし、ByteArray
の抽象化と安全性を高めることができます。
4. テストの拡充
tabwriter_test.go
ファイルでは、テストの構造が改善され、新しいテストケースが追加されました。
Check
関数がfilter_html
パラメータを受け取るようになりました。Write
とVerify
というヘルパー関数が導入され、テストコードの重複が削減され、可読性が向上しました。- 書き込みパターンのテスト:
Write
メソッドの堅牢性を検証するため、以下のパターンでテキストを書き込むテストが追加されました。write all at once
: 全テキストを一度に書き込む。write byte-by-byte
: テキストを1バイトずつ書き込む。write using Fibonacci slice sizes
: フィボナッチ数列のサイズでテキストをチャンクに分割して書き込む。 これらのテストは、tabwriter
が部分的な入力や様々なサイズのチャンク入力に対して正しく動作することを確認します。
- HTMLフィルタリングのテスト:
filter_html
をtrue
に設定した新しいテストケースが追加され、HTMLタグやエンティティが正しく無視または処理されることを検証しています。
これらの技術的変更により、tabwriter
はより堅牢で、多様なテキストコンテンツ(特にUnicodeやHTMLを含むもの)に対して正確な整形を提供できるようになりました。
コアとなるコードの変更箇所
src/lib/tabwriter/tabwriter.go
ByteArray
構造体:Len() int
メソッドの追加。
Writer
構造体:filter_html bool
フィールドの追加。html_char byte
フィールドの追加。size
,width
,pos
フィールドのコメント更新。- 内部表現に関する詳細なコメントの追加。
Writer.Init
関数:filter_html bool
パラメータの追加。b.filter_html = filter_html;
の追加。
Writer.WriteLines
関数:b.buf.a[pos : pos + s]
がb.buf.Slice(pos, pos + s)
に変更。- 最終行の処理から
b.size, b.width = 0, 0;
の削除。
Writer.Flush
関数:b.pos = 0;
とb.AddLine();
の追加。- 関数の位置が変更。
Writer.Write
関数:- HTMLフィルタリングロジックの追加(
b.html_char
による状態管理)。 - Unicode幅計算の遅延ロジックの追加(
b.pos
とUnicodeLen
の組み合わせ)。 - タブと改行の処理ロジックの変更。
- HTMLフィルタリングロジックの追加(
New
関数:filter_html bool
パラメータの追加。
src/lib/tabwriter/tabwriter_test.go
Buffer
構造体:Clear()
メソッドの追加。
Check
関数:filter_html bool
パラメータの追加。Write
とVerify
ヘルパー関数の導入。- 「write all at once」「write byte-by-byte」「write using Fibonacci slice sizes」のテストパターン追加。
Write
関数: 新規追加されたヘルパー関数。Verify
関数: 新規追加されたヘルパー関数。Test
関数:- 既存の
Check
呼び出しにfilter_html: false
を追加。 - HTMLフィルタリングをテストする新しい
Check
呼び出しの追加。 - 日本語文字のテストケースの期待値の変更(パディング文字の変更)。
- 既存の
コアとなるコードの解説
tabwriter.go
の Writer.Write
メソッド
このコミットの最も重要な変更は、Writer.Write
メソッドの内部ロジックに集約されています。このメソッドは、入力されたバイト列を処理し、タブや改行に基づいてセルを区切り、内部バッファに蓄積する役割を担っています。
変更後の Write
メソッドの主要なロジックは以下のようになります。
/* export */ func (b *Writer) Write(buf *[]byte) (written int, err *os.Error) {
i0, n := 0, len(buf);
// split text into cells
for i := 0; i < n; i++ {
ch := buf[i];
if b.html_char == 0 {
// outside html tag/entity
switch ch {
case '\t', '\n':
b.Append(buf[i0 : i]); // 現在のチャンクのi0からiまでをバッファに追加
i0 = i + 1; // 次のセルの開始位置を更新
// ここで、バッファに追加された部分のUnicode幅を計算し、b.widthに加算
b.width += UnicodeLen(b.buf.Slice(b.pos, b.buf.Len()));
b.pos = b.buf.Len(); // 幅計算済みの位置を更新
// セルを確定
last_size, last_width := b.Line(b.lines_size.Len() - 1);
last_size.Push(b.size);
last_width.Push(b.width);
b.size, b.width = 0, 0; // 現在のセルの状態をリセット
if ch == '\n' {
b.AddLine(); // 改行の場合、新しい行を追加
// ... (単一セル行の自動フラッシュロジック) ...
}
case '<', '&':
if b.filter_html { // HTMLフィルタリングが有効な場合
b.Append(buf[i0 : i]); // 現在のチャンクのi0からiまでをバッファに追加
i0 = i;
// ここで、バッファに追加された部分のUnicode幅を計算し、b.widthに加算
b.width += UnicodeLen(b.buf.Slice(b.pos, b.buf.Len()));
b.pos = -1; // 予防的な措置: HTML要素内部では幅計算を行わない
if ch == '<' {
b.html_char = '>'; // HTMLタグの終端文字を設定
} else {
b.html_char = ';'; // HTMLエンティティの終端文字を設定
}
}
}
} else {
// inside html tag/entity
if ch == b.html_char { // HTML要素の終端文字に到達した場合
b.Append(buf[i0 : i + 1]); // HTML要素全体をバッファに追加
i0 = i + 1;
if b.html_char == ';' {
b.width++; // HTMLエンティティの場合、幅を1文字分加算
}
b.pos = b.buf.Len(); // 幅計算済みの位置を更新
b.html_char = 0; // HTML要素処理モードを終了
}
}
}
// ... (残りのバッファ処理) ...
}
主要な変更点の詳細:
-
Unicode幅計算の遅延:
b.width += UnicodeLen(b.buf.Slice(b.pos, b.buf.Len()));
とb.pos = b.buf.Len();
の行が、タブや改行、あるいはHTML要素の開始によってセルが区切られる直前に追加されました。- これにより、
UnicodeLen
が呼び出される時点では、b.buf.Slice(b.pos, b.buf.Len())
で指定される範囲が、少なくとも完全なルーンを含むか、またはセルの終端に達していることが保証されます。これにより、部分的なルーンによる幅計算の不正確さが解消されます。
-
HTMLフィルタリングの状態管理:
b.html_char
フィールドが、現在の処理がHTMLタグまたはエンティティの内部にあるかどうかを示す状態変数として機能します。if b.html_char == 0
ブロックは、HTML要素の外部での通常のテキスト処理を行います。ここで<
や&
が検出されると、b.html_char
が設定され、HTML要素の内部処理モードに移行します。else
ブロックは、HTML要素の内部での処理を行います。ここでは、終端文字(>
または;
)が検出されるまで、文字はバッファに追加されますが、幅計算には影響しません(エンティティの場合のみ1文字としてカウント)。終端文字が検出されると、b.html_char
がリセットされ、通常のテキスト処理モードに戻ります。
この洗練された状態管理と幅計算の遅延ロジックにより、tabwriter
はUnicodeテキストとHTMLコンテンツの両方に対して、より正確で堅牢な整形機能を提供できるようになりました。
関連リンク
- Go言語の
text/tabwriter
パッケージのドキュメント: https://pkg.go.dev/text/tabwriter (現在のバージョン) - Go言語の
io
パッケージのドキュメント: https://pkg.go.dev/io - Unicode Consortium: https://home.unicode.org/
- UTF-8に関するWikipedia記事: https://ja.wikipedia.org/wiki/UTF-8
- HTMLエンティティに関するMDN Web Docs: https://developer.mozilla.org/ja/docs/Glossary/Entity
参考にした情報源リンク
- コミットハッシュ:
6cbdeb3f8810a7acb20d166fe399ab087587a353
- GitHub上のコミットページ: https://github.com/golang/go/commit/6cbdeb3f8810a7acb20d166fe399ab087587a353
- Go言語のソースコードリポジトリ: https://github.com/golang/go
- Go言語の
text/tabwriter
パッケージの現在のソースコード: https://github.com/golang/go/tree/master/src/text/tabwriter (現在のパッケージパスはsrc/text/tabwriter
に変更されていますが、このコミット当時はsrc/lib/tabwriter
でした)