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

[インデックス 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つの主要な変更が含まれています。

  1. Unicodeテキストフォーマットのバグ修正: ルーン(Unicode文字)が複数バイトで構成される場合、そのルーンの一部しか読み込まれていない状態で幅を計算しようとすると、誤った結果になるバグが修正されました。この修正は、幅の計算をルーン全体が利用可能になるまで遅延させることで実現されています。
  2. HTMLフィルタリングモードの追加: HTMLタグ(例: <b>, <div>)やHTMLエンティティ(例: &lt;, &amp;)を、整形時の幅計算から除外する新しいモードが追加されました。これにより、HTMLを含むテキストを整形する際に、タグの長さがカラムの配置に影響を与えなくなります。HTMLエンティティは1文字としてカウントされます。
  3. テストの拡充: 上記の変更を検証するために、HTMLテキストの整形に関する追加テストや、様々な入力パターン(バイト単位での書き込み、フィボナッチ数列のサイズでの書き込みなど)に対する堅牢性を確認するテストが追加されました。

変更の背景

tabwriterパッケージは、Go言語においてテキストを整形し、特にタブ区切りのデータをカラム状に整列させる際に非常に有用なツールです。しかし、初期の実装にはいくつかの課題がありました。

  1. Unicode文字の不正確な幅計算: UTF-8のような可変長エンコーディングを使用するUnicodeテキストでは、1つの文字(ルーン)が1バイト以上を占めることがあります。tabwriterが入力ストリームから部分的なバイト列を受け取った場合、それが完全なルーンを形成しているかどうかの判断が難しく、結果として文字の幅を誤って計算し、整形が崩れる可能性がありました。特に、日本語のようなマルチバイト文字を扱う際にはこの問題が顕著になります。
  2. HTMLコンテンツの整形問題: Webアプリケーションやドキュメント生成において、HTMLタグやエンティティを含むテキストをtabwriterで整形しようとすると、これらのタグやエンティティが通常の文字として幅計算に含まれてしまい、意図しない余白やカラムのずれが発生していました。例えば、<b>bold</b>というテキストは、表示上は「bold」の4文字ですが、tabwriterはタグを含めた11文字として幅を計算してしまい、レイアウトが崩れる原因となっていました。
  3. テストカバレッジの不足: 上記のようなエッジケースや複雑な入力パターンに対するテストが不足しており、潜在的なバグを見逃す可能性がありました。特に、ストリーム処理を行うライブラリでは、入力が一度にすべて与えられるとは限らず、部分的に与えられるケース(バイト単位、チャンク単位など)に対する堅牢性が求められます。

これらの課題に対処し、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エンティティ: &lt; (<), &gt; (>), &amp; (&), &quot; ("), &apos; ('), &nbsp; (非改行スペース) など、特殊文字を表現するためのシーケンスです。これらもテキストデータとしては複数の文字で構成されますが、表示上は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 boolhtml_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_char0にリセットされ、通常の文字処理モードに戻ります。

このロジックにより、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パラメータを受け取るようになりました。
  • WriteVerifyというヘルパー関数が導入され、テストコードの重複が削減され、可読性が向上しました。
  • 書き込みパターンのテスト: Writeメソッドの堅牢性を検証するため、以下のパターンでテキストを書き込むテストが追加されました。
    • write all at once: 全テキストを一度に書き込む。
    • write byte-by-byte: テキストを1バイトずつ書き込む。
    • write using Fibonacci slice sizes: フィボナッチ数列のサイズでテキストをチャンクに分割して書き込む。 これらのテストは、tabwriterが部分的な入力や様々なサイズのチャンク入力に対して正しく動作することを確認します。
  • HTMLフィルタリングのテスト: filter_htmltrueに設定した新しいテストケースが追加され、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.posUnicodeLenの組み合わせ)。
    • タブと改行の処理ロジックの変更。
  • New関数:
    • filter_html bool パラメータの追加。

src/lib/tabwriter/tabwriter_test.go

  • Buffer構造体:
    • Clear() メソッドの追加。
  • Check関数:
    • filter_html bool パラメータの追加。
    • WriteVerifyヘルパー関数の導入。
    • 「write all at once」「write byte-by-byte」「write using Fibonacci slice sizes」のテストパターン追加。
  • Write関数: 新規追加されたヘルパー関数。
  • Verify関数: 新規追加されたヘルパー関数。
  • Test関数:
    • 既存のCheck呼び出しにfilter_html: falseを追加。
    • HTMLフィルタリングをテストする新しいCheck呼び出しの追加。
    • 日本語文字のテストケースの期待値の変更(パディング文字の変更)。

コアとなるコードの解説

tabwriter.goWriter.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要素処理モードを終了
 			}
 		}
 	}
 	// ... (残りのバッファ処理) ...
}

主要な変更点の詳細:

  1. 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())で指定される範囲が、少なくとも完全なルーンを含むか、またはセルの終端に達していることが保証されます。これにより、部分的なルーンによる幅計算の不正確さが解消されます。
  2. HTMLフィルタリングの状態管理:

    • b.html_char フィールドが、現在の処理がHTMLタグまたはエンティティの内部にあるかどうかを示す状態変数として機能します。
    • if b.html_char == 0 ブロックは、HTML要素の外部での通常のテキスト処理を行います。ここで<&が検出されると、b.html_charが設定され、HTML要素の内部処理モードに移行します。
    • else ブロックは、HTML要素の内部での処理を行います。ここでは、終端文字(>または;)が検出されるまで、文字はバッファに追加されますが、幅計算には影響しません(エンティティの場合のみ1文字としてカウント)。終端文字が検出されると、b.html_charがリセットされ、通常のテキスト処理モードに戻ります。

この洗練された状態管理と幅計算の遅延ロジックにより、tabwriterはUnicodeテキストとHTMLコンテンツの両方に対して、より正確で堅牢な整形機能を提供できるようになりました。

関連リンク

参考にした情報源リンク