...

Source file src/github.com/jedib0t/go-pretty/v6/table/table.go

Documentation: github.com/jedib0t/go-pretty/v6/table

     1  package table
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"strings"
     7  
     8  	"github.com/jedib0t/go-pretty/v6/text"
     9  )
    10  
    11  // Row defines a single row in the Table.
    12  type Row []interface{}
    13  
    14  func (r Row) findColumnNumber(colName string) int {
    15  	for colIdx, col := range r {
    16  		if fmt.Sprint(col) == colName {
    17  			return colIdx + 1
    18  		}
    19  	}
    20  	return 0
    21  }
    22  
    23  // RowPainter is a custom function that takes a Row as input and returns the
    24  // text.Colors{} to use on the entire row
    25  type RowPainter func(row Row) text.Colors
    26  
    27  // rowStr defines a single row in the Table comprised of just string objects.
    28  type rowStr []string
    29  
    30  // areEqual returns true if the contents of the 2 given columns are the same
    31  func (row rowStr) areEqual(colIdx1 int, colIdx2 int) bool {
    32  	return colIdx1 >= 0 && colIdx2 < len(row) && row[colIdx1] == row[colIdx2]
    33  }
    34  
    35  // Table helps print a 2-dimensional array in a human readable pretty-table.
    36  type Table struct {
    37  	// allowedRowLength is the max allowed length for a row (or line of output)
    38  	allowedRowLength int
    39  	// enable automatic indexing of the rows and columns like a spreadsheet?
    40  	autoIndex bool
    41  	// autoIndexVIndexMaxLength denotes the length in chars for the last rownum
    42  	autoIndexVIndexMaxLength int
    43  	// caption stores the text to be rendered just below the table; and doesn't
    44  	// get used when rendered as a CSV
    45  	caption string
    46  	// columnIsNonNumeric stores if a column contains non-numbers in all rows
    47  	columnIsNonNumeric []bool
    48  	// columnConfigs stores the custom-configuration for 1 or more columns
    49  	columnConfigs []ColumnConfig
    50  	// columnConfigMap stores the custom-configuration by column
    51  	// number and is generated before rendering
    52  	columnConfigMap map[int]ColumnConfig
    53  	// htmlCSSClass stores the HTML CSS Class to use on the <table> node
    54  	htmlCSSClass string
    55  	// indexColumn stores the number of the column considered as the "index"
    56  	indexColumn int
    57  	// maxColumnLengths stores the length of the longest line in each column
    58  	maxColumnLengths []int
    59  	// maxRowLength stores the length of the longest row
    60  	maxRowLength int
    61  	// numColumns stores the (max.) number of columns seen
    62  	numColumns int
    63  	// numLinesRendered keeps track of the number of lines rendered and helps in
    64  	// paginating long tables
    65  	numLinesRendered int
    66  	// outputMirror stores an io.Writer where the "Render" functions would write
    67  	outputMirror io.Writer
    68  	// pageSize stores the maximum lines to render before rendering the header
    69  	// again (to denote a page break) - useful when you are dealing with really
    70  	// long tables
    71  	pageSize int
    72  	// rows stores the rows that make up the body (in string form)
    73  	rows []rowStr
    74  	// rowsColors stores the text.Colors over-rides for each row as defined by
    75  	// rowPainter
    76  	rowsColors []text.Colors
    77  	// rowsConfigs stores RowConfig for each row
    78  	rowsConfigMap map[int]RowConfig
    79  	// rowsRaw stores the rows that make up the body
    80  	rowsRaw []Row
    81  	// rowsFooter stores the rows that make up the footer (in string form)
    82  	rowsFooter []rowStr
    83  	// rowsFooterConfigs stores RowConfig for each footer row
    84  	rowsFooterConfigMap map[int]RowConfig
    85  	// rowsFooterRaw stores the rows that make up the footer
    86  	rowsFooterRaw []Row
    87  	// rowsHeader stores the rows that make up the header (in string form)
    88  	rowsHeader []rowStr
    89  	// rowsHeaderConfigs stores RowConfig for each header row
    90  	rowsHeaderConfigMap map[int]RowConfig
    91  	// rowsHeaderRaw stores the rows that make up the header
    92  	rowsHeaderRaw []Row
    93  	// rowPainter is a custom function that given a Row, returns the colors to
    94  	// use on the entire row
    95  	rowPainter RowPainter
    96  	// rowSeparator is a dummy row that contains the separator columns (dashes
    97  	// that make up the separator between header/body/footer
    98  	rowSeparator rowStr
    99  	// separators is used to keep track of all rowIndices after which a
   100  	// separator has to be rendered
   101  	separators map[int]bool
   102  	// sortBy stores a map of Column
   103  	sortBy []SortBy
   104  	// style contains all the strings used to draw the table, and more
   105  	style *Style
   106  	// suppressEmptyColumns hides columns which have no content on all regular
   107  	// rows
   108  	suppressEmptyColumns bool
   109  	// title contains the text to appear above the table
   110  	title string
   111  }
   112  
   113  // AppendFooter appends the row to the List of footers to render.
   114  //
   115  // Only the first item in the "config" will be tagged against this row.
   116  func (t *Table) AppendFooter(row Row, config ...RowConfig) {
   117  	t.rowsFooterRaw = append(t.rowsFooterRaw, row)
   118  	if len(config) > 0 {
   119  		if t.rowsFooterConfigMap == nil {
   120  			t.rowsFooterConfigMap = make(map[int]RowConfig)
   121  		}
   122  		t.rowsFooterConfigMap[len(t.rowsFooterRaw)-1] = config[0]
   123  	}
   124  }
   125  
   126  // AppendHeader appends the row to the List of headers to render.
   127  //
   128  // Only the first item in the "config" will be tagged against this row.
   129  func (t *Table) AppendHeader(row Row, config ...RowConfig) {
   130  	t.rowsHeaderRaw = append(t.rowsHeaderRaw, row)
   131  	if len(config) > 0 {
   132  		if t.rowsHeaderConfigMap == nil {
   133  			t.rowsHeaderConfigMap = make(map[int]RowConfig)
   134  		}
   135  		t.rowsHeaderConfigMap[len(t.rowsHeaderRaw)-1] = config[0]
   136  	}
   137  }
   138  
   139  // AppendRow appends the row to the List of rows to render.
   140  //
   141  // Only the first item in the "config" will be tagged against this row.
   142  func (t *Table) AppendRow(row Row, config ...RowConfig) {
   143  	t.rowsRaw = append(t.rowsRaw, row)
   144  	if len(config) > 0 {
   145  		if t.rowsConfigMap == nil {
   146  			t.rowsConfigMap = make(map[int]RowConfig)
   147  		}
   148  		t.rowsConfigMap[len(t.rowsRaw)-1] = config[0]
   149  	}
   150  }
   151  
   152  // AppendRows appends the rows to the List of rows to render.
   153  //
   154  // Only the first item in the "config" will be tagged against all the rows.
   155  func (t *Table) AppendRows(rows []Row, config ...RowConfig) {
   156  	for _, row := range rows {
   157  		t.AppendRow(row, config...)
   158  	}
   159  }
   160  
   161  // AppendSeparator helps render a separator row after the current last row. You
   162  // could call this function over and over, but it will be a no-op unless you
   163  // call AppendRow or AppendRows in between. Likewise, if the last thing you
   164  // append is a separator, it will not be rendered in addition to the usual table
   165  // separator.
   166  //
   167  //******************************************************************************
   168  // Please note the following caveats:
   169  // 1. SetPageSize(): this may end up creating consecutive separator rows near
   170  //    the end of a page or at the beginning of a page
   171  // 2. SortBy(): since SortBy could inherently alter the ordering of rows, the
   172  //    separators may not appear after the row it was originally intended to
   173  //    follow
   174  //******************************************************************************
   175  func (t *Table) AppendSeparator() {
   176  	if t.separators == nil {
   177  		t.separators = make(map[int]bool)
   178  	}
   179  	if len(t.rowsRaw) > 0 {
   180  		t.separators[len(t.rowsRaw)-1] = true
   181  	}
   182  }
   183  
   184  // Length returns the number of rows to be rendered.
   185  func (t *Table) Length() int {
   186  	return len(t.rowsRaw)
   187  }
   188  
   189  // ResetFooters resets and clears all the Footer rows appended earlier.
   190  func (t *Table) ResetFooters() {
   191  	t.rowsFooterRaw = nil
   192  }
   193  
   194  // ResetHeaders resets and clears all the Header rows appended earlier.
   195  func (t *Table) ResetHeaders() {
   196  	t.rowsHeaderRaw = nil
   197  }
   198  
   199  // ResetRows resets and clears all the rows appended earlier.
   200  func (t *Table) ResetRows() {
   201  	t.rowsRaw = nil
   202  	t.separators = nil
   203  }
   204  
   205  // SetAllowedRowLength sets the maximum allowed length or a row (or line of
   206  // output) when rendered as a table. Rows that are longer than this limit will
   207  // be "snipped" to the length. Length has to be a positive value to take effect.
   208  func (t *Table) SetAllowedRowLength(length int) {
   209  	t.allowedRowLength = length
   210  }
   211  
   212  // SetAutoIndex adds a generated header with columns such as "A", "B", "C", etc.
   213  // and a leading column with the row number similar to what you'd see on any
   214  // spreadsheet application. NOTE: Appending a Header will void this
   215  // functionality.
   216  func (t *Table) SetAutoIndex(autoIndex bool) {
   217  	t.autoIndex = autoIndex
   218  }
   219  
   220  // SetCaption sets the text to be rendered just below the table. This will not
   221  // show up when the Table is rendered as a CSV.
   222  func (t *Table) SetCaption(format string, a ...interface{}) {
   223  	t.caption = fmt.Sprintf(format, a...)
   224  }
   225  
   226  // SetColumnConfigs sets the configs for each Column.
   227  func (t *Table) SetColumnConfigs(configs []ColumnConfig) {
   228  	t.columnConfigs = configs
   229  }
   230  
   231  // SetHTMLCSSClass sets the the HTML CSS Class to use on the <table> node
   232  // when rendering the Table in HTML format.
   233  //
   234  // Deprecated: in favor of Style().HTML.CSSClass
   235  func (t *Table) SetHTMLCSSClass(cssClass string) {
   236  	t.htmlCSSClass = cssClass
   237  }
   238  
   239  // SetIndexColumn sets the given Column # as the column that has the row
   240  // "Number". Valid values range from 1 to N. Note that this is not 0-indexed.
   241  func (t *Table) SetIndexColumn(colNum int) {
   242  	t.indexColumn = colNum
   243  }
   244  
   245  // SetOutputMirror sets an io.Writer for all the Render functions to "Write" to
   246  // in addition to returning a string.
   247  func (t *Table) SetOutputMirror(mirror io.Writer) {
   248  	t.outputMirror = mirror
   249  }
   250  
   251  // SetPageSize sets the maximum number of lines to render before rendering the
   252  // header rows again. This can be useful when dealing with tables containing a
   253  // long list of rows that can span pages. Please note that the pagination logic
   254  // will not consider Header/Footer lines for paging.
   255  func (t *Table) SetPageSize(numLines int) {
   256  	t.pageSize = numLines
   257  }
   258  
   259  // SetRowPainter sets the RowPainter function which determines the colors to use
   260  // on a row. Before rendering, this function is invoked on all rows and the
   261  // color of each row is determined. This color takes precedence over other ways
   262  // to set color (ColumnConfig.Color*, SetColor*()).
   263  func (t *Table) SetRowPainter(painter RowPainter) {
   264  	t.rowPainter = painter
   265  }
   266  
   267  // SetStyle overrides the DefaultStyle with the provided one.
   268  func (t *Table) SetStyle(style Style) {
   269  	t.style = &style
   270  }
   271  
   272  // SetTitle sets the title text to be rendered above the table.
   273  func (t *Table) SetTitle(format string, a ...interface{}) {
   274  	t.title = fmt.Sprintf(format, a...)
   275  }
   276  
   277  // SortBy sets the rules for sorting the Rows in the order specified. i.e., the
   278  // first SortBy instruction takes precedence over the second and so on. Any
   279  // duplicate instructions on the same column will be discarded while sorting.
   280  func (t *Table) SortBy(sortBy []SortBy) {
   281  	t.sortBy = sortBy
   282  }
   283  
   284  // Style returns the current style.
   285  func (t *Table) Style() *Style {
   286  	if t.style == nil {
   287  		tempStyle := StyleDefault
   288  		t.style = &tempStyle
   289  	}
   290  	return t.style
   291  }
   292  
   293  // SuppressEmptyColumns hides columns when the column is empty in ALL the
   294  // regular rows.
   295  func (t *Table) SuppressEmptyColumns() {
   296  	t.suppressEmptyColumns = true
   297  }
   298  
   299  func (t *Table) analyzeAndStringify(row Row, hint renderHint) rowStr {
   300  	// update t.numColumns if this row is the longest seen till now
   301  	if len(row) > t.numColumns {
   302  		// init the slice for the first time; and pad it the rest of the time
   303  		if t.numColumns == 0 {
   304  			t.columnIsNonNumeric = make([]bool, len(row))
   305  		} else {
   306  			t.columnIsNonNumeric = append(t.columnIsNonNumeric, make([]bool, len(row)-t.numColumns)...)
   307  		}
   308  		// update t.numColumns
   309  		t.numColumns = len(row)
   310  	}
   311  
   312  	// convert each column to string and figure out if it has non-numeric data
   313  	rowOut := make(rowStr, len(row))
   314  	for colIdx, col := range row {
   315  		// if the column is not a number, keep track of it
   316  		if !hint.isHeaderRow && !hint.isFooterRow && !t.columnIsNonNumeric[colIdx] && !isNumber(col) {
   317  			t.columnIsNonNumeric[colIdx] = true
   318  		}
   319  
   320  		rowOut[colIdx] = t.analyzeAndStringifyColumn(colIdx, col, hint)
   321  	}
   322  	return rowOut
   323  }
   324  
   325  func (t *Table) analyzeAndStringifyColumn(colIdx int, col interface{}, hint renderHint) string {
   326  	// convert to a string and store it in the row
   327  	var colStr string
   328  	if transformer := t.getColumnTransformer(colIdx, hint); transformer != nil {
   329  		colStr = transformer(col)
   330  	} else if colStrVal, ok := col.(string); ok {
   331  		colStr = colStrVal
   332  	} else {
   333  		colStr = fmt.Sprint(col)
   334  	}
   335  	if strings.Contains(colStr, "\t") {
   336  		colStr = strings.Replace(colStr, "\t", "    ", -1)
   337  	}
   338  	if strings.Contains(colStr, "\r") {
   339  		colStr = strings.Replace(colStr, "\r", "", -1)
   340  	}
   341  	return fmt.Sprintf("%s%s", t.style.Format.Direction.Modifier(), colStr)
   342  }
   343  
   344  func (t *Table) getAlign(colIdx int, hint renderHint) text.Align {
   345  	align := text.AlignDefault
   346  	if cfg, ok := t.columnConfigMap[colIdx]; ok {
   347  		if hint.isHeaderRow {
   348  			align = cfg.AlignHeader
   349  		} else if hint.isFooterRow {
   350  			align = cfg.AlignFooter
   351  		} else {
   352  			align = cfg.Align
   353  		}
   354  	}
   355  	if align == text.AlignDefault {
   356  		if !t.columnIsNonNumeric[colIdx] {
   357  			align = text.AlignRight
   358  		} else if hint.isAutoIndexRow {
   359  			align = text.AlignCenter
   360  		}
   361  	}
   362  	return align
   363  }
   364  
   365  func (t *Table) getAutoIndexColumnIDs() rowStr {
   366  	row := make(rowStr, t.numColumns)
   367  	for colIdx := range row {
   368  		row[colIdx] = AutoIndexColumnID(colIdx)
   369  	}
   370  	return row
   371  }
   372  
   373  func (t *Table) getBorderColors(hint renderHint) text.Colors {
   374  	if hint.isFooterRow {
   375  		return t.style.Color.Footer
   376  	} else if t.autoIndex {
   377  		return t.style.Color.IndexColumn
   378  	}
   379  	return t.style.Color.Header
   380  }
   381  
   382  func (t *Table) getBorderLeft(hint renderHint) string {
   383  	border := t.style.Box.Left
   384  	if hint.isBorderTop {
   385  		if t.title != "" {
   386  			border = t.style.Box.LeftSeparator
   387  		} else {
   388  			border = t.style.Box.TopLeft
   389  		}
   390  	} else if hint.isBorderBottom {
   391  		border = t.style.Box.BottomLeft
   392  	} else if hint.isSeparatorRow {
   393  		if t.autoIndex && hint.isHeaderOrFooterSeparator() {
   394  			border = t.style.Box.Left
   395  		} else if !t.autoIndex && t.shouldMergeCellsVertically(0, hint) {
   396  			border = t.style.Box.Left
   397  		} else {
   398  			border = t.style.Box.LeftSeparator
   399  		}
   400  	}
   401  	return border
   402  }
   403  
   404  func (t *Table) getBorderRight(hint renderHint) string {
   405  	border := t.style.Box.Right
   406  	if hint.isBorderTop {
   407  		if t.title != "" {
   408  			border = t.style.Box.RightSeparator
   409  		} else {
   410  			border = t.style.Box.TopRight
   411  		}
   412  	} else if hint.isBorderBottom {
   413  		border = t.style.Box.BottomRight
   414  	} else if hint.isSeparatorRow {
   415  		if t.shouldMergeCellsVertically(t.numColumns-1, hint) {
   416  			border = t.style.Box.Right
   417  		} else {
   418  			border = t.style.Box.RightSeparator
   419  		}
   420  	}
   421  	return border
   422  }
   423  
   424  func (t *Table) getColumnColors(colIdx int, hint renderHint) text.Colors {
   425  	if t.rowPainter != nil && hint.isRegularNonSeparatorRow() && !t.isIndexColumn(colIdx, hint) {
   426  		colors := t.rowsColors[hint.rowNumber-1]
   427  		if colors != nil {
   428  			return colors
   429  		}
   430  	}
   431  	if cfg, ok := t.columnConfigMap[colIdx]; ok {
   432  		if hint.isSeparatorRow {
   433  			return nil
   434  		} else if hint.isHeaderRow {
   435  			return cfg.ColorsHeader
   436  		} else if hint.isFooterRow {
   437  			return cfg.ColorsFooter
   438  		}
   439  		return cfg.Colors
   440  	}
   441  	return nil
   442  }
   443  
   444  func (t *Table) getColumnSeparator(row rowStr, colIdx int, hint renderHint) string {
   445  	separator := t.style.Box.MiddleVertical
   446  	if hint.isSeparatorRow {
   447  		if hint.isBorderTop {
   448  			if t.shouldMergeCellsHorizontallyBelow(row, colIdx, hint) {
   449  				separator = t.style.Box.MiddleHorizontal
   450  			} else {
   451  				separator = t.style.Box.TopSeparator
   452  			}
   453  		} else if hint.isBorderBottom {
   454  			if t.shouldMergeCellsHorizontallyAbove(row, colIdx, hint) {
   455  				separator = t.style.Box.MiddleHorizontal
   456  			} else {
   457  				separator = t.style.Box.BottomSeparator
   458  			}
   459  		} else {
   460  			separator = t.getColumnSeparatorNonBorder(
   461  				t.shouldMergeCellsHorizontallyAbove(row, colIdx, hint),
   462  				t.shouldMergeCellsHorizontallyBelow(row, colIdx, hint),
   463  				colIdx,
   464  				hint,
   465  			)
   466  		}
   467  	}
   468  	return separator
   469  }
   470  
   471  func (t *Table) getColumnSeparatorNonBorder(mergeCellsAbove bool, mergeCellsBelow bool, colIdx int, hint renderHint) string {
   472  	mergeNextCol := t.shouldMergeCellsVertically(colIdx, hint)
   473  	if hint.isAutoIndexColumn {
   474  		return t.getColumnSeparatorNonBorderAutoIndex(mergeNextCol, hint)
   475  	}
   476  
   477  	mergeCurrCol := t.shouldMergeCellsVertically(colIdx-1, hint)
   478  	return t.getColumnSeparatorNonBorderNonAutoIndex(mergeCellsAbove, mergeCellsBelow, mergeCurrCol, mergeNextCol)
   479  }
   480  
   481  func (t *Table) getColumnSeparatorNonBorderAutoIndex(mergeNextCol bool, hint renderHint) string {
   482  	if hint.isHeaderOrFooterSeparator() {
   483  		if mergeNextCol {
   484  			return t.style.Box.MiddleVertical
   485  		}
   486  		return t.style.Box.LeftSeparator
   487  	} else if mergeNextCol {
   488  		return t.style.Box.RightSeparator
   489  	}
   490  	return t.style.Box.MiddleSeparator
   491  }
   492  
   493  func (t *Table) getColumnSeparatorNonBorderNonAutoIndex(mergeCellsAbove bool, mergeCellsBelow bool, mergeCurrCol bool, mergeNextCol bool) string {
   494  	if mergeCellsAbove && mergeCellsBelow && mergeCurrCol && mergeNextCol {
   495  		return t.style.Box.EmptySeparator
   496  	} else if mergeCellsAbove && mergeCellsBelow {
   497  		return t.style.Box.MiddleHorizontal
   498  	} else if mergeCellsAbove {
   499  		return t.style.Box.TopSeparator
   500  	} else if mergeCellsBelow {
   501  		return t.style.Box.BottomSeparator
   502  	} else if mergeCurrCol && mergeNextCol {
   503  		return t.style.Box.MiddleVertical
   504  	} else if mergeCurrCol {
   505  		return t.style.Box.LeftSeparator
   506  	} else if mergeNextCol {
   507  		return t.style.Box.RightSeparator
   508  	}
   509  	return t.style.Box.MiddleSeparator
   510  }
   511  
   512  func (t *Table) getColumnTransformer(colIdx int, hint renderHint) text.Transformer {
   513  	var transformer text.Transformer
   514  	if cfg, ok := t.columnConfigMap[colIdx]; ok {
   515  		if hint.isHeaderRow {
   516  			transformer = cfg.TransformerHeader
   517  		} else if hint.isFooterRow {
   518  			transformer = cfg.TransformerFooter
   519  		} else {
   520  			transformer = cfg.Transformer
   521  		}
   522  	}
   523  	return transformer
   524  }
   525  
   526  func (t *Table) getColumnWidthMax(colIdx int) int {
   527  	if cfg, ok := t.columnConfigMap[colIdx]; ok {
   528  		return cfg.WidthMax
   529  	}
   530  	return 0
   531  }
   532  
   533  func (t *Table) getColumnWidthMin(colIdx int) int {
   534  	if cfg, ok := t.columnConfigMap[colIdx]; ok {
   535  		return cfg.WidthMin
   536  	}
   537  	return 0
   538  }
   539  
   540  func (t *Table) getFormat(hint renderHint) text.Format {
   541  	if hint.isSeparatorRow {
   542  		return text.FormatDefault
   543  	} else if hint.isHeaderRow {
   544  		return t.style.Format.Header
   545  	} else if hint.isFooterRow {
   546  		return t.style.Format.Footer
   547  	}
   548  	return t.style.Format.Row
   549  }
   550  
   551  func (t *Table) getMaxColumnLengthForMerging(colIdx int) int {
   552  	maxColumnLength := t.maxColumnLengths[colIdx]
   553  	maxColumnLength += text.RuneWidthWithoutEscSequences(t.style.Box.PaddingRight + t.style.Box.PaddingLeft)
   554  	if t.style.Options.SeparateColumns {
   555  		maxColumnLength += text.RuneWidthWithoutEscSequences(t.style.Box.EmptySeparator)
   556  	}
   557  	return maxColumnLength
   558  }
   559  
   560  func (t *Table) getRow(rowIdx int, hint renderHint) rowStr {
   561  	switch {
   562  	case hint.isHeaderRow:
   563  		if rowIdx >= 0 && rowIdx < len(t.rowsHeader) {
   564  			return t.rowsHeader[rowIdx]
   565  		}
   566  	case hint.isFooterRow:
   567  		if rowIdx >= 0 && rowIdx < len(t.rowsFooter) {
   568  			return t.rowsFooter[rowIdx]
   569  		}
   570  	default:
   571  		if rowIdx >= 0 && rowIdx < len(t.rows) {
   572  			return t.rows[rowIdx]
   573  		}
   574  	}
   575  	return rowStr{}
   576  }
   577  
   578  func (t *Table) getRowConfig(hint renderHint) RowConfig {
   579  	rowIdx := hint.rowNumber - 1
   580  	if rowIdx < 0 {
   581  		rowIdx = 0
   582  	}
   583  
   584  	switch {
   585  	case hint.isHeaderRow:
   586  		return t.rowsHeaderConfigMap[rowIdx]
   587  	case hint.isFooterRow:
   588  		return t.rowsFooterConfigMap[rowIdx]
   589  	default:
   590  		return t.rowsConfigMap[rowIdx]
   591  	}
   592  }
   593  
   594  func (t *Table) getSeparatorColors(hint renderHint) text.Colors {
   595  	if hint.isHeaderRow {
   596  		return t.style.Color.Header
   597  	} else if hint.isFooterRow {
   598  		return t.style.Color.Footer
   599  	} else if hint.isAutoIndexColumn {
   600  		return t.style.Color.IndexColumn
   601  	} else if hint.rowNumber > 0 && hint.rowNumber%2 == 0 {
   602  		return t.style.Color.RowAlternate
   603  	}
   604  	return t.style.Color.Row
   605  }
   606  
   607  func (t *Table) getVAlign(colIdx int, hint renderHint) text.VAlign {
   608  	vAlign := text.VAlignDefault
   609  	if cfg, ok := t.columnConfigMap[colIdx]; ok {
   610  		if hint.isHeaderRow {
   611  			vAlign = cfg.VAlignHeader
   612  		} else if hint.isFooterRow {
   613  			vAlign = cfg.VAlignFooter
   614  		} else {
   615  			vAlign = cfg.VAlign
   616  		}
   617  	}
   618  	return vAlign
   619  }
   620  
   621  func (t *Table) hasHiddenColumns() bool {
   622  	for _, cc := range t.columnConfigMap {
   623  		if cc.Hidden {
   624  			return true
   625  		}
   626  	}
   627  	return false
   628  }
   629  
   630  func (t *Table) initForRender() {
   631  	// pick a default style if none was set until now
   632  	t.Style()
   633  
   634  	// initialize the column configs and normalize them
   635  	t.initForRenderColumnConfigs()
   636  
   637  	// initialize and stringify all the raw rows
   638  	t.initForRenderRows()
   639  
   640  	// find the longest continuous line in each column
   641  	t.initForRenderColumnLengths()
   642  
   643  	// generate a separator row and calculate maximum row length
   644  	t.initForRenderRowSeparator()
   645  
   646  	// reset the counter for the number of lines rendered
   647  	t.numLinesRendered = 0
   648  }
   649  
   650  func (t *Table) initForRenderColumnConfigs() {
   651  	t.columnConfigMap = map[int]ColumnConfig{}
   652  	for _, colCfg := range t.columnConfigs {
   653  		// find the column number if none provided; this logic can work only if
   654  		// a header row is present and has a column with the given name
   655  		if colCfg.Number == 0 {
   656  			for _, row := range t.rowsHeaderRaw {
   657  				colCfg.Number = row.findColumnNumber(colCfg.Name)
   658  				if colCfg.Number > 0 {
   659  					break
   660  				}
   661  			}
   662  		}
   663  		if colCfg.Number > 0 {
   664  			t.columnConfigMap[colCfg.Number-1] = colCfg
   665  		}
   666  	}
   667  }
   668  
   669  func (t *Table) initForRenderColumnLengths() {
   670  	t.maxColumnLengths = make([]int, t.numColumns)
   671  	t.parseRowForMaxColumnLengths(t.rowsHeader)
   672  	t.parseRowForMaxColumnLengths(t.rows)
   673  	t.parseRowForMaxColumnLengths(t.rowsFooter)
   674  
   675  	// restrict the column lengths if any are over or under the limits
   676  	for colIdx := range t.maxColumnLengths {
   677  		maxWidth := t.getColumnWidthMax(colIdx)
   678  		if maxWidth > 0 && t.maxColumnLengths[colIdx] > maxWidth {
   679  			t.maxColumnLengths[colIdx] = maxWidth
   680  		}
   681  		minWidth := t.getColumnWidthMin(colIdx)
   682  		if minWidth > 0 && t.maxColumnLengths[colIdx] < minWidth {
   683  			t.maxColumnLengths[colIdx] = minWidth
   684  		}
   685  	}
   686  }
   687  
   688  func (t *Table) hideColumns() map[int]int {
   689  	colIdxMap := make(map[int]int)
   690  	numColumns := 0
   691  	hideColumnsInRows := func(rows []rowStr) []rowStr {
   692  		var rsp []rowStr
   693  		for _, row := range rows {
   694  			var rowNew rowStr
   695  			for colIdx, col := range row {
   696  				cc := t.columnConfigMap[colIdx]
   697  				if !cc.Hidden {
   698  					rowNew = append(rowNew, col)
   699  					colIdxMap[colIdx] = len(rowNew) - 1
   700  				}
   701  			}
   702  			if len(rowNew) > numColumns {
   703  				numColumns = len(rowNew)
   704  			}
   705  			rsp = append(rsp, rowNew)
   706  		}
   707  		return rsp
   708  	}
   709  
   710  	// hide columns as directed
   711  	t.rows = hideColumnsInRows(t.rows)
   712  	t.rowsFooter = hideColumnsInRows(t.rowsFooter)
   713  	t.rowsHeader = hideColumnsInRows(t.rowsHeader)
   714  
   715  	// reset numColumns to the new number of columns
   716  	t.numColumns = numColumns
   717  
   718  	return colIdxMap
   719  }
   720  
   721  func (t *Table) initForRenderHideColumns() {
   722  	if !t.hasHiddenColumns() {
   723  		return
   724  	}
   725  	colIdxMap := t.hideColumns()
   726  
   727  	// re-create columnIsNonNumeric with new column indices
   728  	columnIsNonNumeric := make([]bool, t.numColumns)
   729  	for oldColIdx, nonNumeric := range t.columnIsNonNumeric {
   730  		if newColIdx, ok := colIdxMap[oldColIdx]; ok {
   731  			columnIsNonNumeric[newColIdx] = nonNumeric
   732  		}
   733  	}
   734  	t.columnIsNonNumeric = columnIsNonNumeric
   735  
   736  	// re-create columnConfigMap with new column indices
   737  	columnConfigMap := make(map[int]ColumnConfig)
   738  	for oldColIdx, cc := range t.columnConfigMap {
   739  		if newColIdx, ok := colIdxMap[oldColIdx]; ok {
   740  			columnConfigMap[newColIdx] = cc
   741  		}
   742  	}
   743  	t.columnConfigMap = columnConfigMap
   744  }
   745  
   746  func (t *Table) initForRenderRows() {
   747  	t.reset()
   748  
   749  	// auto-index: calc the index column's max length
   750  	t.autoIndexVIndexMaxLength = len(fmt.Sprint(len(t.rowsRaw)))
   751  
   752  	// stringify all the rows to make it easy to render
   753  	if t.rowPainter != nil {
   754  		t.rowsColors = make([]text.Colors, len(t.rowsRaw))
   755  	}
   756  	t.rows = t.initForRenderRowsStringify(t.rowsRaw, renderHint{})
   757  	t.rowsFooter = t.initForRenderRowsStringify(t.rowsFooterRaw, renderHint{isFooterRow: true})
   758  	t.rowsHeader = t.initForRenderRowsStringify(t.rowsHeaderRaw, renderHint{isHeaderRow: true})
   759  
   760  	// sort the rows as requested
   761  	t.initForRenderSortRows()
   762  
   763  	// suppress columns without any content
   764  	t.initForRenderSuppressColumns()
   765  
   766  	// strip out hidden columns
   767  	t.initForRenderHideColumns()
   768  }
   769  
   770  func (t *Table) initForRenderRowsStringify(rows []Row, hint renderHint) []rowStr {
   771  	rowsStr := make([]rowStr, len(rows))
   772  	for idx, row := range rows {
   773  		if t.rowPainter != nil && hint.isRegularRow() {
   774  			t.rowsColors[idx] = t.rowPainter(row)
   775  		}
   776  		rowsStr[idx] = t.analyzeAndStringify(row, hint)
   777  	}
   778  	return rowsStr
   779  }
   780  
   781  func (t *Table) initForRenderRowSeparator() {
   782  	t.maxRowLength = 0
   783  	if t.autoIndex {
   784  		t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.PaddingLeft)
   785  		t.maxRowLength += len(fmt.Sprint(len(t.rows)))
   786  		t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.PaddingRight)
   787  		if t.style.Options.SeparateColumns {
   788  			t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.MiddleSeparator)
   789  		}
   790  	}
   791  	if t.style.Options.SeparateColumns {
   792  		t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.MiddleSeparator) * (t.numColumns - 1)
   793  	}
   794  	t.rowSeparator = make(rowStr, t.numColumns)
   795  	for colIdx, maxColumnLength := range t.maxColumnLengths {
   796  		maxColumnLength += text.RuneWidthWithoutEscSequences(t.style.Box.PaddingLeft + t.style.Box.PaddingRight)
   797  		t.maxRowLength += maxColumnLength
   798  		t.rowSeparator[colIdx] = text.RepeatAndTrim(t.style.Box.MiddleHorizontal, maxColumnLength)
   799  	}
   800  	if t.style.Options.DrawBorder {
   801  		t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.Left + t.style.Box.Right)
   802  	}
   803  }
   804  
   805  func (t *Table) initForRenderSortRows() {
   806  	if len(t.sortBy) == 0 {
   807  		return
   808  	}
   809  
   810  	// sort the rows
   811  	sortedRowIndices := t.getSortedRowIndices()
   812  	sortedRows := make([]rowStr, len(t.rows))
   813  	for idx := range t.rows {
   814  		sortedRows[idx] = t.rows[sortedRowIndices[idx]]
   815  	}
   816  	t.rows = sortedRows
   817  
   818  	// sort the rowsColors
   819  	if len(t.rowsColors) > 0 {
   820  		sortedRowsColors := make([]text.Colors, len(t.rows))
   821  		for idx := range t.rows {
   822  			sortedRowsColors[idx] = t.rowsColors[sortedRowIndices[idx]]
   823  		}
   824  		t.rowsColors = sortedRowsColors
   825  	}
   826  }
   827  
   828  func (t *Table) initForRenderSuppressColumns() {
   829  	shouldSuppressColumn := func(colIdx int) bool {
   830  		for _, row := range t.rows {
   831  			if colIdx < len(row) && row[colIdx] != "" {
   832  				return false
   833  			}
   834  		}
   835  		return true
   836  	}
   837  
   838  	if t.suppressEmptyColumns {
   839  		for colIdx := 0; colIdx < t.numColumns; colIdx++ {
   840  			if shouldSuppressColumn(colIdx) {
   841  				cc := t.columnConfigMap[colIdx]
   842  				cc.Hidden = true
   843  				t.columnConfigMap[colIdx] = cc
   844  			}
   845  		}
   846  	}
   847  }
   848  
   849  func (t *Table) isIndexColumn(colIdx int, hint renderHint) bool {
   850  	return t.indexColumn == colIdx+1 || hint.isAutoIndexColumn
   851  }
   852  
   853  func (t *Table) parseRowForMaxColumnLengths(rows []rowStr) {
   854  	for _, row := range rows {
   855  		for colIdx, colStr := range row {
   856  			longestLineLen := text.LongestLineLen(colStr)
   857  			if longestLineLen > t.maxColumnLengths[colIdx] {
   858  				t.maxColumnLengths[colIdx] = longestLineLen
   859  			}
   860  		}
   861  	}
   862  }
   863  
   864  func (t *Table) render(out *strings.Builder) string {
   865  	outStr := out.String()
   866  	if t.outputMirror != nil && len(outStr) > 0 {
   867  		_, _ = t.outputMirror.Write([]byte(outStr))
   868  		_, _ = t.outputMirror.Write([]byte("\n"))
   869  	}
   870  	return outStr
   871  }
   872  
   873  func (t *Table) reset() {
   874  	t.autoIndexVIndexMaxLength = 0
   875  	t.columnIsNonNumeric = nil
   876  	t.maxColumnLengths = nil
   877  	t.maxRowLength = 0
   878  	t.numColumns = 0
   879  	t.rowsColors = nil
   880  	t.rowSeparator = nil
   881  	t.rows = nil
   882  	t.rowsFooter = nil
   883  	t.rowsHeader = nil
   884  }
   885  
   886  func (t *Table) shouldMergeCellsHorizontallyAbove(row rowStr, colIdx int, hint renderHint) bool {
   887  	if hint.isAutoIndexColumn || hint.isAutoIndexRow {
   888  		return false
   889  	}
   890  
   891  	rowConfig := t.getRowConfig(hint)
   892  	if hint.isSeparatorRow {
   893  		if hint.isHeaderRow && hint.rowNumber == 1 {
   894  			rowConfig = t.getRowConfig(hint)
   895  			row = t.getRow(hint.rowNumber-1, hint)
   896  		} else if hint.isFooterRow && hint.isFirstRow {
   897  			rowConfig = t.getRowConfig(renderHint{isLastRow: true, rowNumber: len(t.rows)})
   898  			row = t.getRow(len(t.rows)-1, renderHint{})
   899  		} else if hint.isFooterRow && hint.isBorderBottom {
   900  			row = t.getRow(len(t.rowsFooter)-1, renderHint{isFooterRow: true})
   901  		} else {
   902  			row = t.getRow(hint.rowNumber-1, hint)
   903  		}
   904  	}
   905  
   906  	if rowConfig.AutoMerge {
   907  		return row.areEqual(colIdx-1, colIdx)
   908  	}
   909  	return false
   910  }
   911  
   912  func (t *Table) shouldMergeCellsHorizontallyBelow(row rowStr, colIdx int, hint renderHint) bool {
   913  	if hint.isAutoIndexColumn || hint.isAutoIndexRow {
   914  		return false
   915  	}
   916  
   917  	var rowConfig RowConfig
   918  	if hint.isSeparatorRow {
   919  		if hint.isHeaderRow && hint.rowNumber == 0 {
   920  			rowConfig = t.getRowConfig(renderHint{isHeaderRow: true, rowNumber: 1})
   921  			row = t.getRow(0, hint)
   922  		} else if hint.isHeaderRow && hint.isLastRow {
   923  			rowConfig = t.getRowConfig(renderHint{rowNumber: 1})
   924  			row = t.getRow(0, renderHint{})
   925  		} else if hint.isHeaderRow {
   926  			rowConfig = t.getRowConfig(renderHint{isHeaderRow: true, rowNumber: hint.rowNumber + 1})
   927  			row = t.getRow(hint.rowNumber, hint)
   928  		} else if hint.isFooterRow && hint.rowNumber >= 0 {
   929  			rowConfig = t.getRowConfig(renderHint{isFooterRow: true, rowNumber: 1})
   930  			row = t.getRow(hint.rowNumber, renderHint{isFooterRow: true})
   931  		} else if hint.isRegularRow() {
   932  			rowConfig = t.getRowConfig(renderHint{rowNumber: hint.rowNumber + 1})
   933  			row = t.getRow(hint.rowNumber, renderHint{})
   934  		}
   935  	}
   936  
   937  	if rowConfig.AutoMerge {
   938  		return row.areEqual(colIdx-1, colIdx)
   939  	}
   940  	return false
   941  }
   942  
   943  func (t *Table) shouldMergeCellsVertically(colIdx int, hint renderHint) bool {
   944  	if t.columnConfigMap[colIdx].AutoMerge && colIdx < t.numColumns {
   945  		if hint.isSeparatorRow {
   946  			rowPrev := t.getRow(hint.rowNumber-1, hint)
   947  			rowNext := t.getRow(hint.rowNumber, hint)
   948  			if colIdx < len(rowPrev) && colIdx < len(rowNext) {
   949  				return rowPrev[colIdx] == rowNext[colIdx] || "" == rowNext[colIdx]
   950  			}
   951  		} else {
   952  			rowPrev := t.getRow(hint.rowNumber-2, hint)
   953  			rowCurr := t.getRow(hint.rowNumber-1, hint)
   954  			if colIdx < len(rowPrev) && colIdx < len(rowCurr) {
   955  				return rowPrev[colIdx] == rowCurr[colIdx] || "" == rowCurr[colIdx]
   956  			}
   957  		}
   958  	}
   959  	return false
   960  }
   961  
   962  func (t *Table) wrapRow(row rowStr) (int, rowStr) {
   963  	colMaxLines := 0
   964  	rowWrapped := make(rowStr, len(row))
   965  	for colIdx, colStr := range row {
   966  		widthEnforcer := t.columnConfigMap[colIdx].getWidthMaxEnforcer()
   967  		rowWrapped[colIdx] = widthEnforcer(colStr, t.maxColumnLengths[colIdx])
   968  		colNumLines := strings.Count(rowWrapped[colIdx], "\n") + 1
   969  		if colNumLines > colMaxLines {
   970  			colMaxLines = colNumLines
   971  		}
   972  	}
   973  	return colMaxLines, rowWrapped
   974  }
   975  
   976  // renderHint has hints for the Render*() logic
   977  type renderHint struct {
   978  	isAutoIndexColumn bool // auto-index column?
   979  	isAutoIndexRow    bool // auto-index row?
   980  	isBorderBottom    bool // bottom-border?
   981  	isBorderTop       bool // top-border?
   982  	isFirstRow        bool // first-row of header/footer/regular-rows?
   983  	isFooterRow       bool // footer row?
   984  	isHeaderRow       bool // header row?
   985  	isLastLineOfRow   bool // last-line of the current row?
   986  	isLastRow         bool // last-row of header/footer/regular-rows?
   987  	isSeparatorRow    bool // separator row?
   988  	rowLineNumber     int  // the line number for a multi-line row
   989  	rowNumber         int  // the row number/index
   990  }
   991  
   992  func (h *renderHint) isRegularRow() bool {
   993  	return !h.isHeaderRow && !h.isFooterRow
   994  }
   995  
   996  func (h *renderHint) isRegularNonSeparatorRow() bool {
   997  	return !h.isHeaderRow && !h.isFooterRow && !h.isSeparatorRow
   998  }
   999  
  1000  func (h *renderHint) isHeaderOrFooterSeparator() bool {
  1001  	return h.isSeparatorRow && !h.isBorderBottom && !h.isBorderTop &&
  1002  		((h.isHeaderRow && !h.isLastRow) || (h.isFooterRow && (!h.isFirstRow || h.rowNumber > 0)))
  1003  }
  1004  
  1005  func (h *renderHint) isLastLineOfLastRow() bool {
  1006  	return h.isLastLineOfRow && h.isLastRow
  1007  }
  1008  

View as plain text