github.com/liamawhite/cli-with-i18n@v6.32.1-0.20171122084555-dede0a5c3448+incompatible/cf/terminal/table.go (about)

     1  package terminal
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"strings"
     8  )
     9  
    10  // PrintableTable is an implementation of the Table interface. It
    11  // remembers the headers, the added rows, the column widths, and a
    12  // number of other things.
    13  type Table struct {
    14  	ui            UI
    15  	headers       []string
    16  	headerPrinted bool
    17  	columnWidth   []int
    18  	rowHeight     []int
    19  	rows          [][]string
    20  	colSpacing    string
    21  	transformer   []Transformer
    22  }
    23  
    24  // Transformer is the type of functions used to modify the content of
    25  // a table cell for actual display. For multi-line content of a cell
    26  // the transformation is applied to each individual line.
    27  type Transformer func(s string) string
    28  
    29  // NewTable is the constructor function creating a new printable table
    30  // from a list of headers. The table is also connected to a UI, which
    31  // is where it will print itself to on demand.
    32  func NewTable(headers []string) *Table {
    33  	pt := &Table{
    34  		headers:     headers,
    35  		columnWidth: make([]int, len(headers)),
    36  		colSpacing:  "   ",
    37  		transformer: make([]Transformer, len(headers)),
    38  	}
    39  	// Standard colorization, column 0 is auto-highlighted as some
    40  	// name. Everything else has no transformation (== identity
    41  	// transform)
    42  	for i := range pt.transformer {
    43  		pt.transformer[i] = nop
    44  	}
    45  	if 0 < len(headers) {
    46  		pt.transformer[0] = TableContentHeaderColor
    47  	}
    48  	return pt
    49  }
    50  
    51  // NoHeaders disables the printing of the header row for the specified
    52  // table.
    53  func (t *Table) NoHeaders() {
    54  	// Fake the Print() code into the belief that the headers have
    55  	// been printed already.
    56  	t.headerPrinted = true
    57  }
    58  
    59  // SetTransformer specifies a string transformer to apply to the
    60  // content of the given column in the specified table.
    61  func (t *Table) SetTransformer(columnIndex int, tr Transformer) {
    62  	t.transformer[columnIndex] = tr
    63  }
    64  
    65  // Add extends the table by another row.
    66  func (t *Table) Add(row ...string) {
    67  	t.rows = append(t.rows, row)
    68  }
    69  
    70  // PrintTo is the core functionality for printing the table, placing
    71  // the formatted table into the writer given to it as argument. The
    72  // exported Print() is just a wrapper around this which redirects the
    73  // result into CF datastructures.
    74  func (t *Table) PrintTo(result io.Writer) error {
    75  	t.rowHeight = make([]int, len(t.rows)+1)
    76  
    77  	rowIndex := 0
    78  	if !t.headerPrinted {
    79  		// row transformer header row
    80  		err := t.calculateMaxSize(transHeader, rowIndex, t.headers)
    81  		if err != nil {
    82  			return err
    83  		}
    84  		rowIndex++
    85  	}
    86  
    87  	for _, row := range t.rows {
    88  		// table is row transformer itself, for content rows
    89  		err := t.calculateMaxSize(t, rowIndex, row)
    90  		if err != nil {
    91  			return err
    92  		}
    93  		rowIndex++
    94  	}
    95  
    96  	rowIndex = 0
    97  	if !t.headerPrinted {
    98  		err := t.printRow(result, transHeader, rowIndex, t.headers)
    99  		if err != nil {
   100  			return err
   101  		}
   102  		t.headerPrinted = true
   103  		rowIndex++
   104  	}
   105  
   106  	for row := range t.rows {
   107  		err := t.printRow(result, t, rowIndex, t.rows[row])
   108  		if err != nil {
   109  			return err
   110  		}
   111  		rowIndex++
   112  	}
   113  
   114  	// Note, printing a table clears it.
   115  	t.rows = [][]string{}
   116  	return nil
   117  }
   118  
   119  // calculateMaxSize iterates over the collected rows of the specified
   120  // table, and their strings, determining the height of each row (in
   121  // lines), and the width of each column (in characters). The results
   122  // are stored in the table for use by Print.
   123  func (t *Table) calculateMaxSize(transformer rowTransformer, rowIndex int, row []string) error {
   124  
   125  	// Iterate columns
   126  	for columnIndex := range row {
   127  		// Truncate long row, ignore the additional fields.
   128  		if columnIndex >= len(t.headers) {
   129  			break
   130  		}
   131  
   132  		// Note that the length of the cell in characters is
   133  		// __not__ equivalent to its width.  Because it may be
   134  		// a multi-line value. We have to split the cell into
   135  		// lines and check the width of each such fragment.
   136  		// The number of lines founds also goes into the row
   137  		// height.
   138  
   139  		lines := strings.Split(row[columnIndex], "\n")
   140  		height := len(lines)
   141  
   142  		if t.rowHeight[rowIndex] < height {
   143  			t.rowHeight[rowIndex] = height
   144  		}
   145  
   146  		for i := range lines {
   147  			// (**) See also 'printCellValue' (pCV). Here
   148  			// and there we have to apply identical
   149  			// transformations to the cell value to get
   150  			// matching cell width information. If they do
   151  			// not match then pCV may compute a cell width
   152  			// larger than the max width found here, a
   153  			// negative padding length from that, and
   154  			// subsequently return an error.  What
   155  			// was further missing is trimming before
   156  			// entering the user-transform. Especially
   157  			// with color transforms any trailing space
   158  			// going in will not be removable for print.
   159  			//
   160  			// This happened for
   161  			// https://www.pivotaltracker.com/n/projects/892938/stories/117404629
   162  
   163  			value := trim(Decolorize(transformer.Transform(columnIndex, trim(lines[i]))))
   164  			width, err := visibleSize(value)
   165  			if err != nil {
   166  				return err
   167  			}
   168  			if t.columnWidth[columnIndex] < width {
   169  				t.columnWidth[columnIndex] = width
   170  			}
   171  		}
   172  	}
   173  	return nil
   174  }
   175  
   176  // printRow is responsible for the layouting, transforming and
   177  // printing of the string in a single row
   178  func (t *Table) printRow(result io.Writer, transformer rowTransformer, rowIndex int, row []string) error {
   179  
   180  	height := t.rowHeight[rowIndex]
   181  
   182  	// Compute the index of the last column as the min number of
   183  	// cells in the header and cells in the current row.
   184  	// Note: math.Min seems to be for float only :(
   185  	last := len(t.headers) - 1
   186  	lastr := len(row) - 1
   187  	if lastr < last {
   188  		last = lastr
   189  	}
   190  
   191  	// Note how we always print into a line buffer before placing
   192  	// the assembled line into the result. This allows us to trim
   193  	// superfluous trailing whitespace from the line before making
   194  	// it final.
   195  
   196  	if height <= 1 {
   197  		// Easy case, all cells in the row are single-line
   198  		line := &bytes.Buffer{}
   199  
   200  		for columnIndex := range row {
   201  			// Truncate long row, ignore the additional fields.
   202  			if columnIndex >= len(t.headers) {
   203  				break
   204  			}
   205  
   206  			err := t.printCellValue(line, transformer, columnIndex, last, row[columnIndex])
   207  			if err != nil {
   208  				return err
   209  			}
   210  		}
   211  
   212  		fmt.Fprintf(result, "%s\n", trim(string(line.Bytes())))
   213  		return nil
   214  	}
   215  
   216  	// We have at least one multi-line cell in this row.
   217  	// Treat it a bit like a mini-table.
   218  
   219  	// Step I. Fill the mini-table. Note how it is stored
   220  	//         column-major, not row-major.
   221  
   222  	// [column][row]string
   223  	sub := make([][]string, len(t.headers))
   224  	for columnIndex := range row {
   225  		// Truncate long row, ignore the additional fields.
   226  		if columnIndex >= len(t.headers) {
   227  			break
   228  		}
   229  		sub[columnIndex] = strings.Split(row[columnIndex], "\n")
   230  		// (*) Extend the column to the full height.
   231  		for len(sub[columnIndex]) < height {
   232  			sub[columnIndex] = append(sub[columnIndex], "")
   233  		}
   234  	}
   235  
   236  	// Step II. Iterate over the rows, then columns to
   237  	//          collect the output. This assumes that all
   238  	//          the rows in sub are the same height. See
   239  	//          (*) above where that is made true.
   240  
   241  	for rowIndex := range sub[0] {
   242  		line := &bytes.Buffer{}
   243  
   244  		for columnIndex := range sub {
   245  			err := t.printCellValue(line, transformer, columnIndex, last, sub[columnIndex][rowIndex])
   246  			if err != nil {
   247  				return err
   248  			}
   249  		}
   250  
   251  		fmt.Fprintf(result, "%s\n", trim(string(line.Bytes())))
   252  	}
   253  	return nil
   254  }
   255  
   256  // printCellValue pads the specified string to the width of the given
   257  // column, adds the spacing bewtween columns, and returns the result.
   258  func (t *Table) printCellValue(result io.Writer, transformer rowTransformer, col, last int, value string) error {
   259  	value = trim(transformer.Transform(col, trim(value)))
   260  	fmt.Fprint(result, value)
   261  
   262  	// Pad all columns, but the last in this row (with the size of
   263  	// the header row limiting this). This ensures that most of
   264  	// the irrelevant spacing is not printed. At the moment
   265  	// irrelevant spacing can only occur when printing a row with
   266  	// multi-line cells, introducing a physical short line for a
   267  	// long logical row. Getting rid of that requires fixing in
   268  	// printRow.
   269  	//
   270  	//  Note how the inter-column spacing is also irrelevant for
   271  	//  that last column.
   272  
   273  	if col < last {
   274  		// (**) See also 'calculateMaxSize' (cMS). Here and
   275  		// there we have to apply identical transformations to
   276  		// the cell value to get matching cell width
   277  		// information. If they do not match then we may here
   278  		// compute a cell width larger than the max width
   279  		// found by cMS, derive a negative padding length from
   280  		// that, and subsequently return an error. What was
   281  		// further missing is trimming before entering the
   282  		// user-transform. Especially with color transforms
   283  		// any trailing space going in will not be removable
   284  		// for print.
   285  		//
   286  		// This happened for
   287  		// https://www.pivotaltracker.com/n/projects/892938/stories/117404629
   288  
   289  		decolorizedLength, err := visibleSize(trim(Decolorize(value)))
   290  		if err != nil {
   291  			return err
   292  		}
   293  		padlen := t.columnWidth[col] - decolorizedLength
   294  		padding := strings.Repeat(" ", padlen)
   295  		fmt.Fprint(result, padding)
   296  		fmt.Fprint(result, t.colSpacing)
   297  	}
   298  	return nil
   299  }
   300  
   301  // rowTransformer is an interface behind which we can specify how to
   302  // transform the strings of an entire row on a column-by-column basis.
   303  type rowTransformer interface {
   304  	Transform(column int, s string) string
   305  }
   306  
   307  // transformHeader is an implementation of rowTransformer which
   308  // highlights all columns as a Header.
   309  type transformHeader struct{}
   310  
   311  // transHeader holds a package-global transformHeader to prevent us
   312  // from continuously allocating a literal of the type whenever we
   313  // print a header row. Instead all tables use this value.
   314  var transHeader = &transformHeader{}
   315  
   316  // Transform performs the Header highlighting for transformHeader
   317  func (th *transformHeader) Transform(column int, s string) string {
   318  	return HeaderColor(s)
   319  }
   320  
   321  // Transform makes a PrintableTable an implementation of
   322  // rowTransformer. It performs the per-column transformation for table
   323  // content, as specified during construction and/or overridden by the
   324  // user of the table, see SetTransformer.
   325  func (t *Table) Transform(column int, s string) string {
   326  	return t.transformer[column](s)
   327  }
   328  
   329  // nop is the identity transformation which does not transform the
   330  // string at all.
   331  func nop(s string) string {
   332  	return s
   333  }
   334  
   335  // trim is a helper to remove trailing whitespace from a string.
   336  func trim(s string) string {
   337  	return strings.TrimRight(s, " \t")
   338  }
   339  
   340  // visibleSize returns the number of columns the string will cover
   341  // when displayed in the terminal. This is the number of runes,
   342  // i.e. characters, not the number of bytes it consists of.
   343  func visibleSize(s string) (int, error) {
   344  	// This code re-implements the basic functionality of
   345  	// RuneCountInString to account for special cases. Namely
   346  	// UTF-8 characters taking up 3 bytes (**) appear as double-width.
   347  	//
   348  	// (**) I wonder if that is the set of characters outside of
   349  	// the BMP <=> the set of characters requiring surrogates (2
   350  	// slots) when encoded in UCS-2.
   351  
   352  	r := strings.NewReader(s)
   353  
   354  	var size int
   355  	for range s {
   356  		_, runeSize, err := r.ReadRune()
   357  		if err != nil {
   358  			return -1, fmt.Errorf("error when calculating visible size of: %s", s)
   359  		}
   360  
   361  		if runeSize == 3 {
   362  			size += 2 // Kanji and Katakana characters appear as double-width
   363  		} else {
   364  			size++
   365  		}
   366  	}
   367  
   368  	return size, nil
   369  }