| // 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 ( |
| "fmt" |
| "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 [Writer.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 [Writer.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(fmt.Sprintf("tabwriter: panic during %s (%v)", op, e)) |
| } |
| } |
| |
| // Flush should be called after the last call to [Writer.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 [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) |
| } |