mirror of
https://github.com/owncloud/ocis
synced 2026-04-25 17:25:21 +02:00
* chore(deps): bump github.com/olekukonko/tablewriter from 0.0.5 to 1.0.9 Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 0.0.5 to 1.0.9. - [Commits](https://github.com/olekukonko/tablewriter/compare/v0.0.5...v1.0.9) --- updated-dependencies: - dependency-name: github.com/olekukonko/tablewriter dependency-version: 1.0.9 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> * fix: adjust to tablewriter 1.0.9 (AI-powered) Signed-off-by: Julian Koberg <julian.koberg@kiteworks.com> * update to NewTable * bring back a Footer * fix the tests --------- Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: Julian Koberg <julian.koberg@kiteworks.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Julian Koberg <julian.koberg@kiteworks.com> Co-authored-by: Roman Perekhod <rperekhod@owncloud.com>
1731 lines
61 KiB
Go
1731 lines
61 KiB
Go
package tablewriter
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"github.com/olekukonko/errors"
|
|
"github.com/olekukonko/tablewriter/pkg/twwidth"
|
|
"github.com/olekukonko/tablewriter/tw"
|
|
"io"
|
|
"math"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// applyHierarchicalMerges applies hierarchical merges to row content.
|
|
// Parameters ctx and mctx hold rendering and merge state.
|
|
// No return value.
|
|
func (t *Table) applyHierarchicalMerges(ctx *renderContext, mctx *mergeContext) {
|
|
ctx.logger.Debug("Applying hierarchical merges (left-to-right vertical flow - snapshot comparison)")
|
|
if len(ctx.rowLines) <= 1 {
|
|
ctx.logger.Debug("Skipping hierarchical merges - less than 2 rows")
|
|
return
|
|
}
|
|
numCols := ctx.numCols
|
|
|
|
originalRowLines := make([][][]string, len(ctx.rowLines))
|
|
for i, row := range ctx.rowLines {
|
|
originalRowLines[i] = make([][]string, len(row))
|
|
for j, line := range row {
|
|
originalRowLines[i][j] = make([]string, len(line))
|
|
copy(originalRowLines[i][j], line)
|
|
}
|
|
}
|
|
ctx.logger.Debug("Created snapshot of original row data for hierarchical merge comparison.")
|
|
|
|
hMergeStartRow := make(map[int]int)
|
|
|
|
for r := 1; r < len(ctx.rowLines); r++ {
|
|
leftCellContinuedHierarchical := false
|
|
|
|
for c := 0; c < numCols; c++ {
|
|
if mctx.rowMerges[r] == nil {
|
|
mctx.rowMerges[r] = make(map[int]tw.MergeState)
|
|
}
|
|
if mctx.rowMerges[r-1] == nil {
|
|
mctx.rowMerges[r-1] = make(map[int]tw.MergeState)
|
|
}
|
|
|
|
canCompare := r > 0 &&
|
|
len(originalRowLines[r]) > 0 &&
|
|
len(originalRowLines[r-1]) > 0
|
|
|
|
if !canCompare {
|
|
currentState := mctx.rowMerges[r][c]
|
|
currentState.Hierarchical = tw.MergeStateOption{}
|
|
mctx.rowMerges[r][c] = currentState
|
|
ctx.logger.Debugf("HCompare Skipped: r=%d, c=%d - Insufficient data in snapshot", r, c)
|
|
leftCellContinuedHierarchical = false
|
|
continue
|
|
}
|
|
|
|
// Join all lines of the cell for comparison
|
|
var currentVal, aboveVal string
|
|
for _, line := range originalRowLines[r] {
|
|
if c < len(line) {
|
|
currentVal += line[c]
|
|
}
|
|
}
|
|
for _, line := range originalRowLines[r-1] {
|
|
if c < len(line) {
|
|
aboveVal += line[c]
|
|
}
|
|
}
|
|
|
|
currentVal = t.Trimmer(currentVal)
|
|
aboveVal = t.Trimmer(aboveVal)
|
|
|
|
currentState := mctx.rowMerges[r][c]
|
|
prevStateAbove := mctx.rowMerges[r-1][c]
|
|
|
|
valuesMatch := currentVal == aboveVal && currentVal != "" && currentVal != "-"
|
|
hierarchyAllowed := c == 0 || leftCellContinuedHierarchical
|
|
shouldContinue := valuesMatch && hierarchyAllowed
|
|
|
|
ctx.logger.Debugf("HCompare: r=%d, c=%d; current='%s', above='%s'; match=%v; leftCont=%v; shouldCont=%v",
|
|
r, c, currentVal, aboveVal, valuesMatch, leftCellContinuedHierarchical, shouldContinue)
|
|
|
|
if shouldContinue {
|
|
currentState.Hierarchical.Present = true
|
|
currentState.Hierarchical.Start = false
|
|
|
|
if prevStateAbove.Hierarchical.Present && !prevStateAbove.Hierarchical.End {
|
|
startRow, ok := hMergeStartRow[c]
|
|
if !ok {
|
|
ctx.logger.Debugf("Hierarchical merge WARNING: Recovering lost start row at r=%d, c=%d. Assuming r-1 was start.", r, c)
|
|
startRow = r - 1
|
|
hMergeStartRow[c] = startRow
|
|
startState := mctx.rowMerges[startRow][c]
|
|
startState.Hierarchical.Present = true
|
|
startState.Hierarchical.Start = true
|
|
startState.Hierarchical.End = false
|
|
mctx.rowMerges[startRow][c] = startState
|
|
}
|
|
ctx.logger.Debugf("Hierarchical merge CONTINUED row %d, col %d. Block previously started row %d", r, c, startRow)
|
|
} else {
|
|
startRow := r - 1
|
|
hMergeStartRow[c] = startRow
|
|
startState := mctx.rowMerges[startRow][c]
|
|
startState.Hierarchical.Present = true
|
|
startState.Hierarchical.Start = true
|
|
startState.Hierarchical.End = false
|
|
mctx.rowMerges[startRow][c] = startState
|
|
ctx.logger.Debugf("Hierarchical merge START detected for block ending at or after row %d, col %d (started at row %d)", r, c, startRow)
|
|
}
|
|
|
|
for lineIdx := range ctx.rowLines[r] {
|
|
if c < len(ctx.rowLines[r][lineIdx]) {
|
|
ctx.rowLines[r][lineIdx][c] = tw.Empty
|
|
}
|
|
}
|
|
|
|
leftCellContinuedHierarchical = true
|
|
} else {
|
|
currentState.Hierarchical = tw.MergeStateOption{}
|
|
|
|
if startRow, ok := hMergeStartRow[c]; ok {
|
|
t.finalizeHierarchicalMergeBlock(ctx, mctx, c, startRow, r-1)
|
|
delete(hMergeStartRow, c)
|
|
}
|
|
|
|
leftCellContinuedHierarchical = false
|
|
}
|
|
|
|
mctx.rowMerges[r][c] = currentState
|
|
}
|
|
}
|
|
|
|
lastRowIdx := len(ctx.rowLines) - 1
|
|
if lastRowIdx >= 0 {
|
|
for c, startRow := range hMergeStartRow {
|
|
t.finalizeHierarchicalMergeBlock(ctx, mctx, c, startRow, lastRowIdx)
|
|
}
|
|
}
|
|
ctx.logger.Debug("Hierarchical merge processing completed")
|
|
}
|
|
|
|
// applyHorizontalMergeWidths adjusts column widths for horizontal merges.
|
|
// Parameters include position, ctx for rendering, and mergeStates for merges.
|
|
// No return value.
|
|
func (t *Table) applyHorizontalMergeWidths(position tw.Position, ctx *renderContext, mergeStates map[int]tw.MergeState) {
|
|
if mergeStates == nil {
|
|
t.logger.Debugf("applyHorizontalMergeWidths: Skipping %s - no merge states", position)
|
|
return
|
|
}
|
|
t.logger.Debugf("applyHorizontalMergeWidths: Applying HMerge width recalc for %s", position)
|
|
|
|
numCols := ctx.numCols
|
|
targetWidthsMap := ctx.widths[position]
|
|
originalNormalizedWidths := tw.NewMapper[int, int]()
|
|
for i := 0; i < numCols; i++ {
|
|
originalNormalizedWidths.Set(i, targetWidthsMap.Get(i))
|
|
}
|
|
|
|
separatorWidth := 0
|
|
if t.renderer != nil {
|
|
rendererConfig := t.renderer.Config()
|
|
if rendererConfig.Settings.Separators.BetweenColumns.Enabled() {
|
|
separatorWidth = twwidth.Width(rendererConfig.Symbols.Column())
|
|
}
|
|
}
|
|
|
|
processedCols := make(map[int]bool)
|
|
|
|
for col := 0; col < numCols; col++ {
|
|
if processedCols[col] {
|
|
continue
|
|
}
|
|
|
|
state, exists := mergeStates[col]
|
|
if !exists {
|
|
continue
|
|
}
|
|
|
|
if state.Horizontal.Present && state.Horizontal.Start {
|
|
totalWidth := 0
|
|
span := state.Horizontal.Span
|
|
t.logger.Debugf(" -> HMerge detected: startCol=%d, span=%d, separatorWidth=%d", col, span, separatorWidth)
|
|
|
|
for i := 0; i < span && (col+i) < numCols; i++ {
|
|
currentColIndex := col + i
|
|
normalizedWidth := originalNormalizedWidths.Get(currentColIndex)
|
|
totalWidth += normalizedWidth
|
|
t.logger.Debugf(" -> col %d: adding normalized width %d", currentColIndex, normalizedWidth)
|
|
|
|
if i > 0 && separatorWidth > 0 {
|
|
totalWidth += separatorWidth
|
|
t.logger.Debugf(" -> col %d: adding separator width %d", currentColIndex, separatorWidth)
|
|
}
|
|
}
|
|
|
|
targetWidthsMap.Set(col, totalWidth)
|
|
t.logger.Debugf(" -> Set %s col %d width to %d (merged)", position, col, totalWidth)
|
|
processedCols[col] = true
|
|
|
|
for i := 1; i < span && (col+i) < numCols; i++ {
|
|
targetWidthsMap.Set(col+i, 0)
|
|
t.logger.Debugf(" -> Set %s col %d width to 0 (part of merge)", position, col+i)
|
|
processedCols[col+i] = true
|
|
}
|
|
}
|
|
}
|
|
ctx.logger.Debugf("applyHorizontalMergeWidths: Final widths for %s: %v", position, targetWidthsMap)
|
|
}
|
|
|
|
// applyVerticalMerges applies vertical merges to row content.
|
|
// Parameters ctx and mctx hold rendering and merge state.
|
|
// No return value.
|
|
func (t *Table) applyVerticalMerges(ctx *renderContext, mctx *mergeContext) {
|
|
ctx.logger.Debugf("Applying vertical merges across %d rows", len(ctx.rowLines))
|
|
numCols := ctx.numCols
|
|
|
|
mergeStartRow := make(map[int]int)
|
|
mergeStartContent := make(map[int]string)
|
|
|
|
for i := 0; i < len(ctx.rowLines); i++ {
|
|
if i >= len(mctx.rowMerges) {
|
|
newRowMerges := make([]map[int]tw.MergeState, i+1)
|
|
copy(newRowMerges, mctx.rowMerges)
|
|
for k := len(mctx.rowMerges); k <= i; k++ {
|
|
newRowMerges[k] = make(map[int]tw.MergeState)
|
|
}
|
|
mctx.rowMerges = newRowMerges
|
|
ctx.logger.Debugf("Extended rowMerges to index %d", i)
|
|
} else if mctx.rowMerges[i] == nil {
|
|
mctx.rowMerges[i] = make(map[int]tw.MergeState)
|
|
}
|
|
|
|
if len(ctx.rowLines[i]) == 0 {
|
|
continue
|
|
}
|
|
currentLineContent := ctx.rowLines[i]
|
|
|
|
for col := 0; col < numCols; col++ {
|
|
// Join all lines of the cell to compare full content
|
|
var currentVal strings.Builder
|
|
for _, line := range currentLineContent {
|
|
if col < len(line) {
|
|
currentVal.WriteString(line[col])
|
|
}
|
|
}
|
|
|
|
currentValStr := t.Trimmer(currentVal.String())
|
|
|
|
startRow, ongoingMerge := mergeStartRow[col]
|
|
startContent := mergeStartContent[col]
|
|
mergeState := mctx.rowMerges[i][col]
|
|
|
|
if ongoingMerge && currentValStr == startContent && currentValStr != "" {
|
|
mergeState.Vertical = tw.MergeStateOption{
|
|
Present: true,
|
|
Span: 0,
|
|
Start: false,
|
|
End: false,
|
|
}
|
|
mctx.rowMerges[i][col] = mergeState
|
|
for lineIdx := range ctx.rowLines[i] {
|
|
if col < len(ctx.rowLines[i][lineIdx]) {
|
|
ctx.rowLines[i][lineIdx][col] = tw.Empty
|
|
}
|
|
}
|
|
ctx.logger.Debugf("Vertical merge continued at row %d, col %d", i, col)
|
|
} else {
|
|
if ongoingMerge {
|
|
endedRow := i - 1
|
|
if endedRow >= 0 && endedRow >= startRow {
|
|
startState := mctx.rowMerges[startRow][col]
|
|
startState.Vertical.Span = (endedRow - startRow) + 1
|
|
startState.Vertical.End = startState.Vertical.Span == 1
|
|
mctx.rowMerges[startRow][col] = startState
|
|
|
|
endState := mctx.rowMerges[endedRow][col]
|
|
endState.Vertical.End = true
|
|
endState.Vertical.Span = startState.Vertical.Span
|
|
mctx.rowMerges[endedRow][col] = endState
|
|
ctx.logger.Debugf("Vertical merge ended at row %d, col %d, span %d", endedRow, col, startState.Vertical.Span)
|
|
}
|
|
delete(mergeStartRow, col)
|
|
delete(mergeStartContent, col)
|
|
}
|
|
|
|
if currentValStr != "" {
|
|
mergeState.Vertical = tw.MergeStateOption{
|
|
Present: true,
|
|
Span: 1,
|
|
Start: true,
|
|
End: false,
|
|
}
|
|
mctx.rowMerges[i][col] = mergeState
|
|
mergeStartRow[col] = i
|
|
mergeStartContent[col] = currentValStr
|
|
ctx.logger.Debugf("Vertical merge started at row %d, col %d", i, col)
|
|
} else if !mergeState.Horizontal.Present {
|
|
mergeState.Vertical = tw.MergeStateOption{}
|
|
mctx.rowMerges[i][col] = mergeState
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
lastRowIdx := len(ctx.rowLines) - 1
|
|
if lastRowIdx >= 0 {
|
|
for col, startRow := range mergeStartRow {
|
|
startState := mctx.rowMerges[startRow][col]
|
|
finalSpan := (lastRowIdx - startRow) + 1
|
|
startState.Vertical.Span = finalSpan
|
|
startState.Vertical.End = finalSpan == 1
|
|
mctx.rowMerges[startRow][col] = startState
|
|
|
|
endState := mctx.rowMerges[lastRowIdx][col]
|
|
endState.Vertical.Present = true
|
|
endState.Vertical.End = true
|
|
endState.Vertical.Span = finalSpan
|
|
if startRow != lastRowIdx {
|
|
endState.Vertical.Start = false
|
|
}
|
|
mctx.rowMerges[lastRowIdx][col] = endState
|
|
ctx.logger.Debugf("Vertical merge finalized at row %d, col %d, span %d", lastRowIdx, col, finalSpan)
|
|
}
|
|
}
|
|
ctx.logger.Debug("Vertical merges completed")
|
|
}
|
|
|
|
// buildAdjacentCells constructs cell contexts for adjacent lines.
|
|
// Parameters include ctx, mctx, hctx, and direction (-1 for prev, +1 for next).
|
|
// Returns a map of column indices to CellContext for the adjacent line.
|
|
func (t *Table) buildAdjacentCells(ctx *renderContext, mctx *mergeContext, hctx *helperContext, direction int) map[int]tw.CellContext {
|
|
adjCells := make(map[int]tw.CellContext)
|
|
var adjLine []string
|
|
var adjMerges map[int]tw.MergeState
|
|
found := false
|
|
adjPosition := hctx.position // Assume adjacent line is in the same section initially
|
|
|
|
switch hctx.position {
|
|
case tw.Header:
|
|
targetLineIdx := hctx.lineIdx + direction
|
|
if direction < 0 { // Previous
|
|
if targetLineIdx >= 0 && targetLineIdx < len(ctx.headerLines) {
|
|
adjLine = ctx.headerLines[targetLineIdx]
|
|
adjMerges = mctx.headerMerges
|
|
found = true
|
|
}
|
|
} else { // Next
|
|
if targetLineIdx < len(ctx.headerLines) {
|
|
adjLine = ctx.headerLines[targetLineIdx]
|
|
adjMerges = mctx.headerMerges
|
|
found = true
|
|
} else if len(ctx.rowLines) > 0 && len(ctx.rowLines[0]) > 0 && len(mctx.rowMerges) > 0 {
|
|
adjLine = ctx.rowLines[0][0]
|
|
adjMerges = mctx.rowMerges[0]
|
|
adjPosition = tw.Row
|
|
found = true
|
|
} else if len(ctx.footerLines) > 0 {
|
|
adjLine = ctx.footerLines[0]
|
|
adjMerges = mctx.footerMerges
|
|
adjPosition = tw.Footer
|
|
found = true
|
|
}
|
|
}
|
|
case tw.Row:
|
|
targetLineIdx := hctx.lineIdx + direction
|
|
if hctx.rowIdx < 0 || hctx.rowIdx >= len(ctx.rowLines) || hctx.rowIdx >= len(mctx.rowMerges) {
|
|
t.logger.Debugf("Warning: Invalid row index %d in buildAdjacentCells", hctx.rowIdx)
|
|
return nil
|
|
}
|
|
currentRowLines := ctx.rowLines[hctx.rowIdx]
|
|
currentMerges := mctx.rowMerges[hctx.rowIdx]
|
|
|
|
if direction < 0 { // Previous
|
|
if targetLineIdx >= 0 && targetLineIdx < len(currentRowLines) {
|
|
adjLine = currentRowLines[targetLineIdx]
|
|
adjMerges = currentMerges
|
|
found = true
|
|
} else if targetLineIdx < 0 {
|
|
targetRowIdx := hctx.rowIdx - 1
|
|
if targetRowIdx >= 0 && targetRowIdx < len(ctx.rowLines) && targetRowIdx < len(mctx.rowMerges) {
|
|
prevRowLines := ctx.rowLines[targetRowIdx]
|
|
if len(prevRowLines) > 0 {
|
|
adjLine = prevRowLines[len(prevRowLines)-1]
|
|
adjMerges = mctx.rowMerges[targetRowIdx]
|
|
found = true
|
|
}
|
|
} else if len(ctx.headerLines) > 0 {
|
|
adjLine = ctx.headerLines[len(ctx.headerLines)-1]
|
|
adjMerges = mctx.headerMerges
|
|
adjPosition = tw.Header
|
|
found = true
|
|
}
|
|
}
|
|
} else { // Next
|
|
if targetLineIdx >= 0 && targetLineIdx < len(currentRowLines) {
|
|
adjLine = currentRowLines[targetLineIdx]
|
|
adjMerges = currentMerges
|
|
found = true
|
|
} else if targetLineIdx >= len(currentRowLines) {
|
|
targetRowIdx := hctx.rowIdx + 1
|
|
if targetRowIdx < len(ctx.rowLines) && targetRowIdx < len(mctx.rowMerges) && len(ctx.rowLines[targetRowIdx]) > 0 {
|
|
adjLine = ctx.rowLines[targetRowIdx][0]
|
|
adjMerges = mctx.rowMerges[targetRowIdx]
|
|
found = true
|
|
} else if len(ctx.footerLines) > 0 {
|
|
adjLine = ctx.footerLines[0]
|
|
adjMerges = mctx.footerMerges
|
|
adjPosition = tw.Footer
|
|
found = true
|
|
}
|
|
}
|
|
}
|
|
case tw.Footer:
|
|
targetLineIdx := hctx.lineIdx + direction
|
|
if direction < 0 { // Previous
|
|
if targetLineIdx >= 0 && targetLineIdx < len(ctx.footerLines) {
|
|
adjLine = ctx.footerLines[targetLineIdx]
|
|
adjMerges = mctx.footerMerges
|
|
found = true
|
|
} else if targetLineIdx < 0 {
|
|
if len(ctx.rowLines) > 0 {
|
|
lastRowIdx := len(ctx.rowLines) - 1
|
|
if lastRowIdx < len(mctx.rowMerges) && len(ctx.rowLines[lastRowIdx]) > 0 {
|
|
lastRowLines := ctx.rowLines[lastRowIdx]
|
|
adjLine = lastRowLines[len(lastRowLines)-1]
|
|
adjMerges = mctx.rowMerges[lastRowIdx]
|
|
adjPosition = tw.Row
|
|
found = true
|
|
}
|
|
} else if len(ctx.headerLines) > 0 {
|
|
adjLine = ctx.headerLines[len(ctx.headerLines)-1]
|
|
adjMerges = mctx.headerMerges
|
|
adjPosition = tw.Header
|
|
found = true
|
|
}
|
|
}
|
|
} else { // Next
|
|
if targetLineIdx >= 0 && targetLineIdx < len(ctx.footerLines) {
|
|
adjLine = ctx.footerLines[targetLineIdx]
|
|
adjMerges = mctx.footerMerges
|
|
found = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return nil
|
|
}
|
|
|
|
if adjMerges == nil {
|
|
adjMerges = make(map[int]tw.MergeState)
|
|
t.logger.Debugf("Warning: adjMerges was nil in buildAdjacentCells despite found=true")
|
|
}
|
|
|
|
paddedAdjLine := padLine(adjLine, ctx.numCols)
|
|
|
|
for j := 0; j < ctx.numCols; j++ {
|
|
mergeState := adjMerges[j]
|
|
cellData := paddedAdjLine[j]
|
|
finalAdjColWidth := ctx.widths[adjPosition].Get(j)
|
|
|
|
adjCells[j] = tw.CellContext{
|
|
Data: cellData,
|
|
Merge: mergeState,
|
|
Width: finalAdjColWidth,
|
|
}
|
|
}
|
|
return adjCells
|
|
}
|
|
|
|
// buildCellContexts creates CellContext objects for a given line in batch mode.
|
|
// Parameters include ctx, mctx, hctx, aligns, and padding for rendering.
|
|
// Returns a renderMergeResponse with current, previous, and next cell contexts.
|
|
func (t *Table) buildCellContexts(ctx *renderContext, mctx *mergeContext, hctx *helperContext, aligns map[int]tw.Align, padding map[int]tw.Padding) renderMergeResponse {
|
|
t.logger.Debugf("buildCellContexts: Building contexts for position=%s, rowIdx=%d, lineIdx=%d", hctx.position, hctx.rowIdx, hctx.lineIdx)
|
|
var merges map[int]tw.MergeState
|
|
switch hctx.position {
|
|
case tw.Header:
|
|
merges = mctx.headerMerges
|
|
case tw.Row:
|
|
if hctx.rowIdx >= 0 && hctx.rowIdx < len(mctx.rowMerges) && mctx.rowMerges[hctx.rowIdx] != nil {
|
|
merges = mctx.rowMerges[hctx.rowIdx]
|
|
} else {
|
|
merges = make(map[int]tw.MergeState)
|
|
t.logger.Warnf("buildCellContexts: Invalid row index %d or nil merges for row", hctx.rowIdx)
|
|
}
|
|
case tw.Footer:
|
|
merges = mctx.footerMerges
|
|
default:
|
|
merges = make(map[int]tw.MergeState)
|
|
t.logger.Warnf("buildCellContexts: Invalid position '%s'", hctx.position)
|
|
}
|
|
|
|
cells := t.buildCoreCellContexts(hctx.line, merges, ctx.widths[hctx.position], aligns, padding, ctx.numCols)
|
|
return renderMergeResponse{
|
|
cells: cells,
|
|
prevCells: t.buildAdjacentCells(ctx, mctx, hctx, -1),
|
|
nextCells: t.buildAdjacentCells(ctx, mctx, hctx, +1),
|
|
location: hctx.location,
|
|
}
|
|
}
|
|
|
|
// buildCoreCellContexts constructs CellContext objects for a single line, shared between batch and streaming modes.
|
|
// Parameters:
|
|
// - line: The content of the current line (padded to numCols).
|
|
// - merges: Merge states for the line's columns (map[int]tw.MergeState).
|
|
// - widths: Column widths (tw.Mapper[int, int]).
|
|
// - aligns: Column alignments (map[int]tw.Align).
|
|
// - padding: Column padding settings (map[int]tw.Padding).
|
|
// - numCols: Number of columns to process.
|
|
// Returns a map of column indices to CellContext for the current line.
|
|
func (t *Table) buildCoreCellContexts(line []string, merges map[int]tw.MergeState, widths tw.Mapper[int, int], aligns map[int]tw.Align, padding map[int]tw.Padding, numCols int) map[int]tw.CellContext {
|
|
cells := make(map[int]tw.CellContext)
|
|
paddedLine := padLine(line, numCols)
|
|
for j := 0; j < numCols; j++ {
|
|
cellData := paddedLine[j]
|
|
mergeState := tw.MergeState{}
|
|
if merges != nil {
|
|
if state, ok := merges[j]; ok {
|
|
mergeState = state
|
|
}
|
|
}
|
|
cells[j] = tw.CellContext{
|
|
Data: cellData,
|
|
Align: aligns[j],
|
|
Padding: padding[j],
|
|
Width: widths.Get(j),
|
|
Merge: mergeState,
|
|
}
|
|
}
|
|
t.logger.Debugf("buildCoreCellContexts: Built cell contexts for %d columns", numCols)
|
|
return cells
|
|
}
|
|
|
|
// buildPaddingLineContents constructs a padding line for a given section, respecting column widths and horizontal merges.
|
|
// It generates a []string where each element is the padding content for a column, using the specified padChar.
|
|
func (t *Table) buildPaddingLineContents(padChar string, widths tw.Mapper[int, int], numCols int, merges map[int]tw.MergeState) []string {
|
|
line := make([]string, numCols)
|
|
padWidth := twwidth.Width(padChar)
|
|
if padWidth < 1 {
|
|
padWidth = 1
|
|
}
|
|
for j := 0; j < numCols; j++ {
|
|
mergeState := tw.MergeState{}
|
|
if merges != nil {
|
|
if state, ok := merges[j]; ok {
|
|
mergeState = state
|
|
}
|
|
}
|
|
if mergeState.Horizontal.Present && !mergeState.Horizontal.Start {
|
|
line[j] = tw.Empty
|
|
continue
|
|
}
|
|
colWd := widths.Get(j)
|
|
repeatCount := 0
|
|
if colWd > 0 && padWidth > 0 {
|
|
repeatCount = colWd / padWidth
|
|
}
|
|
if colWd > 0 && repeatCount < 1 {
|
|
repeatCount = 1
|
|
}
|
|
content := strings.Repeat(padChar, repeatCount)
|
|
line[j] = content
|
|
}
|
|
if t.logger.Enabled() {
|
|
t.logger.Debugf("Built padding line with char '%s' for %d columns", padChar, numCols)
|
|
}
|
|
return line
|
|
}
|
|
|
|
// calculateAndNormalizeWidths computes and normalizes column widths.
|
|
// Parameter ctx holds rendering state with width maps.
|
|
// Returns an error if width calculation fails.
|
|
func (t *Table) calculateAndNormalizeWidths(ctx *renderContext) error {
|
|
ctx.logger.Debugf("calculateAndNormalizeWidths: Computing and normalizing widths for %d columns. Compact: %v",
|
|
ctx.numCols, t.config.Behavior.Compact.Merge.Enabled())
|
|
|
|
// Initialize width maps
|
|
//t.headerWidths = tw.NewMapper[int, int]()
|
|
//t.rowWidths = tw.NewMapper[int, int]()
|
|
//t.footerWidths = tw.NewMapper[int, int]()
|
|
|
|
// Compute content-based widths for each section
|
|
for _, lines := range ctx.headerLines {
|
|
t.updateWidths(lines, t.headerWidths, t.config.Header.Padding)
|
|
}
|
|
rowWidthCache := make([]tw.Mapper[int, int], len(ctx.rowLines))
|
|
for i, row := range ctx.rowLines {
|
|
rowWidthCache[i] = tw.NewMapper[int, int]()
|
|
for _, line := range row {
|
|
t.updateWidths(line, rowWidthCache[i], t.config.Row.Padding)
|
|
for col, width := range rowWidthCache[i] {
|
|
currentMax, _ := t.rowWidths.OK(col)
|
|
if width > currentMax {
|
|
t.rowWidths.Set(col, width)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for _, lines := range ctx.footerLines {
|
|
t.updateWidths(lines, t.footerWidths, t.config.Footer.Padding)
|
|
}
|
|
ctx.logger.Debugf("Content-based widths: header=%v, row=%v, footer=%v", t.headerWidths, t.rowWidths, t.footerWidths)
|
|
|
|
// Analyze header merges for optimization
|
|
var headerMergeSpans map[int]int
|
|
if t.config.Header.Formatting.MergeMode&tw.MergeHorizontal != 0 && len(ctx.headerLines) > 0 {
|
|
headerMergeSpans = make(map[int]int)
|
|
visitedCols := make(map[int]bool)
|
|
firstHeaderLine := ctx.headerLines[0]
|
|
if len(firstHeaderLine) > 0 {
|
|
for i := 0; i < len(firstHeaderLine); {
|
|
if visitedCols[i] {
|
|
i++
|
|
continue
|
|
}
|
|
var currentLogicalCellContentBuilder strings.Builder
|
|
for _, hLine := range ctx.headerLines {
|
|
if i < len(hLine) {
|
|
currentLogicalCellContentBuilder.WriteString(hLine[i])
|
|
}
|
|
}
|
|
currentHeaderCellContent := t.Trimmer(currentLogicalCellContentBuilder.String())
|
|
span := 1
|
|
for j := i + 1; j < len(firstHeaderLine); j++ {
|
|
var nextLogicalCellContentBuilder strings.Builder
|
|
for _, hLine := range ctx.headerLines {
|
|
if j < len(hLine) {
|
|
nextLogicalCellContentBuilder.WriteString(hLine[j])
|
|
}
|
|
}
|
|
nextHeaderCellContent := t.Trimmer(nextLogicalCellContentBuilder.String())
|
|
if currentHeaderCellContent == nextHeaderCellContent && currentHeaderCellContent != "" && currentHeaderCellContent != "-" {
|
|
span++
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
if span > 1 {
|
|
headerMergeSpans[i] = span
|
|
for k := 0; k < span; k++ {
|
|
visitedCols[i+k] = true
|
|
}
|
|
}
|
|
i += span
|
|
}
|
|
}
|
|
if len(headerMergeSpans) > 0 {
|
|
ctx.logger.Debugf("Header merge spans: %v", headerMergeSpans)
|
|
}
|
|
}
|
|
|
|
// Determine natural column widths
|
|
naturalColumnWidths := tw.NewMapper[int, int]()
|
|
for i := 0; i < ctx.numCols; i++ {
|
|
width := 0
|
|
if colWidth, ok := t.config.Widths.PerColumn.OK(i); ok && colWidth >= 0 {
|
|
width = colWidth
|
|
ctx.logger.Debugf("Col %d width from Config.Widths.PerColumn: %d", i, width)
|
|
} else {
|
|
maxRowFooterWidth := tw.Max(t.rowWidths.Get(i), t.footerWidths.Get(i))
|
|
headerCellOriginalWidth := t.headerWidths.Get(i)
|
|
if t.config.Behavior.Compact.Merge.Enabled() &&
|
|
t.config.Header.Formatting.MergeMode&tw.MergeHorizontal != 0 &&
|
|
headerMergeSpans != nil {
|
|
isColInHeaderMerge := false
|
|
for startCol, span := range headerMergeSpans {
|
|
if i >= startCol && i < startCol+span {
|
|
isColInHeaderMerge = true
|
|
break
|
|
}
|
|
}
|
|
if isColInHeaderMerge {
|
|
width = maxRowFooterWidth
|
|
if width == 0 && headerCellOriginalWidth > 0 {
|
|
width = headerCellOriginalWidth
|
|
}
|
|
ctx.logger.Debugf("Col %d (in merge) width: %d (row/footer: %d, header: %d)", i, width, maxRowFooterWidth, headerCellOriginalWidth)
|
|
} else {
|
|
width = tw.Max(headerCellOriginalWidth, maxRowFooterWidth)
|
|
ctx.logger.Debugf("Col %d (not in merge) width: %d", i, width)
|
|
}
|
|
} else {
|
|
width = tw.Max(tw.Max(headerCellOriginalWidth, t.rowWidths.Get(i)), t.footerWidths.Get(i))
|
|
ctx.logger.Debugf("Col %d width (no merge): %d", i, width)
|
|
}
|
|
if width == 0 && (headerCellOriginalWidth > 0 || t.rowWidths.Get(i) > 0 || t.footerWidths.Get(i) > 0) {
|
|
width = tw.Max(tw.Max(headerCellOriginalWidth, t.rowWidths.Get(i)), t.footerWidths.Get(i))
|
|
}
|
|
if width == 0 {
|
|
width = 1
|
|
}
|
|
}
|
|
naturalColumnWidths.Set(i, width)
|
|
}
|
|
ctx.logger.Debugf("Natural column widths: %v", naturalColumnWidths)
|
|
|
|
// Expand columns for merged header content if needed
|
|
workingWidths := naturalColumnWidths.Clone()
|
|
if t.config.Header.Formatting.MergeMode&tw.MergeHorizontal != 0 && headerMergeSpans != nil {
|
|
if span, isOneBigMerge := headerMergeSpans[0]; isOneBigMerge && span == ctx.numCols && ctx.numCols > 0 {
|
|
var firstHeaderCellLogicalContentBuilder strings.Builder
|
|
for _, hLine := range ctx.headerLines {
|
|
if 0 < len(hLine) {
|
|
firstHeaderCellLogicalContentBuilder.WriteString(hLine[0])
|
|
}
|
|
}
|
|
mergedContentString := t.Trimmer(firstHeaderCellLogicalContentBuilder.String())
|
|
headerCellPadding := t.config.Header.Padding.Global
|
|
if 0 < len(t.config.Header.Padding.PerColumn) && t.config.Header.Padding.PerColumn[0].Paddable() {
|
|
headerCellPadding = t.config.Header.Padding.PerColumn[0]
|
|
}
|
|
actualMergedHeaderContentPhysicalWidth := twwidth.Width(mergedContentString) +
|
|
twwidth.Width(headerCellPadding.Left) +
|
|
twwidth.Width(headerCellPadding.Right)
|
|
currentSumOfColumnWidths := 0
|
|
workingWidths.Each(func(_ int, w int) { currentSumOfColumnWidths += w })
|
|
numSeparatorsInFullSpan := 0
|
|
if ctx.numCols > 1 {
|
|
if t.renderer != nil && t.renderer.Config().Settings.Separators.BetweenColumns.Enabled() {
|
|
numSeparatorsInFullSpan = (ctx.numCols - 1) * twwidth.Width(t.renderer.Config().Symbols.Column())
|
|
}
|
|
}
|
|
totalCurrentSpanPhysicalWidth := currentSumOfColumnWidths + numSeparatorsInFullSpan
|
|
if actualMergedHeaderContentPhysicalWidth > totalCurrentSpanPhysicalWidth {
|
|
ctx.logger.Debugf("Merged header content '%s' (width %d) exceeds total width %d. Expanding.",
|
|
mergedContentString, actualMergedHeaderContentPhysicalWidth, totalCurrentSpanPhysicalWidth)
|
|
shortfall := actualMergedHeaderContentPhysicalWidth - totalCurrentSpanPhysicalWidth
|
|
numNonZeroCols := 0
|
|
workingWidths.Each(func(_ int, w int) {
|
|
if w > 0 {
|
|
numNonZeroCols++
|
|
}
|
|
})
|
|
if numNonZeroCols == 0 && ctx.numCols > 0 {
|
|
numNonZeroCols = ctx.numCols
|
|
}
|
|
if numNonZeroCols > 0 && shortfall > 0 {
|
|
extraPerColumn := int(math.Ceil(float64(shortfall) / float64(numNonZeroCols)))
|
|
finalSumAfterExpansion := 0
|
|
workingWidths.Each(func(colIdx int, currentW int) {
|
|
if currentW > 0 || (numNonZeroCols == ctx.numCols && ctx.numCols > 0) {
|
|
newWidth := currentW + extraPerColumn
|
|
workingWidths.Set(colIdx, newWidth)
|
|
finalSumAfterExpansion += newWidth
|
|
ctx.logger.Debugf("Col %d expanded by %d to %d", colIdx, extraPerColumn, newWidth)
|
|
} else {
|
|
finalSumAfterExpansion += currentW
|
|
}
|
|
})
|
|
overDistributed := (finalSumAfterExpansion + numSeparatorsInFullSpan) - actualMergedHeaderContentPhysicalWidth
|
|
if overDistributed > 0 {
|
|
ctx.logger.Debugf("Correcting over-distribution of %d", overDistributed)
|
|
// Sort columns for deterministic reduction
|
|
sortedCols := workingWidths.SortedKeys()
|
|
for i := 0; i < overDistributed; i++ {
|
|
// Reduce from highest-indexed column
|
|
for j := len(sortedCols) - 1; j >= 0; j-- {
|
|
col := sortedCols[j]
|
|
if workingWidths.Get(col) > 1 && naturalColumnWidths.Get(col) < workingWidths.Get(col) {
|
|
workingWidths.Set(col, workingWidths.Get(col)-1)
|
|
ctx.logger.Debugf("Reduced col %d by 1 to %d", col, workingWidths.Get(col))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ctx.logger.Debugf("Widths after merged header expansion: %v", workingWidths)
|
|
|
|
// Apply global width constraint
|
|
finalWidths := workingWidths.Clone()
|
|
if t.config.Widths.Global > 0 {
|
|
ctx.logger.Debugf("Applying global width constraint: %d", t.config.Widths.Global)
|
|
currentSumOfFinalColWidths := 0
|
|
finalWidths.Each(func(_ int, w int) { currentSumOfFinalColWidths += w })
|
|
numSeparators := 0
|
|
if ctx.numCols > 1 && t.renderer != nil && t.renderer.Config().Settings.Separators.BetweenColumns.Enabled() {
|
|
numSeparators = (ctx.numCols - 1) * twwidth.Width(t.renderer.Config().Symbols.Column())
|
|
}
|
|
totalCurrentTablePhysicalWidth := currentSumOfFinalColWidths + numSeparators
|
|
if totalCurrentTablePhysicalWidth > t.config.Widths.Global {
|
|
ctx.logger.Debugf("Table width %d exceeds global limit %d. Shrinking.", totalCurrentTablePhysicalWidth, t.config.Widths.Global)
|
|
targetTotalColumnContentWidth := t.config.Widths.Global - numSeparators
|
|
if targetTotalColumnContentWidth < 0 {
|
|
targetTotalColumnContentWidth = 0
|
|
}
|
|
if ctx.numCols > 0 && targetTotalColumnContentWidth < ctx.numCols {
|
|
targetTotalColumnContentWidth = ctx.numCols
|
|
}
|
|
hardMinimums := tw.NewMapper[int, int]()
|
|
sumOfHardMinimums := 0
|
|
isHeaderContentHardToWrap := !(t.config.Header.Formatting.AutoWrap == tw.WrapNormal || t.config.Header.Formatting.AutoWrap == tw.WrapBreak)
|
|
for i := 0; i < ctx.numCols; i++ {
|
|
minW := 1
|
|
if isHeaderContentHardToWrap && len(ctx.headerLines) > 0 {
|
|
headerColNaturalWidthWithPadding := t.headerWidths.Get(i)
|
|
if headerColNaturalWidthWithPadding > minW {
|
|
minW = headerColNaturalWidthWithPadding
|
|
}
|
|
}
|
|
hardMinimums.Set(i, minW)
|
|
sumOfHardMinimums += minW
|
|
}
|
|
ctx.logger.Debugf("Hard minimums: %v (sum: %d)", hardMinimums, sumOfHardMinimums)
|
|
if targetTotalColumnContentWidth < sumOfHardMinimums && sumOfHardMinimums > 0 {
|
|
ctx.logger.Warnf("Target width %d below minimums %d. Scaling.", targetTotalColumnContentWidth, sumOfHardMinimums)
|
|
scaleFactorMin := float64(targetTotalColumnContentWidth) / float64(sumOfHardMinimums)
|
|
if scaleFactorMin < 0 {
|
|
scaleFactorMin = 0
|
|
}
|
|
tempSum := 0
|
|
scaledHardMinimums := tw.NewMapper[int, int]()
|
|
hardMinimums.Each(func(colIdx int, currentMinW int) {
|
|
scaledMinW := int(math.Round(float64(currentMinW) * scaleFactorMin))
|
|
if scaledMinW < 1 && targetTotalColumnContentWidth > 0 {
|
|
scaledMinW = 1
|
|
} else if scaledMinW < 0 {
|
|
scaledMinW = 0
|
|
}
|
|
scaledHardMinimums.Set(colIdx, scaledMinW)
|
|
tempSum += scaledMinW
|
|
})
|
|
errorDiffMin := targetTotalColumnContentWidth - tempSum
|
|
if errorDiffMin != 0 && scaledHardMinimums.Len() > 0 {
|
|
sortedKeys := scaledHardMinimums.SortedKeys()
|
|
for i := 0; i < int(math.Abs(float64(errorDiffMin))); i++ {
|
|
keyToAdjust := sortedKeys[i%len(sortedKeys)]
|
|
val := scaledHardMinimums.Get(keyToAdjust)
|
|
adj := 1
|
|
if errorDiffMin < 0 {
|
|
adj = -1
|
|
}
|
|
if val+adj >= 1 || (val+adj == 0 && targetTotalColumnContentWidth == 0) {
|
|
scaledHardMinimums.Set(keyToAdjust, val+adj)
|
|
} else if adj > 0 {
|
|
scaledHardMinimums.Set(keyToAdjust, val+adj)
|
|
}
|
|
}
|
|
}
|
|
finalWidths = scaledHardMinimums.Clone()
|
|
ctx.logger.Debugf("Scaled minimums: %v", finalWidths)
|
|
} else {
|
|
finalWidths = hardMinimums.Clone()
|
|
widthAllocatedByMinimums := sumOfHardMinimums
|
|
remainingWidthToDistribute := targetTotalColumnContentWidth - widthAllocatedByMinimums
|
|
ctx.logger.Debugf("Target: %d, minimums: %d, remaining: %d", targetTotalColumnContentWidth, widthAllocatedByMinimums, remainingWidthToDistribute)
|
|
if remainingWidthToDistribute > 0 {
|
|
sumOfFlexiblePotentialBase := 0
|
|
flexibleColsOriginalWidths := tw.NewMapper[int, int]()
|
|
for i := 0; i < ctx.numCols; i++ {
|
|
naturalW := workingWidths.Get(i)
|
|
minW := hardMinimums.Get(i)
|
|
if naturalW > minW {
|
|
sumOfFlexiblePotentialBase += (naturalW - minW)
|
|
flexibleColsOriginalWidths.Set(i, naturalW)
|
|
}
|
|
}
|
|
ctx.logger.Debugf("Flexible potential: %d, flexible widths: %v", sumOfFlexiblePotentialBase, flexibleColsOriginalWidths)
|
|
if sumOfFlexiblePotentialBase > 0 {
|
|
distributedExtraSum := 0
|
|
sortedFlexKeys := flexibleColsOriginalWidths.SortedKeys()
|
|
for _, colIdx := range sortedFlexKeys {
|
|
naturalWOfCol := flexibleColsOriginalWidths.Get(colIdx)
|
|
hardMinOfCol := hardMinimums.Get(colIdx)
|
|
flexiblePartOfCol := naturalWOfCol - hardMinOfCol
|
|
proportion := 0.0
|
|
if sumOfFlexiblePotentialBase > 0 {
|
|
proportion = float64(flexiblePartOfCol) / float64(sumOfFlexiblePotentialBase)
|
|
} else if len(sortedFlexKeys) > 0 {
|
|
proportion = 1.0 / float64(len(sortedFlexKeys))
|
|
}
|
|
extraForThisCol := int(math.Round(float64(remainingWidthToDistribute) * proportion))
|
|
currentAssignedW := finalWidths.Get(colIdx)
|
|
finalWidths.Set(colIdx, currentAssignedW+extraForThisCol)
|
|
distributedExtraSum += extraForThisCol
|
|
}
|
|
errorInDist := remainingWidthToDistribute - distributedExtraSum
|
|
ctx.logger.Debugf("Distributed %d, error: %d", distributedExtraSum, errorInDist)
|
|
if errorInDist != 0 && len(sortedFlexKeys) > 0 {
|
|
for i := 0; i < int(math.Abs(float64(errorInDist))); i++ {
|
|
colToAdjust := sortedFlexKeys[i%len(sortedFlexKeys)]
|
|
w := finalWidths.Get(colToAdjust)
|
|
adj := 1
|
|
if errorInDist < 0 {
|
|
adj = -1
|
|
}
|
|
if !(adj < 0 && w+adj < hardMinimums.Get(colToAdjust)) {
|
|
finalWidths.Set(colToAdjust, w+adj)
|
|
} else if adj > 0 {
|
|
finalWidths.Set(colToAdjust, w+adj)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if ctx.numCols > 0 {
|
|
extraPerCol := remainingWidthToDistribute / ctx.numCols
|
|
rem := remainingWidthToDistribute % ctx.numCols
|
|
for i := 0; i < ctx.numCols; i++ {
|
|
currentW := finalWidths.Get(i)
|
|
add := extraPerCol
|
|
if i < rem {
|
|
add++
|
|
}
|
|
finalWidths.Set(i, currentW+add)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
finalSumCheck := 0
|
|
finalWidths.Each(func(idx int, w int) {
|
|
if w < 1 && targetTotalColumnContentWidth > 0 {
|
|
finalWidths.Set(idx, 1)
|
|
} else if w < 0 {
|
|
finalWidths.Set(idx, 0)
|
|
}
|
|
finalSumCheck += finalWidths.Get(idx)
|
|
})
|
|
ctx.logger.Debugf("Final widths after scaling: %v (sum: %d, target: %d)", finalWidths, finalSumCheck, targetTotalColumnContentWidth)
|
|
}
|
|
}
|
|
|
|
// Assign final widths to context
|
|
ctx.widths[tw.Header] = finalWidths.Clone()
|
|
ctx.widths[tw.Row] = finalWidths.Clone()
|
|
ctx.widths[tw.Footer] = finalWidths.Clone()
|
|
ctx.logger.Debugf("Final normalized widths: header=%v, row=%v, footer=%v", ctx.widths[tw.Header], ctx.widths[tw.Row], ctx.widths[tw.Footer])
|
|
return nil
|
|
}
|
|
|
|
// calculateContentMaxWidth computes the maximum content width for a column, accounting for padding and mode-specific constraints.
|
|
// Returns the effective content width (after subtracting padding) for the given column index.
|
|
func (t *Table) calculateContentMaxWidth(colIdx int, config tw.CellConfig, padLeftWidth, padRightWidth int, isStreaming bool) int {
|
|
var effectiveContentMaxWidth int
|
|
|
|
if isStreaming {
|
|
// Existing streaming logic remains unchanged
|
|
totalColumnWidthFromStream := t.streamWidths.Get(colIdx)
|
|
if totalColumnWidthFromStream < 0 {
|
|
totalColumnWidthFromStream = 0
|
|
}
|
|
effectiveContentMaxWidth = totalColumnWidthFromStream - padLeftWidth - padRightWidth
|
|
if effectiveContentMaxWidth < 1 && totalColumnWidthFromStream > (padLeftWidth+padRightWidth) {
|
|
effectiveContentMaxWidth = 1
|
|
} else if effectiveContentMaxWidth < 0 {
|
|
effectiveContentMaxWidth = 0
|
|
}
|
|
if totalColumnWidthFromStream == 0 {
|
|
effectiveContentMaxWidth = 0
|
|
}
|
|
t.logger.Debugf("calculateContentMaxWidth: Streaming col %d, TotalColWd=%d, PadL=%d, PadR=%d -> ContentMaxWd=%d", colIdx, totalColumnWidthFromStream, padLeftWidth, padRightWidth, effectiveContentMaxWidth)
|
|
} else {
|
|
// New priority-based width constraint checking
|
|
constraintTotalCellWidth := 0
|
|
hasConstraint := false
|
|
|
|
// 1. Check new Widths.PerColumn (highest priority)
|
|
if t.config.Widths.Constrained() {
|
|
|
|
if colWidth, ok := t.config.Widths.PerColumn.OK(colIdx); ok && colWidth > 0 {
|
|
constraintTotalCellWidth = colWidth
|
|
hasConstraint = true
|
|
t.logger.Debugf("calculateContentMaxWidth: Using Widths.PerColumn[%d] = %d",
|
|
colIdx, constraintTotalCellWidth)
|
|
}
|
|
|
|
// 2. Check new Widths.Global
|
|
if !hasConstraint && t.config.Widths.Global > 0 {
|
|
constraintTotalCellWidth = t.config.Widths.Global
|
|
hasConstraint = true
|
|
t.logger.Debugf("calculateContentMaxWidth: Using Widths.Global = %d", constraintTotalCellWidth)
|
|
}
|
|
}
|
|
|
|
// 3. Fall back to legacy ColMaxWidths.PerColumn (backward compatibility)
|
|
if !hasConstraint && config.ColMaxWidths.PerColumn != nil {
|
|
if colMax, ok := config.ColMaxWidths.PerColumn.OK(colIdx); ok && colMax > 0 {
|
|
constraintTotalCellWidth = colMax
|
|
hasConstraint = true
|
|
t.logger.Debugf("calculateContentMaxWidth: Using legacy ColMaxWidths.PerColumn[%d] = %d",
|
|
colIdx, constraintTotalCellWidth)
|
|
}
|
|
}
|
|
|
|
// 4. Fall back to legacy ColMaxWidths.Global
|
|
if !hasConstraint && config.ColMaxWidths.Global > 0 {
|
|
constraintTotalCellWidth = config.ColMaxWidths.Global
|
|
hasConstraint = true
|
|
t.logger.Debugf("calculateContentMaxWidth: Using legacy ColMaxWidths.Global = %d",
|
|
constraintTotalCellWidth)
|
|
}
|
|
|
|
// 5. Fall back to table MaxWidth if auto-wrapping
|
|
if !hasConstraint && t.config.MaxWidth > 0 && config.Formatting.AutoWrap != tw.WrapNone {
|
|
constraintTotalCellWidth = t.config.MaxWidth
|
|
hasConstraint = true
|
|
t.logger.Debugf("calculateContentMaxWidth: Using table MaxWidth = %d (AutoWrap enabled)",
|
|
constraintTotalCellWidth)
|
|
}
|
|
|
|
// Calculate effective width based on found constraint
|
|
if hasConstraint {
|
|
effectiveContentMaxWidth = constraintTotalCellWidth - padLeftWidth - padRightWidth
|
|
if effectiveContentMaxWidth < 1 && constraintTotalCellWidth > (padLeftWidth+padRightWidth) {
|
|
effectiveContentMaxWidth = 1
|
|
} else if effectiveContentMaxWidth < 0 {
|
|
effectiveContentMaxWidth = 0
|
|
}
|
|
t.logger.Debugf("calculateContentMaxWidth: ConstraintTotalCellWidth=%d, PadL=%d, PadR=%d -> EffectiveContentMaxWidth=%d",
|
|
constraintTotalCellWidth, padLeftWidth, padRightWidth, effectiveContentMaxWidth)
|
|
} else {
|
|
effectiveContentMaxWidth = 0
|
|
t.logger.Debugf("calculateContentMaxWidth: No width constraints found for column %d", colIdx)
|
|
}
|
|
}
|
|
|
|
return effectiveContentMaxWidth
|
|
}
|
|
|
|
// convertToStringer invokes the table's stringer function with optional caching.
|
|
func (t *Table) convertToStringer(input interface{}) ([]string, error) {
|
|
// This function is now only called if t.stringer is non-nil.
|
|
if t.stringer == nil {
|
|
return nil, errors.New("internal error: convertToStringer called with nil t.stringer")
|
|
}
|
|
|
|
t.logger.Debugf("convertToString attempt %v using %v", input, t.stringer)
|
|
|
|
inputType := reflect.TypeOf(input)
|
|
stringerFuncVal := reflect.ValueOf(t.stringer)
|
|
stringerFuncType := stringerFuncVal.Type()
|
|
|
|
// Cache lookup (simplified, actual cache logic can be more complex)
|
|
if t.stringerCacheEnabled {
|
|
t.stringerCacheMu.RLock()
|
|
cachedFunc, ok := t.stringerCache[inputType]
|
|
t.stringerCacheMu.RUnlock()
|
|
if ok {
|
|
// Add proper type checking for cachedFunc against input here if necessary
|
|
t.logger.Debugf("convertToStringer: Cache hit for type %v", inputType)
|
|
results := cachedFunc.Call([]reflect.Value{reflect.ValueOf(input)})
|
|
if len(results) == 1 && results[0].Type() == reflect.TypeOf([]string{}) {
|
|
return results[0].Interface().([]string), nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Robust type checking for the stringer function
|
|
validSignature := stringerFuncVal.Kind() == reflect.Func &&
|
|
stringerFuncType.NumIn() == 1 &&
|
|
stringerFuncType.NumOut() == 1 &&
|
|
stringerFuncType.Out(0) == reflect.TypeOf([]string{})
|
|
|
|
if !validSignature {
|
|
return nil, errors.Newf("table stringer (type %T) does not have signature func(SomeType) []string", t.stringer)
|
|
}
|
|
|
|
// Check if input is assignable to stringer's parameter type
|
|
paramType := stringerFuncType.In(0)
|
|
assignable := false
|
|
if inputType != nil { // input is not untyped nil
|
|
if inputType.AssignableTo(paramType) {
|
|
assignable = true
|
|
} else if paramType.Kind() == reflect.Interface && inputType.Implements(paramType) {
|
|
assignable = true
|
|
} else if paramType.Kind() == reflect.Interface && paramType.NumMethod() == 0 { // stringer expects interface{}
|
|
assignable = true
|
|
}
|
|
} else if paramType.Kind() == reflect.Interface || (paramType.Kind() == reflect.Ptr && paramType.Elem().Kind() != reflect.Interface) {
|
|
// If input is nil, it can be assigned if stringer expects an interface or a pointer type
|
|
// (but not a pointer to an interface, which is rare for stringers).
|
|
// A nil value for a concrete type parameter would cause a panic on Call.
|
|
// So, if paramType is not an interface/pointer, and input is nil, it's an issue.
|
|
// This needs careful handling. For now, assume assignable if interface/pointer.
|
|
assignable = true
|
|
}
|
|
|
|
if !assignable {
|
|
return nil, errors.Newf("input type %T cannot be passed to table stringer expecting %s", input, paramType)
|
|
}
|
|
|
|
var callArgs []reflect.Value
|
|
if input == nil {
|
|
// If input is nil, we must pass a zero value of the stringer's parameter type
|
|
// if that type is a pointer or interface.
|
|
// Passing reflect.ValueOf(nil) directly will cause issues if paramType is concrete.
|
|
callArgs = []reflect.Value{reflect.Zero(paramType)}
|
|
} else {
|
|
callArgs = []reflect.Value{reflect.ValueOf(input)}
|
|
}
|
|
|
|
resultValues := stringerFuncVal.Call(callArgs)
|
|
|
|
if t.stringerCacheEnabled && inputType != nil { // Only cache if inputType is valid
|
|
t.stringerCacheMu.Lock()
|
|
t.stringerCache[inputType] = stringerFuncVal
|
|
t.stringerCacheMu.Unlock()
|
|
}
|
|
|
|
return resultValues[0].Interface().([]string), nil
|
|
}
|
|
|
|
// convertToString converts a value to its string representation.
|
|
func (t *Table) convertToString(value interface{}) string {
|
|
if value == nil {
|
|
return ""
|
|
}
|
|
switch v := value.(type) {
|
|
case tw.Formatter:
|
|
return v.Format()
|
|
case io.Reader:
|
|
const maxReadSize = 512
|
|
var buf strings.Builder
|
|
_, err := io.CopyN(&buf, v, maxReadSize)
|
|
if err != nil && err != io.EOF {
|
|
return fmt.Sprintf("[reader error: %v]", err) // Keep fmt.Sprintf for rare error case
|
|
}
|
|
if buf.Len() == maxReadSize {
|
|
buf.WriteString(tw.CharEllipsis)
|
|
}
|
|
return buf.String()
|
|
case sql.NullString:
|
|
if v.Valid {
|
|
return v.String
|
|
}
|
|
return ""
|
|
case sql.NullInt64:
|
|
if v.Valid {
|
|
return strconv.FormatInt(v.Int64, 10)
|
|
}
|
|
return ""
|
|
case sql.NullFloat64:
|
|
if v.Valid {
|
|
return strconv.FormatFloat(v.Float64, 'f', -1, 64)
|
|
}
|
|
return ""
|
|
case sql.NullBool:
|
|
if v.Valid {
|
|
return strconv.FormatBool(v.Bool)
|
|
}
|
|
return ""
|
|
case sql.NullTime:
|
|
if v.Valid {
|
|
return v.Time.String()
|
|
}
|
|
return ""
|
|
case []byte:
|
|
return string(v)
|
|
case error:
|
|
return v.Error()
|
|
case fmt.Stringer:
|
|
return v.String()
|
|
case string:
|
|
return v
|
|
case int:
|
|
return strconv.FormatInt(int64(v), 10)
|
|
case int8:
|
|
return strconv.FormatInt(int64(v), 10)
|
|
case int16:
|
|
return strconv.FormatInt(int64(v), 10)
|
|
case int32:
|
|
return strconv.FormatInt(int64(v), 10)
|
|
case int64:
|
|
return strconv.FormatInt(v, 10)
|
|
case uint:
|
|
return strconv.FormatUint(uint64(v), 10)
|
|
case uint8:
|
|
return strconv.FormatUint(uint64(v), 10)
|
|
case uint16:
|
|
return strconv.FormatUint(uint64(v), 10)
|
|
case uint32:
|
|
return strconv.FormatUint(uint64(v), 10)
|
|
case uint64:
|
|
return strconv.FormatUint(v, 10)
|
|
case float32:
|
|
return strconv.FormatFloat(float64(v), 'f', -1, 32)
|
|
case float64:
|
|
return strconv.FormatFloat(v, 'f', -1, 64)
|
|
case bool:
|
|
return strconv.FormatBool(v)
|
|
default:
|
|
t.logger.Debugf("convertToString: Falling back to fmt.Sprintf for type %T", value)
|
|
return fmt.Sprintf("%v", value) // Fallback for rare types
|
|
}
|
|
}
|
|
|
|
// convertItemToCells is responsible for converting a single input item (which could be
|
|
// a struct, a basic type, or an item implementing Stringer/Formatter) into a slice
|
|
// of strings, where each string represents a cell for the table row.
|
|
// zoo.go
|
|
|
|
// convertItemToCells is responsible for converting a single input item into a slice of strings.
|
|
// It now uses the unified struct parser for structs.
|
|
func (t *Table) convertItemToCells(item interface{}) ([]string, error) {
|
|
t.logger.Debugf("convertItemToCells: Converting item of type %T", item)
|
|
|
|
// 1. User-defined table-wide stringer (t.stringer) takes highest precedence.
|
|
if t.stringer != nil {
|
|
res, err := t.convertToStringer(item)
|
|
if err == nil {
|
|
t.logger.Debugf("convertItemToCells: Used custom table stringer for type %T. Produced %d cells: %v", item, len(res), res)
|
|
return res, nil
|
|
}
|
|
t.logger.Warnf("convertItemToCells: Custom table stringer was set but incompatible for type %T: %v. Will attempt other methods.", item, err)
|
|
}
|
|
|
|
// 2. Handle untyped nil directly.
|
|
if item == nil {
|
|
t.logger.Debugf("convertItemToCells: Item is untyped nil. Returning single empty cell.")
|
|
return []string{""}, nil
|
|
}
|
|
|
|
// 3. Use the new unified struct parser. It handles pointers and embedding.
|
|
// We only care about the values it returns.
|
|
_, values := t.extractFieldsAndValuesFromStruct(item)
|
|
if values != nil {
|
|
t.logger.Debugf("convertItemToCells: Structs %T reflected into %d cells: %v", item, len(values), values)
|
|
return values, nil
|
|
}
|
|
|
|
// 4. Fallback for any other single item (e.g., basic types, or types that implement Stringer/Formatter).
|
|
// This code path is now for non-struct types.
|
|
if formatter, ok := item.(tw.Formatter); ok {
|
|
t.logger.Debugf("convertItemToCells: Item (non-struct, type %T) is tw.Formatter. Using Format().", item)
|
|
return []string{formatter.Format()}, nil
|
|
}
|
|
if stringer, ok := item.(fmt.Stringer); ok {
|
|
t.logger.Debugf("convertItemToCells: Item (non-struct, type %T) is fmt.Stringer. Using String().", item)
|
|
return []string{stringer.String()}, nil
|
|
}
|
|
|
|
t.logger.Debugf("convertItemToCells: Item (type %T) is a basic type. Treating as single cell via convertToString.", item)
|
|
return []string{t.convertToString(item)}, nil
|
|
}
|
|
|
|
// convertCellsToStrings converts a row to its raw string representation using specified cell config for filters.
|
|
// 'rowInput' can be []string, []any, or a custom type if t.stringer is set.
|
|
func (t *Table) convertCellsToStrings(rowInput interface{}, cellCfg tw.CellConfig) ([]string, error) {
|
|
t.logger.Debugf("convertCellsToStrings: Converting row: %v (type: %T)", rowInput, rowInput)
|
|
|
|
var cells []string
|
|
var err error
|
|
|
|
switch v := rowInput.(type) {
|
|
//Directly supported slice types
|
|
case []string:
|
|
cells = v
|
|
case []interface{}: // Catches variadic simple types grouped by Append
|
|
cells = make([]string, len(v))
|
|
for i, val := range v {
|
|
cells[i] = t.convertToString(val)
|
|
}
|
|
case []int:
|
|
cells = make([]string, len(v))
|
|
for i, val := range v {
|
|
cells[i] = strconv.Itoa(val)
|
|
}
|
|
case []int8:
|
|
cells = make([]string, len(v))
|
|
for i, val := range v {
|
|
cells[i] = strconv.FormatInt(int64(val), 10)
|
|
}
|
|
case []int16:
|
|
cells = make([]string, len(v))
|
|
for i, val := range v {
|
|
cells[i] = strconv.FormatInt(int64(val), 10)
|
|
}
|
|
case []int32: // Also rune
|
|
cells = make([]string, len(v))
|
|
for i, val := range v {
|
|
cells[i] = t.convertToString(val)
|
|
} // Use convertToString for potential rune
|
|
case []int64:
|
|
cells = make([]string, len(v))
|
|
for i, val := range v {
|
|
cells[i] = strconv.FormatInt(val, 10)
|
|
}
|
|
case []uint:
|
|
cells = make([]string, len(v))
|
|
for i, val := range v {
|
|
cells[i] = strconv.FormatUint(uint64(val), 10)
|
|
}
|
|
case []uint8: // Also byte
|
|
cells = make([]string, len(v))
|
|
// If it's truly []byte, convertToString will handle it as a string.
|
|
// If it's a slice of small numbers, convertToString will handle them individually.
|
|
for i, val := range v {
|
|
cells[i] = t.convertToString(val)
|
|
}
|
|
case []uint16:
|
|
cells = make([]string, len(v))
|
|
for i, val := range v {
|
|
cells[i] = strconv.FormatUint(uint64(val), 10)
|
|
}
|
|
case []uint32:
|
|
cells = make([]string, len(v))
|
|
for i, val := range v {
|
|
cells[i] = strconv.FormatUint(uint64(val), 10)
|
|
}
|
|
case []uint64:
|
|
cells = make([]string, len(v))
|
|
for i, val := range v {
|
|
cells[i] = strconv.FormatUint(val, 10)
|
|
}
|
|
case []float32:
|
|
cells = make([]string, len(v))
|
|
for i, val := range v {
|
|
cells[i] = strconv.FormatFloat(float64(val), 'f', -1, 32)
|
|
}
|
|
case []float64:
|
|
cells = make([]string, len(v))
|
|
for i, val := range v {
|
|
cells[i] = strconv.FormatFloat(val, 'f', -1, 64)
|
|
}
|
|
case []bool:
|
|
cells = make([]string, len(v))
|
|
for i, val := range v {
|
|
cells[i] = strconv.FormatBool(val)
|
|
}
|
|
case []tw.Formatter:
|
|
cells = make([]string, len(v))
|
|
for i, val := range v {
|
|
cells[i] = val.Format()
|
|
}
|
|
case []fmt.Stringer:
|
|
cells = make([]string, len(v))
|
|
for i, val := range v {
|
|
cells[i] = val.String()
|
|
}
|
|
|
|
//Cases for single items that are NOT slices
|
|
// These are now dispatched to convertItemToCells by the default case.
|
|
// Keeping direct tw.Formatter and fmt.Stringer here could be a micro-optimization
|
|
// if `rowInput` is *exactly* that type (not a struct implementing it),
|
|
// but for clarity, `convertItemToCells` can handle these too.
|
|
// For this iteration, to match the described flow:
|
|
case tw.Formatter: // This handles a single Formatter item
|
|
t.logger.Debugf("convertCellsToStrings: Input is a single tw.Formatter. Using Format().")
|
|
cells = []string{v.Format()}
|
|
case fmt.Stringer: // This handles a single Stringer item
|
|
t.logger.Debugf("convertCellsToStrings: Input is a single fmt.Stringer. Using String().")
|
|
cells = []string{v.String()}
|
|
|
|
default:
|
|
// If rowInput is not one of the recognized slice types above,
|
|
// or not a single Formatter/Stringer that was directly matched,
|
|
// it's treated as a single item that needs to be converted into potentially multiple cells.
|
|
// This is where structs (for field expansion) or other single values (for a single cell) are handled.
|
|
t.logger.Debugf("convertCellsToStrings: Default case for type %T. Dispatching to convertItemToCells.", rowInput)
|
|
cells, err = t.convertItemToCells(rowInput)
|
|
if err != nil {
|
|
t.logger.Errorf("convertCellsToStrings: Error from convertItemToCells for type %T: %v", rowInput, err)
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Apply filters (common logic for all successful conversions)
|
|
if err == nil && cells != nil {
|
|
if cellCfg.Filter.Global != nil {
|
|
t.logger.Debugf("convertCellsToStrings: Applying global filter to cells: %v", cells)
|
|
cells = cellCfg.Filter.Global(cells)
|
|
}
|
|
if len(cellCfg.Filter.PerColumn) > 0 {
|
|
t.logger.Debugf("convertCellsToStrings: Applying per-column filters to %d cells", len(cells))
|
|
for i := 0; i < len(cellCfg.Filter.PerColumn); i++ {
|
|
if i < len(cells) && cellCfg.Filter.PerColumn[i] != nil {
|
|
originalCell := cells[i]
|
|
cells[i] = cellCfg.Filter.PerColumn[i](cells[i])
|
|
if cells[i] != originalCell {
|
|
t.logger.Debugf(" convertCellsToStrings: Col %d filter applied: '%s' -> '%s'", i, originalCell, cells[i])
|
|
}
|
|
} else if i >= len(cells) && cellCfg.Filter.PerColumn[i] != nil {
|
|
t.logger.Warnf(" convertCellsToStrings: Per-column filter defined for col %d, but item only produced %d cells. Filter for this column skipped.", i, len(cells))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
t.logger.Debugf("convertCellsToStrings: Returning with error: %v", err)
|
|
return nil, err
|
|
}
|
|
t.logger.Debugf("convertCellsToStrings: Conversion and filtering completed, raw cells: %v", cells)
|
|
return cells, nil
|
|
}
|
|
|
|
// determineLocation determines the boundary location for a line.
|
|
// Parameters include lineIdx, totalLines, topPad, and bottomPad.
|
|
// Returns a tw.Location indicating First, Middle, or End.
|
|
func (t *Table) determineLocation(lineIdx, totalLines int, topPad, bottomPad string) tw.Location {
|
|
if lineIdx == 0 && topPad == tw.Empty {
|
|
return tw.LocationFirst
|
|
}
|
|
if lineIdx == totalLines-1 && bottomPad == tw.Empty {
|
|
return tw.LocationEnd
|
|
}
|
|
return tw.LocationMiddle
|
|
}
|
|
|
|
// ensureStreamWidthsCalculated ensures that stream widths and column count are initialized for streaming mode.
|
|
// It uses sampleData and sectionConfig to calculate widths if not already set.
|
|
// Returns an error if the column count cannot be determined.
|
|
func (t *Table) ensureStreamWidthsCalculated(sampleData []string, sectionConfig tw.CellConfig) error {
|
|
if t.streamWidths != nil && t.streamWidths.Len() > 0 {
|
|
t.logger.Debugf("Stream widths already set: %v", t.streamWidths)
|
|
return nil
|
|
}
|
|
t.streamCalculateWidths(sampleData, sectionConfig)
|
|
if t.streamNumCols == 0 {
|
|
t.logger.Warn("Failed to determine column count from sample data")
|
|
return errors.New("failed to determine column count for streaming")
|
|
}
|
|
for i := 0; i < t.streamNumCols; i++ {
|
|
if _, ok := t.streamWidths.OK(i); !ok {
|
|
t.streamWidths.Set(i, 0)
|
|
}
|
|
}
|
|
t.logger.Debugf("Initialized stream widths: %v", t.streamWidths)
|
|
return nil
|
|
}
|
|
|
|
// getColMaxWidths retrieves maximum column widths for a section.
|
|
// Parameter position specifies the section (Header, Row, Footer).
|
|
// Returns a map of column indices to maximum widths.
|
|
func (t *Table) getColMaxWidths(position tw.Position) tw.CellWidth {
|
|
switch position {
|
|
case tw.Header:
|
|
return t.config.Header.ColMaxWidths
|
|
case tw.Row:
|
|
return t.config.Row.ColMaxWidths
|
|
case tw.Footer:
|
|
return t.config.Footer.ColMaxWidths
|
|
default:
|
|
return tw.CellWidth{}
|
|
}
|
|
}
|
|
|
|
// getEmptyColumnInfo identifies empty columns in row data.
|
|
// Parameter numOriginalCols specifies the total column count.
|
|
// Returns a boolean slice (true for empty) and visible column count.
|
|
func (t *Table) getEmptyColumnInfo(numOriginalCols int) (isEmpty []bool, visibleColCount int) {
|
|
isEmpty = make([]bool, numOriginalCols)
|
|
for i := range isEmpty {
|
|
isEmpty[i] = true
|
|
}
|
|
|
|
if t.config.Behavior.AutoHide.Disabled() {
|
|
t.logger.Debugf("getEmptyColumnInfo: AutoHide disabled, marking all %d columns as visible.", numOriginalCols)
|
|
for i := range isEmpty {
|
|
isEmpty[i] = false
|
|
}
|
|
visibleColCount = numOriginalCols
|
|
return isEmpty, visibleColCount
|
|
}
|
|
|
|
t.logger.Debugf("getEmptyColumnInfo: Checking %d rows for %d columns...", len(t.rows), numOriginalCols)
|
|
|
|
for rowIdx, logicalRow := range t.rows {
|
|
for lineIdx, visualLine := range logicalRow {
|
|
for colIdx, cellContent := range visualLine {
|
|
if colIdx >= numOriginalCols {
|
|
continue
|
|
}
|
|
if !isEmpty[colIdx] {
|
|
continue
|
|
}
|
|
|
|
cellContent = t.Trimmer(cellContent)
|
|
|
|
if cellContent != "" {
|
|
isEmpty[colIdx] = false
|
|
t.logger.Debugf("getEmptyColumnInfo: Found content in row %d, line %d, col %d ('%s'). Marked as not empty.", rowIdx, lineIdx, colIdx, cellContent)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
visibleColCount = 0
|
|
for _, empty := range isEmpty {
|
|
if !empty {
|
|
visibleColCount++
|
|
}
|
|
}
|
|
|
|
t.logger.Debugf("getEmptyColumnInfo: Detection complete. isEmpty: %v, visibleColCount: %d", isEmpty, visibleColCount)
|
|
return isEmpty, visibleColCount
|
|
}
|
|
|
|
// getNumColsToUse determines the number of columns to use for rendering, based on streaming or batch mode.
|
|
// Returns the number of columns (streamNumCols for streaming, maxColumns for batch).
|
|
func (t *Table) getNumColsToUse() int {
|
|
if t.config.Stream.Enable && t.hasPrinted {
|
|
t.logger.Debugf("getNumColsToUse: Using streamNumCols: %d", t.streamNumCols)
|
|
return t.streamNumCols
|
|
}
|
|
|
|
// For batch mode:
|
|
if t.isBatchRenderNumColsSet {
|
|
// If the flag is set, batchRenderNumCols holds the authoritative count
|
|
// for the current Render() pass, even if that count is 0.
|
|
t.logger.Debugf("getNumColsToUse (batch): Using cached t.batchRenderNumCols: %d (because isBatchRenderNumColsSet is true)", t.batchRenderNumCols)
|
|
return t.batchRenderNumCols
|
|
}
|
|
|
|
// Fallback: If not streaming and cache flag is not set (e.g., called outside a Render pass)
|
|
num := t.maxColumns()
|
|
t.logger.Debugf("getNumColsToUse (batch): Cache not active, calculated via t.maxColumns(): %d", num)
|
|
return num
|
|
}
|
|
|
|
// prepareTableSection prepares either headers or footers for the table
|
|
func (t *Table) prepareTableSection(elements []any, config tw.CellConfig, sectionName string) [][]string {
|
|
actualCellsToProcess := t.processVariadic(elements)
|
|
t.logger.Debugf("%s(): Effective cells to process: %v", sectionName, actualCellsToProcess)
|
|
|
|
stringsResult, err := t.convertCellsToStrings(actualCellsToProcess, config)
|
|
if err != nil {
|
|
t.logger.Errorf("%s(): Failed to convert elements to strings: %v", sectionName, err)
|
|
stringsResult = []string{}
|
|
}
|
|
|
|
prepared := t.prepareContent(stringsResult, config)
|
|
numColsBatch := t.maxColumns()
|
|
|
|
if len(prepared) > 0 {
|
|
for i := range prepared {
|
|
if len(prepared[i]) < numColsBatch {
|
|
t.logger.Debugf("Padding %s line %d from %d to %d columns", sectionName, i, len(prepared[i]), numColsBatch)
|
|
paddedLine := make([]string, numColsBatch)
|
|
copy(paddedLine, prepared[i])
|
|
for j := len(prepared[i]); j < numColsBatch; j++ {
|
|
paddedLine[j] = tw.Empty
|
|
}
|
|
prepared[i] = paddedLine
|
|
} else if len(prepared[i]) > numColsBatch {
|
|
t.logger.Debugf("Truncating %s line %d from %d to %d columns", sectionName, i, len(prepared[i]), numColsBatch)
|
|
prepared[i] = prepared[i][:numColsBatch]
|
|
}
|
|
}
|
|
}
|
|
|
|
return prepared
|
|
}
|
|
|
|
// processVariadic handles the common logic for processing variadic arguments
|
|
// that could be either individual elements or a slice of elements
|
|
func (t *Table) processVariadic(elements []any) []any {
|
|
if len(elements) == 1 {
|
|
switch v := elements[0].(type) {
|
|
case []string:
|
|
t.logger.Debugf("Detected single []string argument. Unpacking it (fast path).")
|
|
out := make([]any, len(v))
|
|
for i := range v {
|
|
out[i] = v[i]
|
|
}
|
|
return out
|
|
|
|
case []interface{}:
|
|
t.logger.Debugf("Detected single []interface{} argument. Unpacking it (fast path).")
|
|
out := make([]any, len(v))
|
|
copy(out, v)
|
|
return out
|
|
}
|
|
}
|
|
|
|
t.logger.Debugf("Input has multiple elements or single non-slice. Using variadic elements as-is.")
|
|
return elements
|
|
}
|
|
|
|
// toStringLines converts raw cells to formatted lines for table output
|
|
func (t *Table) toStringLines(row interface{}, config tw.CellConfig) ([][]string, error) {
|
|
cells, err := t.convertCellsToStrings(row, config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return t.prepareContent(cells, config), nil
|
|
}
|
|
|
|
// updateWidths updates the width map based on cell content and padding.
|
|
// Parameters include row content, widths map, and padding configuration.
|
|
// No return value.
|
|
func (t *Table) updateWidths(row []string, widths tw.Mapper[int, int], padding tw.CellPadding) {
|
|
t.logger.Debugf("Updating widths for row: %v", row)
|
|
for i, cell := range row {
|
|
colPad := padding.Global
|
|
|
|
if i < len(padding.PerColumn) && padding.PerColumn[i].Paddable() {
|
|
colPad = padding.PerColumn[i]
|
|
t.logger.Debugf(" Col %d: Using per-column padding: L:'%s' R:'%s'", i, colPad.Left, colPad.Right)
|
|
} else {
|
|
t.logger.Debugf(" Col %d: Using global padding: L:'%s' R:'%s'", i, padding.Global.Left, padding.Global.Right)
|
|
}
|
|
|
|
padLeftWidth := twwidth.Width(colPad.Left)
|
|
padRightWidth := twwidth.Width(colPad.Right)
|
|
|
|
// Split cell into lines and find maximum content width
|
|
lines := strings.Split(cell, tw.NewLine)
|
|
contentWidth := 0
|
|
for _, line := range lines {
|
|
lineWidth := twwidth.Width(line)
|
|
if t.config.Behavior.TrimSpace.Enabled() {
|
|
lineWidth = twwidth.Width(t.Trimmer(line))
|
|
}
|
|
if lineWidth > contentWidth {
|
|
contentWidth = lineWidth
|
|
}
|
|
}
|
|
|
|
totalWidth := contentWidth + padLeftWidth + padRightWidth
|
|
minRequiredPaddingWidth := padLeftWidth + padRightWidth
|
|
|
|
if contentWidth == 0 && totalWidth < minRequiredPaddingWidth {
|
|
t.logger.Debugf(" Col %d: Empty content, ensuring width >= padding width (%d). Setting totalWidth to %d.", i, minRequiredPaddingWidth, minRequiredPaddingWidth)
|
|
totalWidth = minRequiredPaddingWidth
|
|
}
|
|
|
|
if totalWidth < 1 {
|
|
t.logger.Debugf(" Col %d: Calculated totalWidth is zero, setting minimum width to 1.", i)
|
|
totalWidth = 1
|
|
}
|
|
|
|
currentMax, _ := widths.OK(i)
|
|
if totalWidth > currentMax {
|
|
widths.Set(i, totalWidth)
|
|
t.logger.Debugf(" Col %d: Updated width from %d to %d (content:%d + padL:%d + padR:%d) for cell '%s'", i, currentMax, totalWidth, contentWidth, padLeftWidth, padRightWidth, cell)
|
|
} else {
|
|
t.logger.Debugf(" Col %d: Width %d not greater than current max %d for cell '%s'", i, totalWidth, currentMax, cell)
|
|
}
|
|
}
|
|
}
|
|
|
|
// extractHeadersFromStruct is now a thin wrapper around the new unified function.
|
|
// It only cares about the header names.
|
|
func (t *Table) extractHeadersFromStruct(sample interface{}) []string {
|
|
headers, _ := t.extractFieldsAndValuesFromStruct(sample)
|
|
return headers
|
|
}
|
|
|
|
// extractFieldsAndValuesFromStruct is the new single source of truth for struct reflection.
|
|
// It recursively processes a struct, handling pointers and embedded structs,
|
|
// and returns two slices: one for header names and one for string-converted values.
|
|
func (t *Table) extractFieldsAndValuesFromStruct(sample interface{}) ([]string, []string) {
|
|
v := reflect.ValueOf(sample)
|
|
if v.Kind() == reflect.Ptr {
|
|
if v.IsNil() {
|
|
return nil, nil
|
|
}
|
|
v = v.Elem()
|
|
}
|
|
|
|
if v.Kind() != reflect.Struct {
|
|
return nil, nil
|
|
}
|
|
|
|
typ := v.Type()
|
|
headers := make([]string, 0, typ.NumField())
|
|
values := make([]string, 0, typ.NumField())
|
|
|
|
for i := 0; i < typ.NumField(); i++ {
|
|
field := typ.Field(i)
|
|
fieldValue := v.Field(i)
|
|
|
|
// Skip unexported fields
|
|
if field.PkgPath != "" {
|
|
continue
|
|
}
|
|
|
|
// Handle embedded structs recursively
|
|
if field.Anonymous {
|
|
h, val := t.extractFieldsAndValuesFromStruct(fieldValue.Interface())
|
|
if h != nil {
|
|
headers = append(headers, h...)
|
|
values = append(values, val...)
|
|
}
|
|
continue
|
|
}
|
|
|
|
var tagName string
|
|
skipField := false
|
|
|
|
// Loop through the priority list of configured tags (e.g., ["json", "db"])
|
|
for _, tagKey := range t.config.Behavior.Structs.Tags {
|
|
tagValue := field.Tag.Get(tagKey)
|
|
|
|
// If a tag is found...
|
|
if tagValue != "" {
|
|
// If the tag is "-", this field should be skipped entirely.
|
|
if tagValue == "-" {
|
|
skipField = true
|
|
break // Stop processing tags for this field.
|
|
}
|
|
// Otherwise, we've found our highest-priority tag. Store it and stop.
|
|
tagName = tagValue
|
|
break // Stop processing tags for this field.
|
|
}
|
|
}
|
|
|
|
// If the field was marked for skipping, continue to the next field.
|
|
if skipField {
|
|
continue
|
|
}
|
|
|
|
// Determine header name from the tag or fallback to the field name
|
|
headerName := field.Name
|
|
if tagName != "" {
|
|
headerName = strings.Split(tagName, ",")[0]
|
|
}
|
|
headers = append(headers, tw.Title(headerName))
|
|
|
|
// Determine value, respecting omitempty from the found tag
|
|
value := ""
|
|
if !strings.Contains(tagName, ",omitempty") || !fieldValue.IsZero() {
|
|
value = t.convertToString(fieldValue.Interface())
|
|
}
|
|
values = append(values, value)
|
|
}
|
|
|
|
return headers, values
|
|
}
|