diff options
Diffstat (limited to 'src/text/tabwriter')
-rw-r--r-- | src/text/tabwriter/example_test.go | 73 | ||||
-rw-r--r-- | src/text/tabwriter/tabwriter.go | 600 | ||||
-rw-r--r-- | src/text/tabwriter/tabwriter_test.go | 754 |
3 files changed, 1427 insertions, 0 deletions
diff --git a/src/text/tabwriter/example_test.go b/src/text/tabwriter/example_test.go new file mode 100644 index 0000000..422ec11 --- /dev/null +++ b/src/text/tabwriter/example_test.go @@ -0,0 +1,73 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tabwriter_test + +import ( + "fmt" + "os" + "text/tabwriter" +) + +func ExampleWriter_Init() { + w := new(tabwriter.Writer) + + // Format in tab-separated columns with a tab stop of 8. + w.Init(os.Stdout, 0, 8, 0, '\t', 0) + fmt.Fprintln(w, "a\tb\tc\td\t.") + fmt.Fprintln(w, "123\t12345\t1234567\t123456789\t.") + fmt.Fprintln(w) + w.Flush() + + // Format right-aligned in space-separated columns of minimal width 5 + // and at least one blank of padding (so wider column entries do not + // touch each other). + w.Init(os.Stdout, 5, 0, 1, ' ', tabwriter.AlignRight) + fmt.Fprintln(w, "a\tb\tc\td\t.") + fmt.Fprintln(w, "123\t12345\t1234567\t123456789\t.") + fmt.Fprintln(w) + w.Flush() + + // output: + // a b c d . + // 123 12345 1234567 123456789 . + // + // a b c d. + // 123 12345 1234567 123456789. +} + +func Example_elastic() { + // Observe how the b's and the d's, despite appearing in the + // second cell of each line, belong to different columns. + w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, '.', tabwriter.AlignRight|tabwriter.Debug) + fmt.Fprintln(w, "a\tb\tc") + fmt.Fprintln(w, "aa\tbb\tcc") + fmt.Fprintln(w, "aaa\t") // trailing tab + fmt.Fprintln(w, "aaaa\tdddd\teeee") + w.Flush() + + // output: + // ....a|..b|c + // ...aa|.bb|cc + // ..aaa| + // .aaaa|.dddd|eeee +} + +func Example_trailingTab() { + // Observe that the third line has no trailing tab, + // so its final cell is not part of an aligned column. + const padding = 3 + w := tabwriter.NewWriter(os.Stdout, 0, 0, padding, '-', tabwriter.AlignRight|tabwriter.Debug) + fmt.Fprintln(w, "a\tb\taligned\t") + fmt.Fprintln(w, "aa\tbb\taligned\t") + fmt.Fprintln(w, "aaa\tbbb\tunaligned") // no trailing tab + fmt.Fprintln(w, "aaaa\tbbbb\taligned\t") + w.Flush() + + // output: + // ------a|------b|---aligned| + // -----aa|-----bb|---aligned| + // ----aaa|----bbb|unaligned + // ---aaaa|---bbbb|---aligned| +} diff --git a/src/text/tabwriter/tabwriter.go b/src/text/tabwriter/tabwriter.go new file mode 100644 index 0000000..d4cfcf5 --- /dev/null +++ b/src/text/tabwriter/tabwriter.go @@ -0,0 +1,600 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package tabwriter implements a write filter (tabwriter.Writer) that +// translates tabbed columns in input into properly aligned text. +// +// The package is using the Elastic Tabstops algorithm described at +// http://nickgravgaard.com/elastictabstops/index.html. +// +// The text/tabwriter package is frozen and is not accepting new features. +package tabwriter + +import ( + "io" + "unicode/utf8" +) + +// ---------------------------------------------------------------------------- +// Filter implementation + +// A cell represents a segment of text terminated by tabs or line breaks. +// The text itself is stored in a separate buffer; cell only describes the +// segment's size in bytes, its width in runes, and whether it's an htab +// ('\t') terminated cell. +type cell struct { + size int // cell size in bytes + width int // cell width in runes + htab bool // true if the cell is terminated by an htab ('\t') +} + +// A Writer is a filter that inserts padding around tab-delimited +// columns in its input to align them in the output. +// +// The Writer treats incoming bytes as UTF-8-encoded text consisting +// of cells terminated by horizontal ('\t') or vertical ('\v') tabs, +// and newline ('\n') or formfeed ('\f') characters; both newline and +// formfeed act as line breaks. +// +// Tab-terminated cells in contiguous lines constitute a column. The +// Writer inserts padding as needed to make all cells in a column have +// the same width, effectively aligning the columns. It assumes that +// all characters have the same width, except for tabs for which a +// tabwidth must be specified. Column cells must be tab-terminated, not +// tab-separated: non-tab terminated trailing text at the end of a line +// forms a cell but that cell is not part of an aligned column. +// For instance, in this example (where | stands for a horizontal tab): +// +// aaaa|bbb|d +// aa |b |dd +// a | +// aa |cccc|eee +// +// the b and c are in distinct columns (the b column is not contiguous +// all the way). The d and e are not in a column at all (there's no +// terminating tab, nor would the column be contiguous). +// +// The Writer assumes that all Unicode code points have the same width; +// this may not be true in some fonts or if the string contains combining +// characters. +// +// If DiscardEmptyColumns is set, empty columns that are terminated +// entirely by vertical (or "soft") tabs are discarded. Columns +// terminated by horizontal (or "hard") tabs are not affected by +// this flag. +// +// If a Writer is configured to filter HTML, HTML tags and entities +// are passed through. The widths of tags and entities are +// assumed to be zero (tags) and one (entities) for formatting purposes. +// +// A segment of text may be escaped by bracketing it with Escape +// characters. The tabwriter passes escaped text segments through +// unchanged. In particular, it does not interpret any tabs or line +// breaks within the segment. If the StripEscape flag is set, the +// Escape characters are stripped from the output; otherwise they +// are passed through as well. For the purpose of formatting, the +// width of the escaped text is always computed excluding the Escape +// characters. +// +// The formfeed character acts like a newline but it also terminates +// all columns in the current line (effectively calling Flush). Tab- +// terminated cells in the next line start new columns. Unless found +// inside an HTML tag or inside an escaped text segment, formfeed +// characters appear as newlines in the output. +// +// The Writer must buffer input internally, because proper spacing +// of one line may depend on the cells in future lines. Clients must +// call Flush when done calling Write. +type Writer struct { + // configuration + output io.Writer + minwidth int + tabwidth int + padding int + padbytes [8]byte + flags uint + + // current state + buf []byte // collected text excluding tabs or line breaks + pos int // buffer position up to which cell.width of incomplete cell has been computed + cell cell // current incomplete cell; cell.width is up to buf[pos] excluding ignored sections + endChar byte // terminating char of escaped sequence (Escape for escapes, '>', ';' for HTML tags/entities, or 0) + lines [][]cell // list of lines; each line is a list of cells + widths []int // list of column widths in runes - re-used during formatting +} + +// addLine adds a new line. +// flushed is a hint indicating whether the underlying writer was just flushed. +// If so, the previous line is not likely to be a good indicator of the new line's cells. +func (b *Writer) addLine(flushed bool) { + // Grow slice instead of appending, + // as that gives us an opportunity + // to re-use an existing []cell. + if n := len(b.lines) + 1; n <= cap(b.lines) { + b.lines = b.lines[:n] + b.lines[n-1] = b.lines[n-1][:0] + } else { + b.lines = append(b.lines, nil) + } + + if !flushed { + // The previous line is probably a good indicator + // of how many cells the current line will have. + // If the current line's capacity is smaller than that, + // abandon it and make a new one. + if n := len(b.lines); n >= 2 { + if prev := len(b.lines[n-2]); prev > cap(b.lines[n-1]) { + b.lines[n-1] = make([]cell, 0, prev) + } + } + } +} + +// Reset the current state. +func (b *Writer) reset() { + b.buf = b.buf[:0] + b.pos = 0 + b.cell = cell{} + b.endChar = 0 + b.lines = b.lines[0:0] + b.widths = b.widths[0:0] + b.addLine(true) +} + +// Internal representation (current state): +// +// - all text written is appended to buf; tabs and line breaks are stripped away +// - at any given time there is a (possibly empty) incomplete cell at the end +// (the cell starts after a tab or line break) +// - cell.size is the number of bytes belonging to the cell so far +// - cell.width is text width in runes of that cell from the start of the cell to +// position pos; html tags and entities are excluded from this width if html +// filtering is enabled +// - the sizes and widths of processed text are kept in the lines list +// which contains a list of cells for each line +// - the widths list is a temporary list with current widths used during +// formatting; it is kept in Writer because it's re-used +// +// |<---------- size ---------->| +// | | +// |<- width ->|<- ignored ->| | +// | | | | +// [---processed---tab------------<tag>...</tag>...] +// ^ ^ ^ +// | | | +// buf start of incomplete cell pos + +// Formatting can be controlled with these flags. +const ( + // Ignore html tags and treat entities (starting with '&' + // and ending in ';') as single characters (width = 1). + FilterHTML uint = 1 << iota + + // Strip Escape characters bracketing escaped text segments + // instead of passing them through unchanged with the text. + StripEscape + + // Force right-alignment of cell content. + // Default is left-alignment. + AlignRight + + // Handle empty columns as if they were not present in + // the input in the first place. + DiscardEmptyColumns + + // Always use tabs for indentation columns (i.e., padding of + // leading empty cells on the left) independent of padchar. + TabIndent + + // Print a vertical bar ('|') between columns (after formatting). + // Discarded columns appear as zero-width columns ("||"). + Debug +) + +// A Writer must be initialized with a call to Init. The first parameter (output) +// specifies the filter output. The remaining parameters control the formatting: +// +// minwidth minimal cell width including any padding +// tabwidth width of tab characters (equivalent number of spaces) +// padding padding added to a cell before computing its width +// padchar ASCII char used for padding +// if padchar == '\t', the Writer will assume that the +// width of a '\t' in the formatted output is tabwidth, +// and cells are left-aligned independent of align_left +// (for correct-looking results, tabwidth must correspond +// to the tab width in the viewer displaying the result) +// flags formatting control +func (b *Writer) Init(output io.Writer, minwidth, tabwidth, padding int, padchar byte, flags uint) *Writer { + if minwidth < 0 || tabwidth < 0 || padding < 0 { + panic("negative minwidth, tabwidth, or padding") + } + b.output = output + b.minwidth = minwidth + b.tabwidth = tabwidth + b.padding = padding + for i := range b.padbytes { + b.padbytes[i] = padchar + } + if padchar == '\t' { + // tab padding enforces left-alignment + flags &^= AlignRight + } + b.flags = flags + + b.reset() + + return b +} + +// debugging support (keep code around) +func (b *Writer) dump() { + pos := 0 + for i, line := range b.lines { + print("(", i, ") ") + for _, c := range line { + print("[", string(b.buf[pos:pos+c.size]), "]") + pos += c.size + } + print("\n") + } + print("\n") +} + +// local error wrapper so we can distinguish errors we want to return +// as errors from genuine panics (which we don't want to return as errors) +type osError struct { + err error +} + +func (b *Writer) write0(buf []byte) { + n, err := b.output.Write(buf) + if n != len(buf) && err == nil { + err = io.ErrShortWrite + } + if err != nil { + panic(osError{err}) + } +} + +func (b *Writer) writeN(src []byte, n int) { + for n > len(src) { + b.write0(src) + n -= len(src) + } + b.write0(src[0:n]) +} + +var ( + newline = []byte{'\n'} + tabs = []byte("\t\t\t\t\t\t\t\t") +) + +func (b *Writer) writePadding(textw, cellw int, useTabs bool) { + if b.padbytes[0] == '\t' || useTabs { + // padding is done with tabs + if b.tabwidth == 0 { + return // tabs have no width - can't do any padding + } + // make cellw the smallest multiple of b.tabwidth + cellw = (cellw + b.tabwidth - 1) / b.tabwidth * b.tabwidth + n := cellw - textw // amount of padding + if n < 0 { + panic("internal error") + } + b.writeN(tabs, (n+b.tabwidth-1)/b.tabwidth) + return + } + + // padding is done with non-tab characters + b.writeN(b.padbytes[0:], cellw-textw) +} + +var vbar = []byte{'|'} + +func (b *Writer) writeLines(pos0 int, line0, line1 int) (pos int) { + pos = pos0 + for i := line0; i < line1; i++ { + line := b.lines[i] + + // if TabIndent is set, use tabs to pad leading empty cells + useTabs := b.flags&TabIndent != 0 + + for j, c := range line { + if j > 0 && b.flags&Debug != 0 { + // indicate column break + b.write0(vbar) + } + + if c.size == 0 { + // empty cell + if j < len(b.widths) { + b.writePadding(c.width, b.widths[j], useTabs) + } + } else { + // non-empty cell + useTabs = false + if b.flags&AlignRight == 0 { // align left + b.write0(b.buf[pos : pos+c.size]) + pos += c.size + if j < len(b.widths) { + b.writePadding(c.width, b.widths[j], false) + } + } else { // align right + if j < len(b.widths) { + b.writePadding(c.width, b.widths[j], false) + } + b.write0(b.buf[pos : pos+c.size]) + pos += c.size + } + } + } + + if i+1 == len(b.lines) { + // last buffered line - we don't have a newline, so just write + // any outstanding buffered data + b.write0(b.buf[pos : pos+b.cell.size]) + pos += b.cell.size + } else { + // not the last line - write newline + b.write0(newline) + } + } + return +} + +// Format the text between line0 and line1 (excluding line1); pos +// is the buffer position corresponding to the beginning of line0. +// Returns the buffer position corresponding to the beginning of +// line1 and an error, if any. +func (b *Writer) format(pos0 int, line0, line1 int) (pos int) { + pos = pos0 + column := len(b.widths) + for this := line0; this < line1; this++ { + line := b.lines[this] + + if column >= len(line)-1 { + continue + } + // cell exists in this column => this line + // has more cells than the previous line + // (the last cell per line is ignored because cells are + // tab-terminated; the last cell per line describes the + // text before the newline/formfeed and does not belong + // to a column) + + // print unprinted lines until beginning of block + pos = b.writeLines(pos, line0, this) + line0 = this + + // column block begin + width := b.minwidth // minimal column width + discardable := true // true if all cells in this column are empty and "soft" + for ; this < line1; this++ { + line = b.lines[this] + if column >= len(line)-1 { + break + } + // cell exists in this column + c := line[column] + // update width + if w := c.width + b.padding; w > width { + width = w + } + // update discardable + if c.width > 0 || c.htab { + discardable = false + } + } + // column block end + + // discard empty columns if necessary + if discardable && b.flags&DiscardEmptyColumns != 0 { + width = 0 + } + + // format and print all columns to the right of this column + // (we know the widths of this column and all columns to the left) + b.widths = append(b.widths, width) // push width + pos = b.format(pos, line0, this) + b.widths = b.widths[0 : len(b.widths)-1] // pop width + line0 = this + } + + // print unprinted lines until end + return b.writeLines(pos, line0, line1) +} + +// Append text to current cell. +func (b *Writer) append(text []byte) { + b.buf = append(b.buf, text...) + b.cell.size += len(text) +} + +// Update the cell width. +func (b *Writer) updateWidth() { + b.cell.width += utf8.RuneCount(b.buf[b.pos:]) + b.pos = len(b.buf) +} + +// To escape a text segment, bracket it with Escape characters. +// For instance, the tab in this string "Ignore this tab: \xff\t\xff" +// does not terminate a cell and constitutes a single character of +// width one for formatting purposes. +// +// The value 0xff was chosen because it cannot appear in a valid UTF-8 sequence. +const Escape = '\xff' + +// Start escaped mode. +func (b *Writer) startEscape(ch byte) { + switch ch { + case Escape: + b.endChar = Escape + case '<': + b.endChar = '>' + case '&': + b.endChar = ';' + } +} + +// Terminate escaped mode. If the escaped text was an HTML tag, its width +// is assumed to be zero for formatting purposes; if it was an HTML entity, +// its width is assumed to be one. In all other cases, the width is the +// unicode width of the text. +func (b *Writer) endEscape() { + switch b.endChar { + case Escape: + b.updateWidth() + if b.flags&StripEscape == 0 { + b.cell.width -= 2 // don't count the Escape chars + } + case '>': // tag of zero width + case ';': + b.cell.width++ // entity, count as one rune + } + b.pos = len(b.buf) + b.endChar = 0 +} + +// Terminate the current cell by adding it to the list of cells of the +// current line. Returns the number of cells in that line. +func (b *Writer) terminateCell(htab bool) int { + b.cell.htab = htab + line := &b.lines[len(b.lines)-1] + *line = append(*line, b.cell) + b.cell = cell{} + return len(*line) +} + +func (b *Writer) handlePanic(err *error, op string) { + if e := recover(); e != nil { + if op == "Flush" { + // If Flush ran into a panic, we still need to reset. + b.reset() + } + if nerr, ok := e.(osError); ok { + *err = nerr.err + return + } + panic("tabwriter: panic during " + op) + } +} + +// Flush should be called after the last call to Write to ensure +// that any data buffered in the Writer is written to output. Any +// incomplete escape sequence at the end is considered +// complete for formatting purposes. +func (b *Writer) Flush() error { + return b.flush() +} + +// flush is the internal version of Flush, with a named return value which we +// don't want to expose. +func (b *Writer) flush() (err error) { + defer b.handlePanic(&err, "Flush") + b.flushNoDefers() + return nil +} + +// flushNoDefers is like flush, but without a deferred handlePanic call. This +// can be called from other methods which already have their own deferred +// handlePanic calls, such as Write, and avoid the extra defer work. +func (b *Writer) flushNoDefers() { + // add current cell if not empty + if b.cell.size > 0 { + if b.endChar != 0 { + // inside escape - terminate it even if incomplete + b.endEscape() + } + b.terminateCell(false) + } + + // format contents of buffer + b.format(0, 0, len(b.lines)) + b.reset() +} + +var hbar = []byte("---\n") + +// Write writes buf to the writer b. +// The only errors returned are ones encountered +// while writing to the underlying output stream. +func (b *Writer) Write(buf []byte) (n int, err error) { + defer b.handlePanic(&err, "Write") + + // split text into cells + n = 0 + for i, ch := range buf { + if b.endChar == 0 { + // outside escape + switch ch { + case '\t', '\v', '\n', '\f': + // end of cell + b.append(buf[n:i]) + b.updateWidth() + n = i + 1 // ch consumed + ncells := b.terminateCell(ch == '\t') + if ch == '\n' || ch == '\f' { + // terminate line + b.addLine(ch == '\f') + if ch == '\f' || ncells == 1 { + // A '\f' always forces a flush. Otherwise, if the previous + // line has only one cell which does not have an impact on + // the formatting of the following lines (the last cell per + // line is ignored by format()), thus we can flush the + // Writer contents. + b.flushNoDefers() + if ch == '\f' && b.flags&Debug != 0 { + // indicate section break + b.write0(hbar) + } + } + } + + case Escape: + // start of escaped sequence + b.append(buf[n:i]) + b.updateWidth() + n = i + if b.flags&StripEscape != 0 { + n++ // strip Escape + } + b.startEscape(Escape) + + case '<', '&': + // possibly an html tag/entity + if b.flags&FilterHTML != 0 { + // begin of tag/entity + b.append(buf[n:i]) + b.updateWidth() + n = i + b.startEscape(ch) + } + } + + } else { + // inside escape + if ch == b.endChar { + // end of tag/entity + j := i + 1 + if ch == Escape && b.flags&StripEscape != 0 { + j = i // strip Escape + } + b.append(buf[n:j]) + n = i + 1 // ch consumed + b.endEscape() + } + } + } + + // append leftover text + b.append(buf[n:]) + n = len(buf) + return +} + +// NewWriter allocates and initializes a new tabwriter.Writer. +// The parameters are the same as for the Init function. +func NewWriter(output io.Writer, minwidth, tabwidth, padding int, padchar byte, flags uint) *Writer { + return new(Writer).Init(output, minwidth, tabwidth, padding, padchar, flags) +} diff --git a/src/text/tabwriter/tabwriter_test.go b/src/text/tabwriter/tabwriter_test.go new file mode 100644 index 0000000..a51358d --- /dev/null +++ b/src/text/tabwriter/tabwriter_test.go @@ -0,0 +1,754 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tabwriter_test + +import ( + "bytes" + "fmt" + "io" + "testing" + . "text/tabwriter" +) + +type buffer struct { + a []byte +} + +func (b *buffer) init(n int) { b.a = make([]byte, 0, n) } + +func (b *buffer) clear() { b.a = b.a[0:0] } + +func (b *buffer) Write(buf []byte) (written int, err error) { + n := len(b.a) + m := len(buf) + if n+m <= cap(b.a) { + b.a = b.a[0 : n+m] + for i := 0; i < m; i++ { + b.a[n+i] = buf[i] + } + } else { + panic("buffer.Write: buffer too small") + } + return len(buf), nil +} + +func (b *buffer) String() string { return string(b.a) } + +func write(t *testing.T, testname string, w *Writer, src string) { + written, err := io.WriteString(w, src) + if err != nil { + t.Errorf("--- test: %s\n--- src:\n%q\n--- write error: %v\n", testname, src, err) + } + if written != len(src) { + t.Errorf("--- test: %s\n--- src:\n%q\n--- written = %d, len(src) = %d\n", testname, src, written, len(src)) + } +} + +func verify(t *testing.T, testname string, w *Writer, b *buffer, src, expected string) { + err := w.Flush() + if err != nil { + t.Errorf("--- test: %s\n--- src:\n%q\n--- flush error: %v\n", testname, src, err) + } + + res := b.String() + if res != expected { + t.Errorf("--- test: %s\n--- src:\n%q\n--- found:\n%q\n--- expected:\n%q\n", testname, src, res, expected) + } +} + +func check(t *testing.T, testname string, minwidth, tabwidth, padding int, padchar byte, flags uint, src, expected string) { + var b buffer + b.init(1000) + + var w Writer + w.Init(&b, minwidth, tabwidth, padding, padchar, flags) + + // write all at once + title := testname + " (written all at once)" + b.clear() + write(t, title, &w, src) + verify(t, title, &w, &b, src, expected) + + // write byte-by-byte + title = testname + " (written byte-by-byte)" + b.clear() + for i := 0; i < len(src); i++ { + write(t, title, &w, src[i:i+1]) + } + verify(t, title, &w, &b, src, expected) + + // write using Fibonacci slice sizes + title = testname + " (written in fibonacci slices)" + b.clear() + for i, d := 0, 0; i < len(src); { + write(t, title, &w, src[i:i+d]) + i, d = i+d, d+1 + if i+d > len(src) { + d = len(src) - i + } + } + verify(t, title, &w, &b, src, expected) +} + +var tests = []struct { + testname string + minwidth, tabwidth, padding int + padchar byte + flags uint + src, expected string +}{ + { + "1a", + 8, 0, 1, '.', 0, + "", + "", + }, + + { + "1a debug", + 8, 0, 1, '.', Debug, + "", + "", + }, + + { + "1b esc stripped", + 8, 0, 1, '.', StripEscape, + "\xff\xff", + "", + }, + + { + "1b esc", + 8, 0, 1, '.', 0, + "\xff\xff", + "\xff\xff", + }, + + { + "1c esc stripped", + 8, 0, 1, '.', StripEscape, + "\xff\t\xff", + "\t", + }, + + { + "1c esc", + 8, 0, 1, '.', 0, + "\xff\t\xff", + "\xff\t\xff", + }, + + { + "1d esc stripped", + 8, 0, 1, '.', StripEscape, + "\xff\"foo\t\n\tbar\"\xff", + "\"foo\t\n\tbar\"", + }, + + { + "1d esc", + 8, 0, 1, '.', 0, + "\xff\"foo\t\n\tbar\"\xff", + "\xff\"foo\t\n\tbar\"\xff", + }, + + { + "1e esc stripped", + 8, 0, 1, '.', StripEscape, + "abc\xff\tdef", // unterminated escape + "abc\tdef", + }, + + { + "1e esc", + 8, 0, 1, '.', 0, + "abc\xff\tdef", // unterminated escape + "abc\xff\tdef", + }, + + { + "2", + 8, 0, 1, '.', 0, + "\n\n\n", + "\n\n\n", + }, + + { + "3", + 8, 0, 1, '.', 0, + "a\nb\nc", + "a\nb\nc", + }, + + { + "4a", + 8, 0, 1, '.', 0, + "\t", // '\t' terminates an empty cell on last line - nothing to print + "", + }, + + { + "4b", + 8, 0, 1, '.', AlignRight, + "\t", // '\t' terminates an empty cell on last line - nothing to print + "", + }, + + { + "5", + 8, 0, 1, '.', 0, + "*\t*", + "*.......*", + }, + + { + "5b", + 8, 0, 1, '.', 0, + "*\t*\n", + "*.......*\n", + }, + + { + "5c", + 8, 0, 1, '.', 0, + "*\t*\t", + "*.......*", + }, + + { + "5c debug", + 8, 0, 1, '.', Debug, + "*\t*\t", + "*.......|*", + }, + + { + "5d", + 8, 0, 1, '.', AlignRight, + "*\t*\t", + ".......**", + }, + + { + "6", + 8, 0, 1, '.', 0, + "\t\n", + "........\n", + }, + + { + "7a", + 8, 0, 1, '.', 0, + "a) foo", + "a) foo", + }, + + { + "7b", + 8, 0, 1, ' ', 0, + "b) foo\tbar", + "b) foo bar", + }, + + { + "7c", + 8, 0, 1, '.', 0, + "c) foo\tbar\t", + "c) foo..bar", + }, + + { + "7d", + 8, 0, 1, '.', 0, + "d) foo\tbar\n", + "d) foo..bar\n", + }, + + { + "7e", + 8, 0, 1, '.', 0, + "e) foo\tbar\t\n", + "e) foo..bar.....\n", + }, + + { + "7f", + 8, 0, 1, '.', FilterHTML, + "f) f<o\t<b>bar</b>\t\n", + "f) f<o..<b>bar</b>.....\n", + }, + + { + "7g", + 8, 0, 1, '.', FilterHTML, + "g) f<o\t<b>bar</b>\t non-terminated entity &", + "g) f<o..<b>bar</b>..... non-terminated entity &", + }, + + { + "7g debug", + 8, 0, 1, '.', FilterHTML | Debug, + "g) f<o\t<b>bar</b>\t non-terminated entity &", + "g) f<o..|<b>bar</b>.....| non-terminated entity &", + }, + + { + "8", + 8, 0, 1, '*', 0, + "Hello, world!\n", + "Hello, world!\n", + }, + + { + "9a", + 1, 0, 0, '.', 0, + "1\t2\t3\t4\n" + + "11\t222\t3333\t44444\n", + + "1.2..3...4\n" + + "11222333344444\n", + }, + + { + "9b", + 1, 0, 0, '.', FilterHTML, + "1\t2<!---\f--->\t3\t4\n" + // \f inside HTML is ignored + "11\t222\t3333\t44444\n", + + "1.2<!---\f--->..3...4\n" + + "11222333344444\n", + }, + + { + "9c", + 1, 0, 0, '.', 0, + "1\t2\t3\t4\f" + // \f causes a newline and flush + "11\t222\t3333\t44444\n", + + "1234\n" + + "11222333344444\n", + }, + + { + "9c debug", + 1, 0, 0, '.', Debug, + "1\t2\t3\t4\f" + // \f causes a newline and flush + "11\t222\t3333\t44444\n", + + "1|2|3|4\n" + + "---\n" + + "11|222|3333|44444\n", + }, + + { + "10a", + 5, 0, 0, '.', 0, + "1\t2\t3\t4\n", + "1....2....3....4\n", + }, + + { + "10b", + 5, 0, 0, '.', 0, + "1\t2\t3\t4\t\n", + "1....2....3....4....\n", + }, + + { + "11", + 8, 0, 1, '.', 0, + "本\tb\tc\n" + + "aa\t\u672c\u672c\u672c\tcccc\tddddd\n" + + "aaa\tbbbb\n", + + "本.......b.......c\n" + + "aa......本本本.....cccc....ddddd\n" + + "aaa.....bbbb\n", + }, + + { + "12a", + 8, 0, 1, ' ', AlignRight, + "a\tè\tc\t\n" + + "aa\tèèè\tcccc\tddddd\t\n" + + "aaa\tèèèè\t\n", + + " a è c\n" + + " aa èèè cccc ddddd\n" + + " aaa èèèè\n", + }, + + { + "12b", + 2, 0, 0, ' ', 0, + "a\tb\tc\n" + + "aa\tbbb\tcccc\n" + + "aaa\tbbbb\n", + + "a b c\n" + + "aa bbbcccc\n" + + "aaabbbb\n", + }, + + { + "12c", + 8, 0, 1, '_', 0, + "a\tb\tc\n" + + "aa\tbbb\tcccc\n" + + "aaa\tbbbb\n", + + "a_______b_______c\n" + + "aa______bbb_____cccc\n" + + "aaa_____bbbb\n", + }, + + { + "13a", + 4, 0, 1, '-', 0, + "4444\t日本語\t22\t1\t333\n" + + "999999999\t22\n" + + "7\t22\n" + + "\t\t\t88888888\n" + + "\n" + + "666666\t666666\t666666\t4444\n" + + "1\t1\t999999999\t0000000000\n", + + "4444------日本語-22--1---333\n" + + "999999999-22\n" + + "7---------22\n" + + "------------------88888888\n" + + "\n" + + "666666-666666-666666----4444\n" + + "1------1------999999999-0000000000\n", + }, + + { + "13b", + 4, 0, 3, '.', 0, + "4444\t333\t22\t1\t333\n" + + "999999999\t22\n" + + "7\t22\n" + + "\t\t\t88888888\n" + + "\n" + + "666666\t666666\t666666\t4444\n" + + "1\t1\t999999999\t0000000000\n", + + "4444........333...22...1...333\n" + + "999999999...22\n" + + "7...........22\n" + + "....................88888888\n" + + "\n" + + "666666...666666...666666......4444\n" + + "1........1........999999999...0000000000\n", + }, + + { + "13c", + 8, 8, 1, '\t', FilterHTML, + "4444\t333\t22\t1\t333\n" + + "999999999\t22\n" + + "7\t22\n" + + "\t\t\t88888888\n" + + "\n" + + "666666\t666666\t666666\t4444\n" + + "1\t1\t<font color=red attr=日本語>999999999</font>\t0000000000\n", + + "4444\t\t333\t22\t1\t333\n" + + "999999999\t22\n" + + "7\t\t22\n" + + "\t\t\t\t88888888\n" + + "\n" + + "666666\t666666\t666666\t\t4444\n" + + "1\t1\t<font color=red attr=日本語>999999999</font>\t0000000000\n", + }, + + { + "14", + 1, 0, 2, ' ', AlignRight, + ".0\t.3\t2.4\t-5.1\t\n" + + "23.0\t12345678.9\t2.4\t-989.4\t\n" + + "5.1\t12.0\t2.4\t-7.0\t\n" + + ".0\t0.0\t332.0\t8908.0\t\n" + + ".0\t-.3\t456.4\t22.1\t\n" + + ".0\t1.2\t44.4\t-13.3\t\t", + + " .0 .3 2.4 -5.1\n" + + " 23.0 12345678.9 2.4 -989.4\n" + + " 5.1 12.0 2.4 -7.0\n" + + " .0 0.0 332.0 8908.0\n" + + " .0 -.3 456.4 22.1\n" + + " .0 1.2 44.4 -13.3", + }, + + { + "14 debug", + 1, 0, 2, ' ', AlignRight | Debug, + ".0\t.3\t2.4\t-5.1\t\n" + + "23.0\t12345678.9\t2.4\t-989.4\t\n" + + "5.1\t12.0\t2.4\t-7.0\t\n" + + ".0\t0.0\t332.0\t8908.0\t\n" + + ".0\t-.3\t456.4\t22.1\t\n" + + ".0\t1.2\t44.4\t-13.3\t\t", + + " .0| .3| 2.4| -5.1|\n" + + " 23.0| 12345678.9| 2.4| -989.4|\n" + + " 5.1| 12.0| 2.4| -7.0|\n" + + " .0| 0.0| 332.0| 8908.0|\n" + + " .0| -.3| 456.4| 22.1|\n" + + " .0| 1.2| 44.4| -13.3|", + }, + + { + "15a", + 4, 0, 0, '.', 0, + "a\t\tb", + "a.......b", + }, + + { + "15b", + 4, 0, 0, '.', DiscardEmptyColumns, + "a\t\tb", // htabs - do not discard column + "a.......b", + }, + + { + "15c", + 4, 0, 0, '.', DiscardEmptyColumns, + "a\v\vb", + "a...b", + }, + + { + "15d", + 4, 0, 0, '.', AlignRight | DiscardEmptyColumns, + "a\v\vb", + "...ab", + }, + + { + "16a", + 100, 100, 0, '\t', 0, + "a\tb\t\td\n" + + "a\tb\t\td\te\n" + + "a\n" + + "a\tb\tc\td\n" + + "a\tb\tc\td\te\n", + + "a\tb\t\td\n" + + "a\tb\t\td\te\n" + + "a\n" + + "a\tb\tc\td\n" + + "a\tb\tc\td\te\n", + }, + + { + "16b", + 100, 100, 0, '\t', DiscardEmptyColumns, + "a\vb\v\vd\n" + + "a\vb\v\vd\ve\n" + + "a\n" + + "a\vb\vc\vd\n" + + "a\vb\vc\vd\ve\n", + + "a\tb\td\n" + + "a\tb\td\te\n" + + "a\n" + + "a\tb\tc\td\n" + + "a\tb\tc\td\te\n", + }, + + { + "16b debug", + 100, 100, 0, '\t', DiscardEmptyColumns | Debug, + "a\vb\v\vd\n" + + "a\vb\v\vd\ve\n" + + "a\n" + + "a\vb\vc\vd\n" + + "a\vb\vc\vd\ve\n", + + "a\t|b\t||d\n" + + "a\t|b\t||d\t|e\n" + + "a\n" + + "a\t|b\t|c\t|d\n" + + "a\t|b\t|c\t|d\t|e\n", + }, + + { + "16c", + 100, 100, 0, '\t', DiscardEmptyColumns, + "a\tb\t\td\n" + // hard tabs - do not discard column + "a\tb\t\td\te\n" + + "a\n" + + "a\tb\tc\td\n" + + "a\tb\tc\td\te\n", + + "a\tb\t\td\n" + + "a\tb\t\td\te\n" + + "a\n" + + "a\tb\tc\td\n" + + "a\tb\tc\td\te\n", + }, + + { + "16c debug", + 100, 100, 0, '\t', DiscardEmptyColumns | Debug, + "a\tb\t\td\n" + // hard tabs - do not discard column + "a\tb\t\td\te\n" + + "a\n" + + "a\tb\tc\td\n" + + "a\tb\tc\td\te\n", + + "a\t|b\t|\t|d\n" + + "a\t|b\t|\t|d\t|e\n" + + "a\n" + + "a\t|b\t|c\t|d\n" + + "a\t|b\t|c\t|d\t|e\n", + }, +} + +func Test(t *testing.T) { + for _, e := range tests { + check(t, e.testname, e.minwidth, e.tabwidth, e.padding, e.padchar, e.flags, e.src, e.expected) + } +} + +type panicWriter struct{} + +func (panicWriter) Write([]byte) (int, error) { + panic("cannot write") +} + +func wantPanicString(t *testing.T, want string) { + if e := recover(); e != nil { + got, ok := e.(string) + switch { + case !ok: + t.Errorf("got %v (%T), want panic string", e, e) + case got != want: + t.Errorf("wrong panic message: got %q, want %q", got, want) + } + } +} + +func TestPanicDuringFlush(t *testing.T) { + defer wantPanicString(t, "tabwriter: panic during Flush") + var p panicWriter + w := new(Writer) + w.Init(p, 0, 0, 5, ' ', 0) + io.WriteString(w, "a") + w.Flush() + t.Errorf("failed to panic during Flush") +} + +func TestPanicDuringWrite(t *testing.T) { + defer wantPanicString(t, "tabwriter: panic during Write") + var p panicWriter + w := new(Writer) + w.Init(p, 0, 0, 5, ' ', 0) + io.WriteString(w, "a\n\n") // the second \n triggers a call to w.Write and thus a panic + t.Errorf("failed to panic during Write") +} + +func BenchmarkTable(b *testing.B) { + for _, w := range [...]int{1, 10, 100} { + // Build a line with w cells. + line := bytes.Repeat([]byte("a\t"), w) + line = append(line, '\n') + for _, h := range [...]int{10, 1000, 100000} { + b.Run(fmt.Sprintf("%dx%d", w, h), func(b *testing.B) { + b.Run("new", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + w := NewWriter(io.Discard, 4, 4, 1, ' ', 0) // no particular reason for these settings + // Write the line h times. + for j := 0; j < h; j++ { + w.Write(line) + } + w.Flush() + } + }) + + b.Run("reuse", func(b *testing.B) { + b.ReportAllocs() + w := NewWriter(io.Discard, 4, 4, 1, ' ', 0) // no particular reason for these settings + for i := 0; i < b.N; i++ { + // Write the line h times. + for j := 0; j < h; j++ { + w.Write(line) + } + w.Flush() + } + }) + }) + } + } +} + +func BenchmarkPyramid(b *testing.B) { + for _, x := range [...]int{10, 100, 1000} { + // Build a line with x cells. + line := bytes.Repeat([]byte("a\t"), x) + b.Run(fmt.Sprintf("%d", x), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + w := NewWriter(io.Discard, 4, 4, 1, ' ', 0) // no particular reason for these settings + // Write increasing prefixes of that line. + for j := 0; j < x; j++ { + w.Write(line[:j*2]) + w.Write([]byte{'\n'}) + } + w.Flush() + } + }) + } +} + +func BenchmarkRagged(b *testing.B) { + var lines [8][]byte + for i, w := range [8]int{6, 2, 9, 5, 5, 7, 3, 8} { + // Build a line with w cells. + lines[i] = bytes.Repeat([]byte("a\t"), w) + } + for _, h := range [...]int{10, 100, 1000} { + b.Run(fmt.Sprintf("%d", h), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + w := NewWriter(io.Discard, 4, 4, 1, ' ', 0) // no particular reason for these settings + // Write the lines in turn h times. + for j := 0; j < h; j++ { + w.Write(lines[j%len(lines)]) + w.Write([]byte{'\n'}) + } + w.Flush() + } + }) + } +} + +const codeSnippet = ` +some command + +foo # aligned +barbaz # comments + +but +mostly +single +cell +lines +` + +func BenchmarkCode(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + w := NewWriter(io.Discard, 4, 4, 1, ' ', 0) // no particular reason for these settings + // The code is small, so it's reasonable for the tabwriter user + // to write it all at once, or buffer the writes. + w.Write([]byte(codeSnippet)) + w.Flush() + } +} |