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

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

コミット

  • コミットハッシュ: 6bf84214c117bd1ea081b93437dbf8463e0dabe8
  • 作者: Russ Cox rsc@golang.org
  • 日付: 2011年12月13日 火曜日 13:33:40 -0500
  • コミットメッセージ: godoc: text wrapping

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

https://github.com/golang/go/commit/6bf84214c117bd1ea081b93437dbf8463e0dabe8

元コミット内容

godoc: text wrapping

Example:

PACKAGE

package utf8
    import "unicode/utf8"

    Package utf8 implements functions and constants to support text
    encoded in UTF-8.  This package calls a Unicode character a rune for
    brevity.

CONSTANTS

const (
    RuneError = unicode.ReplacementChar // the "error" Rune or "replacement character".
    RuneSelf  = 0x80                    // characters below Runeself are represented as themselves in a single byte.
    UTFMax    = 4                       // maximum number of bytes of a UTF-8 encoded Unicode character.
)
    Numbers fundamental to the encoding.

FUNCTIONS

func DecodeLastRune(p []byte) (r rune, size int)
    DecodeLastRune unpacks the last UTF-8 encoding in p and returns the
    rune and its width in bytes.

func DecodeLastRuneInString(s string) (r rune, size int)
    DecodeLastRuneInString is like DecodeLastRune but its input is a
    string.

func DecodeRune(p []byte) (r rune, size int)
    DecodeRune unpacks the first UTF-8 encoding in p and returns the rune
    and its width in bytes.

func DecodeRuneInString(s string) (r rune, size int)
    DecodeRuneInString is like DecodeRune but its input is a string.

func EncodeRune(p []byte, r rune) int
    EncodeRune writes into p (which must be large enough) the UTF-8
    encoding of the rune.  It returns the number of bytes written.

func FullRune(p []byte) bool
    FullRune reports whether the bytes in p begin with a full UTF-8
    encoding of a rune.  An invalid encoding is considered a full Rune
    since it will convert as a width-1 error rune.

func FullRuneInString(s string) bool
    FullRuneInString is like FullRune but its input is a string.

func RuneCount(p []byte) int
    RuneCount returns the number of runes in p.  Erroneous and short
    encodings are treated as single runes of width 1 byte.

func RuneCountInString(s string) (n int)
    RuneCountInString is like RuneCount but its input is a string.

func RuneLen(r rune) int
    RuneLen returns the number of bytes required to encode the rune.

func RuneStart(b byte) bool
    RuneStart reports whether the byte could be the first byte of an
    encoded rune.  Second and subsequent bytes always have the top two
    bits set to 10.

func Valid(p []byte) bool
    Valid reports whether p consists entirely of valid UTF-8-encoded
    runes.

func ValidString(s string) bool
    ValidString reports whether s consists entirely of valid UTF-8-encoded
    runes.

TYPES

type String struct {
    // contains filtered or unexported fields
}
    String wraps a regular string with a small structure that provides
    more efficient indexing by code point index, as opposed to byte index.
    Scanning incrementally forwards or backwards is O(1) per index
    operation (although not as fast a range clause going forwards).
    Random access is O(N) in the length of the string, but the overhead is
    less than always scanning from the beginning.  If the string is ASCII,
    random access is O(1).  Unlike the built-in string type, String has
    internal mutable state and is not thread-safe.

func NewString(contents string) *String
    NewString returns a new UTF-8 string with the provided contents.

func (s *String) At(i int) rune
    At returns the rune with index i in the String.  The sequence of runes
    is the same as iterating over the contents with a "for range" clause.

func (s *String) Init(contents string) *String
    Init initializes an existing String to hold the provided contents.
    It returns a pointer to the initialized String.

func (s *String) IsASCII() bool
    IsASCII returns a boolean indicating whether the String contains only
    ASCII bytes.

func (s *String) RuneCount() int
    RuneCount returns the number of runes (Unicode code points) in the
    String.

func (s *String) Slice(i, j int) string
    Slice returns the string sliced at rune positions [i:j].

func (s *String) String() string
    String returns the contents of the String.  This method also means the
    String is directly printable by fmt.Print.

Fixes #2479.

R=golang-dev, dsymonds, mattn.jp, r, gri, r
CC=golang-dev
https://golang.org/cl/5472051

変更の背景

このコミットは、Go言語のドキュメンテーションツールであるgodocのテキスト出力におけるテキストラッピング(自動改行)機能を追加するものです。以前のgodocのテキスト出力は、長い行がそのまま表示され、特にターミナルや固定幅の表示環境でドキュメントを読む際に、行が画面外にはみ出してしまい、可読性が低いという問題がありました。この変更により、ドキュメントのテキストが指定された幅(デフォルト80カラム)で適切に折り返されるようになり、可読性が大幅に向上しました。

コミットメッセージに記載されているFixes #2479については、現在のGo言語のIssueトラッカーでは直接関連するIssueが見つかりませんでした。これは、コミットが2011年のものであり、当時のIssue管理システムやIssue番号の割り当てが現在とは異なる可能性、あるいはGo言語のメインリポジトリ以外のプロジェクトのIssueを参照している可能性が考えられます。

前提知識の解説

godoc

godocは、Go言語のソースコードからドキュメンテーションを生成し、表示するためのツールです。Go言語では、コード内のコメントがそのままドキュメンテーションとして機能する「Go Doc」という文化があり、godocはそのコメントを解析して整形されたドキュメントとして提供します。ローカルでHTTPサーバーを起動してブラウザで閲覧したり、コマンドラインでテキスト形式で表示したりできます。このコミットは、特にコマンドラインでのテキスト表示の改善を目的としています。

テキストラッピング(自動改行)

テキストラッピングとは、テキストが特定の幅を超えた場合に、自動的に次の行に折り返す処理のことです。これにより、長い文章でも画面の幅に合わせて表示され、ユーザーは水平スクロールなしで内容を読み進めることができます。プログラミングにおいては、コードコメントやドキュメンテーションの可読性を高める上で非常に重要な機能です。

UTF-8とRune

Go言語では、文字列はUTF-8でエンコードされたバイト列として扱われます。Unicodeのコードポイントはrune型で表現され、これはGo言語における文字の概念に近いです。UTF-8は可変長エンコーディングであり、1つのruneが1バイトから4バイトの範囲で表現されます。テキストラッピングを正確に行うためには、バイト数ではなくrune(文字)の数を基準に幅を計算する必要があります。

Goのtext/templateパッケージ

Go言語のtext/templateパッケージは、テキストベースの出力を生成するためのテンプレートエンジンを提供します。godocは、このテンプレートエンジンを使用して、Goのソースコードから抽出したドキュメンテーション情報を整形して表示します。テンプレート内で関数を呼び出すことで、動的なコンテンツ生成やフォーマットを行うことができます。

template.FuncMap

template.FuncMapは、text/templateパッケージで使用されるマップで、テンプレート内で呼び出すことができるカスタム関数を登録するために使用されます。このマップに登録された関数は、テンプレート内で{{funcName .Data}}のような形式で呼び出すことができます。

技術的詳細

このコミットの主要な変更点は、godocのテキスト出力におけるコメントの整形ロジックにテキストラッピング機能を追加したことです。具体的には、以下のファイルが変更されています。

  1. lib/godoc/package.txt: godocがパッケージドキュメントを生成する際に使用するテンプレートファイルです。このファイルでは、ドキュメントコメントを表示する箇所で、新しく追加されたcomment_textテンプレート関数が使用されるように変更されています。これにより、コメントがテキストラッピングされて出力されるようになります。

    変更前: {{.Doc}} 変更後: {{comment_text .Doc " " "\t"}}

    comment_text関数は、コメントテキスト、インデント文字列、プリインデント文字列を引数として受け取ります。

  2. src/cmd/godoc/godoc.go: godocコマンドのメインロジックが含まれるファイルです。

    • punchCardWidthという定数が導入されました。これは、テキストラッピングの基準となる固定幅(80カラム)を定義しています。この幅は、環境やTTYの幅に依存せず、常に一定の出力を保証するために固定されています。これは、lsコマンドのようにTTYへの出力とファイルへの出力で挙動が変わることで混乱を招くことを避けるための設計判断です。
    • comment_textFuncという新しい関数が追加され、template.FuncMapcomment_textという名前で登録されました。この関数は、コメントテキスト、インデント、プリインデント、そしてpunchCardWidthから計算された幅を引数としてdoc.ToText関数を呼び出します。
  3. src/pkg/go/doc/comment.go: Goのドキュメンテーションコメントを処理するためのコアロジックが含まれるファイルです。このファイルが最も大きく変更されています。

    • 既存のToHTML関数がリファクタリングされ、コメントテキストをHTMLに変換するロジックが改善されました。特に、コメントを段落(opPara)、見出し(opHead)、整形済みテキスト(opPre)のブロックに分割する新しいblocks関数が導入されました。
    • block構造体とop列挙型が定義され、コメントの構造をより明確に表現できるようになりました。
    • ToText関数が新しく追加されました。 この関数は、コメントテキストをプレーンテキスト形式で整形し、指定された幅でテキストラッピングを行う主要なロジックを実装しています。
    • lineWrapperという新しい構造体が導入されました。これは、テキストラッピング処理を効率的に行うためのヘルパー構造体で、出力バッファ、行の幅、インデント、現在の行の長さなどを管理します。writeメソッドは単語をバッファに書き込み、必要に応じて改行を挿入します。flushメソッドは、残りのバッファ内容をフラッシュします。
  4. src/pkg/go/doc/comment_test.go: src/pkg/go/doc/comment.goのテストファイルです。

    • 新しく追加されたblocks関数の動作を検証するためのテストケースblocksTestsが追加されました。これにより、コメントが正しく構造化されたブロックに分割されることが保証されます。

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

このコミットのコアとなる変更は、src/pkg/go/doc/comment.goファイルに新しく追加されたToText関数と、それに付随するlineWrapper構造体です。

// src/pkg/go/doc/comment.go (抜粋)

// ToText prepares comment text for presentation in textual output.
// It wraps paragraphs of text to width or fewer Unicode code points
// and then prefixes each line with the indent.  In preformatted sections
// (such as program text), it prefixes each non-blank line with preIndent.
func ToText(w io.Writer, text string, indent, preIndent string, width int) {
	l := lineWrapper{
		out:    w,
		width:  width,
		indent: indent,
	}
	for i, b := range blocks(text) {
		switch b.op {
		case opPara:
			if i > 0 {
				w.Write(nl) // blank line before new paragraph
			}
			for _, line := range b.lines {
				l.write(line)
			}
			l.flush()
		case opHead:
			w.Write(nl)
			for _, line := range b.lines {
				l.write(line + "\n") // Headings are always followed by a newline
			}
			l.flush()
		case opPre:
			w.Write(nl)
			for _, line := range b.lines {
				if !isBlank(line) {
					w.Write([]byte(preIndent))
					w.Write([]byte(line))
				}
				w.Write(nl) // Each line in preformatted block gets its own newline
			}
		}
	}
}

type lineWrapper struct {
	out       io.Writer
	printed   bool // true if anything has been printed to out
	width     int
	indent    string
	n         int // current length of line (in runes)
	pendSpace int // number of pending spaces to write
}

var nl = []byte("\n")
var space = []byte(" ")

func (l *lineWrapper) write(text string) {
	if l.n == 0 && l.printed {
		l.out.Write(nl) // blank line before new paragraph
	}
	l.printed = true

	for _, f := range strings.Fields(text) { // Iterate over words
		w := utf8.RuneCountInString(f) // Get width in runes
		// wrap if line is too long
		if l.n > 0 && l.n+l.pendSpace+w > l.width {
			l.out.Write(nl)
			l.n = 0
			l.pendSpace = 0
		}
		if l.n == 0 {
			l.out.Write([]byte(l.indent)) // Apply indent at start of new line
		}
		l.out.Write(space[:l.pendSpace]) // Write pending spaces
		l.out.Write([]byte(f))           // Write the word
		l.n += l.pendSpace + w           // Update current line length
		l.pendSpace = 1                  // Next word needs a space
	}
}

func (l *lineWrapper) flush() {
	if l.n == 0 {
		return
	}
	l.out.Write(nl) // End the current line
	l.pendSpace = 0
	l.n = 0
}

コアとなるコードの解説

ToText関数は、Goのドキュメンテーションコメントをプレーンテキスト形式で整形し、指定された幅でテキストラッピングを行うための中心的な役割を担います。

  1. blocks関数によるコメントの構造化: ToText関数はまず、入力されたコメントテキストをblocks関数によって、段落(opPara)、見出し(opHead)、整形済みテキスト(opPre)の論理的なブロックに分割します。これにより、各ブロックの特性に応じた異なるラッピング処理が可能になります。例えば、整形済みテキスト(コードブロックなど)はラッピングされず、そのままの形式で出力されます。

  2. lineWrapper構造体: lineWrapperは、実際のテキストラッピング処理を行うためのヘルパーです。

    • out io.Writer: 整形されたテキストの出力先。
    • printed bool: これまでに出力が行われたかどうかを示すフラグ。新しい段落の前に空行を挿入するかどうかを判断するために使用されます。
    • width int: テキストを折り返す最大幅(Unicodeルーン数)。
    • indent string: 各行の先頭に付加するインデント文字列。
    • n int: 現在の行に書き込まれたルーンの数。
    • pendSpace int: 次の単語の前に挿入すべきスペースの数。
  3. lineWrapper.writeメソッド: このメソッドは、与えられたテキスト(通常は単語)をlineWrapperの内部バッファに書き込み、必要に応じて改行を挿入します。

    • strings.Fields(text): 入力テキストをスペースで区切って単語のリストに分割します。
    • utf8.RuneCountInString(f): 各単語の長さをバイト数ではなくUnicodeルーン数で正確に計算します。これにより、マルチバイト文字を含むテキストでも正しくラッピングが行われます。
    • ラッピングロジック: if l.n > 0 && l.n+l.pendSpace+w > l.widthの条件で、現在の行の長さ(l.n)、保留中のスペース(l.pendSpace)、次の単語の長さ(w)の合計が指定されたwidthを超える場合に、改行(l.out.Write(nl))を挿入します。
    • インデント: 新しい行の開始時には、l.indentで指定されたインデントが適用されます。
    • スペースの管理: l.pendSpaceを使用して、単語間のスペースを適切に挿入します。
  4. lineWrapper.flushメソッド: このメソッドは、lineWrapperにまだ書き込まれていない内容があれば、強制的に改行を挿入して出力バッファをフラッシュします。これにより、段落の最後に残ったテキストが確実に新しい行で閉じられます。

この一連の処理により、godocのテキスト出力は、固定幅のターミナル環境でも読みやすいように、適切に整形され、自動改行されるようになりました。特に、Unicodeルーンを考慮した正確な幅計算は、多言語対応のドキュメントにおいても重要な改善点です。

関連リンク

  • Go言語の公式ドキュメンテーション: https://go.dev/doc/
  • godocコマンドの現在の推奨代替ツール: go docまたはpkgsite (pkg.go.dev)

参考にした情報源リンク