Files
renovate-sh-app[bot]GitHubrenovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com>
e1a79277fa fix(deps): update module github.com/jedib0t/go-pretty/v6 to v6.7.8 (#6152)
| datasource | package                         | from   | to     |
| ---------- | ------------------------------- | ------ | ------ |
| go         | github.com/jedib0t/go-pretty/v6 | v6.7.5 | v6.7.8 |

Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com>
Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com>
2026-01-07 23:39:01 +00:00

535 lines
16 KiB
Go

package table
import (
"fmt"
"sort"
"strings"
"unicode"
"github.com/jedib0t/go-pretty/v6/text"
)
func (t *Table) analyzeAndStringify(row Row, hint renderHint) rowStr {
// update t.numColumns if this row is the longest seen till now
if len(row) > t.numColumns {
// init the slice for the first time; and pad it the rest of the time
if t.numColumns == 0 {
t.columnIsNonNumeric = make([]bool, len(row))
} else {
t.columnIsNonNumeric = append(t.columnIsNonNumeric, make([]bool, len(row)-t.numColumns)...)
}
// update t.numColumns
t.numColumns = len(row)
}
// convert each column to string and figure out if it has non-numeric data
rowOut := make(rowStr, len(row))
for colIdx, col := range row {
// if the column is not a number, keep track of it
if !hint.isHeaderRow && !hint.isFooterRow && !t.columnIsNonNumeric[colIdx] && !isNumber(col) {
t.columnIsNonNumeric[colIdx] = true
}
rowOut[colIdx] = t.analyzeAndStringifyColumn(colIdx, col, hint)
}
return rowOut
}
func (t *Table) analyzeAndStringifyColumn(colIdx int, col interface{}, hint renderHint) string {
// convert to a string and store it in the row
var colStr string
if transformer := t.getColumnTransformer(colIdx, hint); transformer != nil {
colStr = transformer(col)
} else if colStrVal, ok := col.(string); ok {
colStr = colStrVal
} else {
colStr = convertValueToString(col)
}
colStr = strings.ReplaceAll(colStr, "\t", " ")
colStr = text.ProcessCRLF(colStr)
// Avoid fmt.Sprintf when direction modifier is empty (most common case)
if t.directionModifier == "" {
return colStr
}
return t.directionModifier + colStr
}
func (t *Table) extractMaxColumnLengths(rows []rowStr, hint renderHint) {
for rowIdx, row := range rows {
hint.rowNumber = rowIdx + 1
t.extractMaxColumnLengthsFromRow(row, t.getMergedColumnIndices(row, hint))
}
}
func (t *Table) extractMaxColumnLengthsFromRow(row rowStr, mci mergedColumnIndices) {
for colIdx := 0; colIdx < len(row); colIdx++ {
colStr := row[colIdx]
longestLineLen := text.LongestLineLen(colStr)
maxColWidth := t.getColumnWidthMax(colIdx)
if maxColWidth > 0 && maxColWidth < longestLineLen {
longestLineLen = maxColWidth
}
if mergeEndIndex, ok := mci[colIdx]; ok {
startIndexMap := t.maxMergedColumnLengths[mergeEndIndex]
if startIndexMap == nil {
startIndexMap = make(map[int]int)
t.maxMergedColumnLengths[mergeEndIndex] = startIndexMap
}
if longestLineLen > startIndexMap[colIdx] {
startIndexMap[colIdx] = longestLineLen
}
colIdx = mergeEndIndex
} else if longestLineLen > t.maxColumnLengths[colIdx] {
t.maxColumnLengths[colIdx] = longestLineLen
}
}
}
// reBalanceMaxMergedColumnLengths tries to re-balance the merged column lengths
// across all columns. It does this from the lowest end index to the highest,
// and within that set from the highest start index to the lowest. It
// distributes the length across the columns not already exceeding the average.
func (t *Table) reBalanceMaxMergedColumnLengths() {
endIndexKeys, startIndexKeysMap := getSortedKeys(t.maxMergedColumnLengths)
middleSepLen := text.StringWidthWithoutEscSequences(t.style.Box.MiddleSeparator)
for _, endIndexKey := range endIndexKeys {
startIndexKeys := startIndexKeysMap[endIndexKey]
for idx := len(startIndexKeys) - 1; idx >= 0; idx-- {
startIndexKey := startIndexKeys[idx]
columnBalanceMap := map[int]struct{}{}
for index := startIndexKey; index <= endIndexKey; index++ {
columnBalanceMap[index] = struct{}{}
}
mergedColumnLength := t.maxMergedColumnLengths[endIndexKey][startIndexKey] -
((len(columnBalanceMap) - 1) * middleSepLen)
// keep reducing the set of columns until the remainder are the ones less than
// the average of the remaining length (total merged length - all lengths > average)
for {
if mergedColumnLength <= 0 { // already exceeded the merged length
columnBalanceMap = map[int]struct{}{}
break
}
numMergedColumns := len(columnBalanceMap)
maxLengthSplitAcrossColumns := mergedColumnLength / numMergedColumns
mapReduced := false
for mergedColumn := range columnBalanceMap {
maxColumnLength := t.maxColumnLengths[mergedColumn]
if maxColumnLength >= maxLengthSplitAcrossColumns {
mapReduced = true
mergedColumnLength -= maxColumnLength
delete(columnBalanceMap, mergedColumn)
}
}
if !mapReduced {
break
}
}
// act on any remaining columns that need balancing
if len(columnBalanceMap) > 0 {
// remove the max column sizes from the remaining amount to balance, then
// share out the remainder amongst the columns.
numRebalancedColumns := len(columnBalanceMap)
balanceColumns := make([]int, 0, numRebalancedColumns)
for balanceColumn := range columnBalanceMap {
mergedColumnLength -= t.maxColumnLengths[balanceColumn]
balanceColumns = append(balanceColumns, balanceColumn)
}
// pad out the columns one by one
sort.Ints(balanceColumns)
columnLengthRemaining := mergedColumnLength
columnsRemaining := numRebalancedColumns
for index := 0; index < numRebalancedColumns; index++ {
balancedSpace := columnLengthRemaining / columnsRemaining
balanceColumn := balanceColumns[index]
t.maxColumnLengths[balanceColumn] += balancedSpace
columnLengthRemaining -= balancedSpace
columnsRemaining--
}
}
}
}
}
func (t *Table) initForRender(mode renderMode) {
t.renderMode = mode
// pick a default style if none was set until now
t.Style()
// reset rendering state
t.reset()
// cache the direction modifier to avoid repeated calls
t.directionModifier = t.style.Format.Direction.Modifier()
// initialize the column configs and normalize them
t.initForRenderColumnConfigs()
// initialize and stringify all the raw rows
t.initForRenderRows()
// find the longest continuous line in each column
t.initForRenderColumnLengths()
t.initForRenderMaxRowLength()
t.initForRenderPaddedColumns()
// generate a separator row and calculate maximum row length
t.initForRenderRowSeparator()
// reset the counter for the number of lines rendered
t.numLinesRendered = 0
}
func (t *Table) initForRenderColumnConfigs() {
t.columnConfigMap = map[int]ColumnConfig{}
for _, colCfg := range t.columnConfigs {
// find the column number if none provided; this logic can work only if
// a header row is present and has a column with the given name
if colCfg.Number == 0 {
for _, row := range t.rowsHeaderRaw {
colCfg.Number = row.findColumnNumber(colCfg.Name)
if colCfg.Number > 0 {
break
}
}
}
if colCfg.Number > 0 {
t.columnConfigMap[colCfg.Number-1] = colCfg
}
}
}
func (t *Table) initForRenderColumnLengths() {
t.maxColumnLengths = make([]int, t.numColumns)
t.maxMergedColumnLengths = make(map[int]map[int]int)
t.extractMaxColumnLengths(t.rowsHeader, renderHint{isHeaderRow: true})
t.extractMaxColumnLengths(t.rows, renderHint{})
t.extractMaxColumnLengths(t.rowsFooter, renderHint{isFooterRow: true})
// increase the column lengths if any are under the limits
for colIdx := range t.maxColumnLengths {
minWidth := t.getColumnWidthMin(colIdx)
if minWidth > 0 && t.maxColumnLengths[colIdx] < minWidth {
t.maxColumnLengths[colIdx] = minWidth
}
}
t.reBalanceMaxMergedColumnLengths()
}
func (t *Table) initForRenderHideColumns() {
if !t.hasHiddenColumns() {
return
}
colIdxMap := t.hideColumns()
// re-create columnIsNonNumeric with new column indices
columnIsNonNumeric := make([]bool, t.numColumns)
for oldColIdx, nonNumeric := range t.columnIsNonNumeric {
if newColIdx, ok := colIdxMap[oldColIdx]; ok {
columnIsNonNumeric[newColIdx] = nonNumeric
}
}
t.columnIsNonNumeric = columnIsNonNumeric
// re-create columnConfigMap with new column indices
columnConfigMap := make(map[int]ColumnConfig)
for oldColIdx, cc := range t.columnConfigMap {
if newColIdx, ok := colIdxMap[oldColIdx]; ok {
columnConfigMap[newColIdx] = cc
}
}
t.columnConfigMap = columnConfigMap
}
func (t *Table) initForRenderMaxRowLength() {
t.maxRowLength = 0
if t.autoIndex {
t.maxRowLength += text.StringWidthWithoutEscSequences(t.style.Box.PaddingLeft)
t.maxRowLength += len(fmt.Sprint(len(t.rows)))
t.maxRowLength += text.StringWidthWithoutEscSequences(t.style.Box.PaddingRight)
if t.style.Options.SeparateColumns {
t.maxRowLength += text.StringWidthWithoutEscSequences(t.style.Box.MiddleSeparator)
}
}
if t.style.Options.SeparateColumns {
t.maxRowLength += text.StringWidthWithoutEscSequences(t.style.Box.MiddleSeparator) * (t.numColumns - 1)
}
for _, maxColumnLength := range t.maxColumnLengths {
maxColumnLength += text.StringWidthWithoutEscSequences(t.style.Box.PaddingLeft + t.style.Box.PaddingRight)
t.maxRowLength += maxColumnLength
}
if t.style.Options.DrawBorder {
t.maxRowLength += text.StringWidthWithoutEscSequences(t.style.Box.Left + t.style.Box.Right)
}
}
func (t *Table) initForRenderPaddedColumns() {
paddingSize := t.style.Size.WidthMin - t.maxRowLength
for paddingSize > 0 {
// distribute padding equally among all columns
numColumnsPadded := 0
for colIdx := 0; paddingSize > 0 && colIdx < t.numColumns; colIdx++ {
colWidthMax := t.getColumnWidthMax(colIdx)
if colWidthMax == 0 || t.maxColumnLengths[colIdx] < colWidthMax {
t.maxColumnLengths[colIdx]++
numColumnsPadded++
paddingSize--
}
}
// avoid endless looping because all columns are at max size and cannot
// be expanded any further
if numColumnsPadded == 0 {
break
}
}
}
func (t *Table) initForRenderRows() {
// filter the rows as requested (before stringification and sorting)
t.initForRenderFilterRows()
// auto-index: calc the index column's max length
t.autoIndexVIndexMaxLength = len(fmt.Sprint(len(t.rowsRawFiltered)))
// stringify the filtered rows
t.numColumns = 0
t.rows = t.initForRenderRowsStringify(t.rowsRawFiltered, renderHint{})
t.rowsFooter = t.initForRenderRowsStringify(t.rowsFooterRaw, renderHint{isFooterRow: true})
t.rowsHeader = t.initForRenderRowsStringify(t.rowsHeaderRaw, renderHint{isHeaderRow: true})
// sort the rows as requested
t.initForRenderSortRows()
// find the row colors (if any)
t.initForRenderRowPainterColors()
// suppress columns without any content
t.initForRenderSuppressColumns()
// strip out hidden columns
t.initForRenderHideColumns()
}
// initForRenderFilterRows filters the raw rows by removing non-matching rows from t.rowsRawFiltered.
func (t *Table) initForRenderFilterRows() {
// Restore original rows before filtering (in case of multiple renders with different filters)
if len(t.rowsRaw) > 0 {
t.rowsRawFiltered = make([]Row, len(t.rowsRaw))
for i, row := range t.rowsRaw {
rowCopy := make(Row, len(row))
copy(rowCopy, row)
t.rowsRawFiltered[i] = rowCopy
}
}
if len(t.filterBy) == 0 {
// No filters, nothing to do
return
}
// Store original separators and track which rows are kept
originalSeparators := make(map[int]bool)
for k, v := range t.separators {
originalSeparators[k] = v
}
// Calculate numColumns from raw rows/headers for filter parsing
t.calculateNumColumnsFromRaw()
parsedFilterBy := t.parseFilterBy(t.filterBy)
if len(parsedFilterBy) == 0 {
// No valid filters, nothing to do
return
}
// Filter rows in place and track which original rows were kept
filteredRows := t.rowsRawFiltered[:0]
keptIndices := make([]int, 0, len(t.rowsRawFiltered))
for origIdx, row := range t.rowsRawFiltered {
if t.matchesFiltersRaw(row, parsedFilterBy) {
filteredRows = append(filteredRows, row)
keptIndices = append(keptIndices, origIdx)
}
}
t.rowsRawFiltered = filteredRows
// Update separators map to reflect filtered rows
if len(originalSeparators) > 0 {
newSeparators := make(map[int]bool)
for newIdx, origIdx := range keptIndices {
if originalSeparators[origIdx] {
newSeparators[newIdx] = true
}
}
t.separators = newSeparators
}
}
func (t *Table) initForRenderRowsStringify(rows []Row, hint renderHint) []rowStr {
rowsStr := make([]rowStr, len(rows))
for idx, row := range rows {
hint.rowNumber = idx + 1
rowsStr[idx] = t.analyzeAndStringify(row, hint)
}
return rowsStr
}
func (t *Table) initForRenderRowPainterColors() {
if !t.hasRowPainter() {
return
}
// generate the colors for the final rows (after filtering and sorting)
// rowsColors will be indexed by the final position in t.rows
t.rowsColors = make([]text.Colors, len(t.rows))
// For each final position, find the row index in t.rowsRawFiltered (which is already filtered)
for finalPos := range t.rows {
var rowIdx int
if len(t.sortedRowIndices) > 0 {
// Rows were sorted: finalPos -> sortedRowIndices[finalPos] -> rowIdx in t.rowsRawFiltered
rowIdx = t.sortedRowIndices[finalPos]
} else {
// No sorting: finalPos -> rowIdx in t.rowsRawFiltered
rowIdx = finalPos
}
if rowIdx >= 0 && rowIdx < len(t.rowsRawFiltered) {
row := t.rowsRawFiltered[rowIdx]
if t.rowPainter != nil {
t.rowsColors[finalPos] = t.rowPainter(row)
} else if t.rowPainterWithAttributes != nil {
t.rowsColors[finalPos] = t.rowPainterWithAttributes(row, RowAttributes{
Number: rowIdx + 1,
NumberSorted: finalPos + 1,
})
}
}
}
}
func (t *Table) initForRenderRowSeparator() {
// this is needed only for default render mode
if t.renderMode != renderModeDefault {
return
}
// init the separatorType -> separator-string map
t.initForRenderRowSeparatorStrings()
// init the separator-string -> separator-row map
t.rowSeparators = make(map[string]rowStr, len(t.rowSeparatorStrings))
paddingLength := text.StringWidthWithoutEscSequences(t.style.Box.PaddingLeft + t.style.Box.PaddingRight)
for _, separator := range t.rowSeparatorStrings {
t.rowSeparators[separator] = make(rowStr, t.numColumns)
for colIdx, maxColumnLength := range t.maxColumnLengths {
t.rowSeparators[separator][colIdx] = text.RepeatAndTrim(separator, maxColumnLength+paddingLength)
}
}
}
func (t *Table) initForRenderRowSeparatorStrings() {
// allocate and init only the separators that are needed
t.rowSeparatorStrings = make(map[separatorType]string)
addSeparatorType := func(st separatorType) {
t.rowSeparatorStrings[st] = t.style.Box.middleHorizontal(st)
}
// for other render modes, we need all the separators
if t.title != "" {
addSeparatorType(separatorTypeTitleTop)
addSeparatorType(separatorTypeTitleBottom)
}
if len(t.rowsHeader) > 0 || t.autoIndex {
addSeparatorType(separatorTypeHeaderTop)
addSeparatorType(separatorTypeHeaderBottom)
if len(t.rowsHeader) > 1 {
addSeparatorType(separatorTypeHeaderMiddle)
}
}
if len(t.rows) > 0 {
addSeparatorType(separatorTypeRowTop)
addSeparatorType(separatorTypeRowBottom)
if len(t.rows) > 1 {
addSeparatorType(separatorTypeRowMiddle)
}
} else if len(t.rowsHeader) > 0 || t.autoIndex {
// When there are headers but no data rows, we still need separatorTypeRowBottom
// for the bottom border.
addSeparatorType(separatorTypeRowBottom)
}
if len(t.rowsFooter) > 0 || t.autoIndex {
addSeparatorType(separatorTypeFooterTop)
addSeparatorType(separatorTypeFooterBottom)
if len(t.rowsFooter) > 1 {
addSeparatorType(separatorTypeFooterMiddle)
}
}
}
func (t *Table) initForRenderSortRows() {
if len(t.sortBy) == 0 {
return
}
// sort the rows
t.sortedRowIndices = t.getSortedRowIndices()
sortedRows := make([]rowStr, len(t.rows))
for idx := range t.rows {
sortedRows[idx] = t.rows[t.sortedRowIndices[idx]]
}
t.rows = sortedRows
}
func (t *Table) initForRenderSuppressColumns() {
shouldSuppressColumn := func(colIdx int) bool {
for _, row := range t.rows {
if colIdx < len(row) && row[colIdx] != "" {
// Columns may contain non-printable characters. For example
// the text.Direction modifiers. These should not be considered
// when deciding to suppress a column.
for _, r := range row[colIdx] {
if unicode.IsPrint(r) {
return false
}
}
return true
}
}
return true
}
if t.suppressEmptyColumns {
for colIdx := 0; colIdx < t.numColumns; colIdx++ {
if shouldSuppressColumn(colIdx) {
cc := t.columnConfigMap[colIdx]
cc.Hidden = true
t.columnConfigMap[colIdx] = cc
}
}
}
}
// reset initializes all the variables used to maintain rendering information
// that are written to in this file
func (t *Table) reset() {
t.autoIndexVIndexMaxLength = 0
t.columnConfigMap = nil
t.columnIsNonNumeric = nil
t.firstRowOfPage = true
t.maxColumnLengths = nil
t.maxRowLength = 0
t.numColumns = 0
t.numLinesRendered = 0
t.rowSeparators = nil
t.rows = nil
t.rowsColors = nil
t.rowsFooter = nil
t.rowsHeader = nil
t.sortedRowIndices = nil
}