mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-05 11:51:16 -06:00
Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 1.0.9 to 1.1.0. - [Commits](https://github.com/olekukonko/tablewriter/compare/v1.0.9...v1.1.0) --- updated-dependencies: - dependency-name: github.com/olekukonko/tablewriter dependency-version: 1.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
2273 lines
83 KiB
Go
2273 lines
83 KiB
Go
package tablewriter
|
||
|
||
import (
|
||
"bytes"
|
||
"io"
|
||
"math"
|
||
"os"
|
||
"reflect"
|
||
"runtime"
|
||
"strings"
|
||
"sync"
|
||
|
||
"github.com/olekukonko/errors"
|
||
"github.com/olekukonko/ll"
|
||
"github.com/olekukonko/ll/lh"
|
||
"github.com/olekukonko/tablewriter/pkg/twwarp"
|
||
"github.com/olekukonko/tablewriter/pkg/twwidth"
|
||
"github.com/olekukonko/tablewriter/renderer"
|
||
"github.com/olekukonko/tablewriter/tw"
|
||
)
|
||
|
||
// Table represents a table instance with content and rendering capabilities.
|
||
type Table struct {
|
||
writer io.Writer // Destination for table output
|
||
counters []tw.Counter // Counters for indices
|
||
rows [][][]string // Row data, supporting multi-line cells
|
||
headers [][]string // Header content
|
||
footers [][]string // Footer content
|
||
headerWidths tw.Mapper[int, int] // Computed widths for header columns
|
||
rowWidths tw.Mapper[int, int] // Computed widths for row columns
|
||
footerWidths tw.Mapper[int, int] // Computed widths for footer columns
|
||
renderer tw.Renderer // Engine for rendering the table
|
||
config Config // Table configuration settings
|
||
stringer any // Function to convert rows to strings
|
||
newLine string // Newline character (e.g., "\n")
|
||
hasPrinted bool // Indicates if the table has been rendered
|
||
logger *ll.Logger // Debug trace log
|
||
trace *bytes.Buffer // Debug trace log
|
||
|
||
// Caption fields
|
||
caption tw.Caption
|
||
|
||
// streaming
|
||
streamWidths tw.Mapper[int, int] // Fixed column widths for streaming mode, calculated once
|
||
streamFooterLines [][]string // Processed footer lines for streaming, stored until Close().
|
||
headerRendered bool // Tracks if header has been rendered in streaming mode
|
||
firstRowRendered bool // Tracks if the first data row has been rendered in streaming mode
|
||
lastRenderedLineContent []string // Content of the very last line rendered (for Previous context in streaming)
|
||
lastRenderedMergeState tw.Mapper[int, tw.MergeState] // Merge state of the very last line rendered (for Previous context in streaming)
|
||
lastRenderedPosition tw.Position // Position (Header/Row/Footer/Separator) of the last line rendered (for Previous context in streaming)
|
||
streamNumCols int // The derived number of columns in streaming mode
|
||
streamRowCounter int // Counter for rows rendered in streaming mode (0-indexed logical rows)
|
||
|
||
// cache
|
||
stringerCache map[reflect.Type]reflect.Value // Cache for stringer reflection
|
||
stringerCacheMu sync.RWMutex // Mutex for thread-safe cache access
|
||
stringerCacheEnabled bool // Flag to enable/disable caching
|
||
|
||
batchRenderNumCols int
|
||
isBatchRenderNumColsSet bool
|
||
}
|
||
|
||
// renderContext holds the core state for rendering the table.
|
||
type renderContext struct {
|
||
table *Table // Reference to the table instance
|
||
renderer tw.Renderer // Renderer instance
|
||
cfg tw.Rendition // Renderer configuration
|
||
numCols int // Total number of columns
|
||
headerLines [][]string // Processed header lines
|
||
rowLines [][][]string // Processed row lines
|
||
footerLines [][]string // Processed footer lines
|
||
widths tw.Mapper[tw.Position, tw.Mapper[int, int]] // Widths per section
|
||
footerPrepared bool // Tracks if footer is prepared
|
||
emptyColumns []bool // Tracks which original columns are empty (true if empty)
|
||
visibleColCount int // Count of columns that are NOT empty
|
||
logger *ll.Logger // Debug trace log
|
||
}
|
||
|
||
// mergeContext holds state related to cell merging.
|
||
type mergeContext struct {
|
||
headerMerges map[int]tw.MergeState // Merge states for header columns
|
||
rowMerges []map[int]tw.MergeState // Merge states for each row
|
||
footerMerges map[int]tw.MergeState // Merge states for footer columns
|
||
horzMerges map[tw.Position]map[int]bool // Tracks horizontal merges (unused)
|
||
}
|
||
|
||
// helperContext holds additional data for rendering helpers.
|
||
type helperContext struct {
|
||
position tw.Position // Section being processed (Header, Row, Footer)
|
||
rowIdx int // Row index within section
|
||
lineIdx int // Line index within row
|
||
location tw.Location // Boundary location (First, Middle, End)
|
||
line []string // Current line content
|
||
}
|
||
|
||
// renderMergeResponse holds cell context data from rendering operations.
|
||
type renderMergeResponse struct {
|
||
cells map[int]tw.CellContext // Current line cells
|
||
prevCells map[int]tw.CellContext // Previous line cells
|
||
nextCells map[int]tw.CellContext // Next line cells
|
||
location tw.Location // Determined Location for this line
|
||
cellsContent []string
|
||
}
|
||
|
||
// NewTable creates a new table instance with specified writer and options.
|
||
// Parameters include writer for output and optional configuration options.
|
||
// Returns a pointer to the initialized Table instance.
|
||
func NewTable(w io.Writer, opts ...Option) *Table {
|
||
t := &Table{
|
||
writer: w,
|
||
headerWidths: tw.NewMapper[int, int](),
|
||
rowWidths: tw.NewMapper[int, int](),
|
||
footerWidths: tw.NewMapper[int, int](),
|
||
renderer: renderer.NewBlueprint(),
|
||
config: defaultConfig(),
|
||
newLine: tw.NewLine,
|
||
trace: &bytes.Buffer{},
|
||
|
||
// Streaming
|
||
streamWidths: tw.NewMapper[int, int](), // Initialize empty mapper for streaming widths
|
||
lastRenderedMergeState: tw.NewMapper[int, tw.MergeState](),
|
||
headerRendered: false,
|
||
firstRowRendered: false,
|
||
lastRenderedPosition: "",
|
||
streamNumCols: 0,
|
||
streamRowCounter: 0,
|
||
|
||
// Cache
|
||
stringerCache: make(map[reflect.Type]reflect.Value),
|
||
stringerCacheEnabled: false, // Disabled by default
|
||
}
|
||
|
||
// set Options
|
||
t.Options(opts...)
|
||
|
||
t.logger.Infof("Table initialized with %d options", len(opts))
|
||
return t
|
||
}
|
||
|
||
// NewWriter creates a new table with default settings for backward compatibility.
|
||
// It logs the creation if debugging is enabled.
|
||
func NewWriter(w io.Writer) *Table {
|
||
t := NewTable(w)
|
||
if t.logger != nil {
|
||
t.logger.Debug("NewWriter created buffered Table")
|
||
}
|
||
return t
|
||
}
|
||
|
||
// Caption sets the table caption (legacy method).
|
||
// Defaults to BottomCenter alignment, wrapping to table width.
|
||
// Use SetCaptionOptions for more control.
|
||
func (t *Table) Caption(caption tw.Caption) *Table { // This is the one we modified
|
||
originalSpot := caption.Spot
|
||
originalAlign := caption.Align
|
||
|
||
if caption.Spot == tw.SpotNone {
|
||
caption.Spot = tw.SpotBottomCenter
|
||
t.logger.Debugf("[Table.Caption] Input Spot was SpotNone, defaulting Spot to SpotBottomCenter (%d)", caption.Spot)
|
||
}
|
||
|
||
if caption.Align == "" || caption.Align == tw.AlignDefault || caption.Align == tw.AlignNone {
|
||
switch caption.Spot {
|
||
case tw.SpotTopLeft, tw.SpotBottomLeft:
|
||
caption.Align = tw.AlignLeft
|
||
case tw.SpotTopRight, tw.SpotBottomRight:
|
||
caption.Align = tw.AlignRight
|
||
default:
|
||
caption.Align = tw.AlignCenter
|
||
}
|
||
t.logger.Debugf("[Table.Caption] Input Align was empty/default, defaulting Align to %s for Spot %d", caption.Align, caption.Spot)
|
||
}
|
||
|
||
t.caption = caption // t.caption on the struct is now updated.
|
||
t.logger.Debugf("Caption method called: Input(Spot:%v, Align:%q), Final(Spot:%v, Align:%q), Text:'%.20s', MaxWidth:%d",
|
||
originalSpot, originalAlign, t.caption.Spot, t.caption.Align, t.caption.Text, t.caption.Width)
|
||
return t
|
||
}
|
||
|
||
// Append adds data to the current row being built for the table.
|
||
// This method always contributes to a single logical row in the table.
|
||
// To add multiple distinct rows, call Append multiple times (once for each row's data)
|
||
// or use the Bulk() method if providing a slice where each element is a row.
|
||
func (t *Table) Append(rows ...interface{}) error {
|
||
t.ensureInitialized()
|
||
|
||
if t.config.Stream.Enable && t.hasPrinted {
|
||
// Streaming logic remains unchanged, as AutoHeader is a batch-mode concept.
|
||
t.logger.Debugf("Append() called in streaming mode with %d items for a single row", len(rows))
|
||
var rowItemForStream interface{}
|
||
if len(rows) == 1 {
|
||
rowItemForStream = rows[0]
|
||
} else {
|
||
rowItemForStream = rows
|
||
}
|
||
if err := t.streamAppendRow(rowItemForStream); err != nil {
|
||
t.logger.Errorf("Error rendering streaming row: %v", err)
|
||
return errors.Newf("failed to stream append row").Wrap(err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Batch Mode Logic
|
||
t.logger.Debugf("Append (Batch) received %d arguments: %v", len(rows), rows)
|
||
|
||
var cellsSource interface{}
|
||
if len(rows) == 1 {
|
||
cellsSource = rows[0]
|
||
} else {
|
||
cellsSource = rows
|
||
}
|
||
// Check if we should attempt to auto-generate headers from this append operation.
|
||
// Conditions: AutoHeader is on, no headers are set yet, and this is the first data row.
|
||
isFirstRow := len(t.rows) == 0
|
||
if t.config.Behavior.Structs.AutoHeader.Enabled() && len(t.headers) == 0 && isFirstRow {
|
||
t.logger.Debug("Append: Triggering AutoHeader for the first row.")
|
||
headers := t.extractHeadersFromStruct(cellsSource)
|
||
if len(headers) > 0 {
|
||
// Set the extracted headers. The Header() method handles the rest.
|
||
t.Header(headers)
|
||
}
|
||
}
|
||
|
||
// The rest of the function proceeds as before, converting the data to string lines.
|
||
lines, err := t.toStringLines(cellsSource, t.config.Row)
|
||
if err != nil {
|
||
t.logger.Errorf("Append (Batch) failed for cellsSource %v: %v", cellsSource, err)
|
||
return err
|
||
}
|
||
t.rows = append(t.rows, lines)
|
||
|
||
t.logger.Debugf("Append (Batch) completed for one row, total rows in table: %d", len(t.rows))
|
||
return nil
|
||
}
|
||
|
||
// Bulk adds multiple rows from a slice to the table.
|
||
// If Behavior.AutoHeader is enabled, no headers set, and rows is a slice of structs,
|
||
// automatically extracts/sets headers from the first struct.
|
||
func (t *Table) Bulk(rows interface{}) error {
|
||
rv := reflect.ValueOf(rows)
|
||
if rv.Kind() != reflect.Slice {
|
||
return errors.Newf("Bulk expects a slice, got %T", rows)
|
||
}
|
||
if rv.Len() == 0 {
|
||
return nil
|
||
}
|
||
|
||
// AutoHeader logic remains here, as it's a "Bulk" operation concept.
|
||
if t.config.Behavior.Structs.AutoHeader.Enabled() && len(t.headers) == 0 {
|
||
first := rv.Index(0).Interface()
|
||
// We can now correctly get headers from pointers or embedded structs
|
||
headers := t.extractHeadersFromStruct(first)
|
||
if len(headers) > 0 {
|
||
t.Header(headers)
|
||
}
|
||
}
|
||
|
||
// The rest of the logic is now just a loop over Append.
|
||
for i := 0; i < rv.Len(); i++ {
|
||
row := rv.Index(i).Interface()
|
||
if err := t.Append(row); err != nil { // Use Append
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Config returns the current table configuration.
|
||
// No parameters are required.
|
||
// Returns the Config struct with current settings.
|
||
func (t *Table) Config() Config {
|
||
return t.config
|
||
}
|
||
|
||
// Configure updates the table's configuration using a provided function.
|
||
// Parameter fn is a function that modifies the Config struct.
|
||
// Returns the Table instance for method chaining.
|
||
func (t *Table) Configure(fn func(cfg *Config)) *Table {
|
||
fn(&t.config) // Let the user modify the config directly
|
||
// Handle any immediate side-effects of config changes, e.g., logger state
|
||
if t.config.Debug {
|
||
t.logger.Enable()
|
||
t.logger.Resume() // in case it was suspended
|
||
} else {
|
||
t.logger.Disable()
|
||
t.logger.Suspend() // suspend totally, especially because of tight loops
|
||
}
|
||
t.logger.Debugf("Configure complete. New t.config: %+v", t.config)
|
||
return t
|
||
}
|
||
|
||
// Debug retrieves the accumulated debug trace logs.
|
||
// No parameters are required.
|
||
// Returns a slice of debug messages including renderer logs.
|
||
func (t *Table) Debug() *bytes.Buffer {
|
||
return t.trace
|
||
}
|
||
|
||
// Header sets the table's header content, padding to match column count.
|
||
// Parameter elements is a slice of strings for header content.
|
||
// No return value.
|
||
// In streaming mode, this processes and renders the header immediately.
|
||
func (t *Table) Header(elements ...any) {
|
||
t.ensureInitialized()
|
||
t.logger.Debugf("Header() method called with raw variadic elements: %v (len %d). Streaming: %v, Started: %v", elements, len(elements), t.config.Stream.Enable, t.hasPrinted)
|
||
|
||
// just forget
|
||
if t.config.Behavior.Header.Hide.Enabled() {
|
||
return
|
||
}
|
||
|
||
// add come common default
|
||
if t.config.Header.Formatting.AutoFormat == tw.Unknown {
|
||
t.config.Header.Formatting.AutoFormat = tw.On
|
||
}
|
||
|
||
if t.config.Stream.Enable && t.hasPrinted {
|
||
// Streaming Path
|
||
actualCellsToProcess := t.processVariadic(elements)
|
||
headersAsStrings, err := t.convertCellsToStrings(actualCellsToProcess, t.config.Header)
|
||
if err != nil {
|
||
t.logger.Errorf("Header(): Failed to convert header elements to strings for streaming: %v", err)
|
||
headersAsStrings = []string{} // Use empty on error
|
||
}
|
||
errStream := t.streamRenderHeader(headersAsStrings) // streamRenderHeader handles padding to streamNumCols internally
|
||
if errStream != nil {
|
||
t.logger.Errorf("Error rendering streaming header: %v", errStream)
|
||
}
|
||
return
|
||
}
|
||
|
||
// Batch Path
|
||
processedElements := t.processVariadic(elements)
|
||
t.logger.Debugf("Header() (Batch): Effective cells to process: %v", processedElements)
|
||
|
||
headersAsStrings, err := t.convertCellsToStrings(processedElements, t.config.Header)
|
||
if err != nil {
|
||
t.logger.Errorf("Header() (Batch): Failed to convert to strings: %v", err)
|
||
t.headers = [][]string{} // Set to empty on error
|
||
return
|
||
}
|
||
|
||
// prepareContent uses t.config.Header for AutoFormat and MaxWidth constraints.
|
||
// It processes based on the number of columns in headersAsStrings.
|
||
preparedHeaderLines := t.prepareContent(headersAsStrings, t.config.Header)
|
||
t.headers = preparedHeaderLines // Store directly. Padding to t.maxColumns() will happen in prepareContexts.
|
||
|
||
t.logger.Debugf("Header set (batch mode), lines stored: %d. First line if exists: %v", len(t.headers), func() []string {
|
||
if len(t.headers) > 0 {
|
||
return t.headers[0]
|
||
} else {
|
||
return nil
|
||
}
|
||
}())
|
||
}
|
||
|
||
// Footer sets the table's footer content, padding to match column count.
|
||
// Parameter footers is a slice of strings for footer content.
|
||
// No return value.
|
||
// Footer sets the table's footer content.
|
||
// Parameter footers is a slice of strings for footer content.
|
||
// In streaming mode, this processes and stores the footer for rendering by Close().
|
||
func (t *Table) Footer(elements ...any) {
|
||
t.ensureInitialized()
|
||
t.logger.Debugf("Footer() method called with raw variadic elements: %v (len %d). Streaming: %v, Started: %v", elements, len(elements), t.config.Stream.Enable, t.hasPrinted)
|
||
|
||
// just forget
|
||
if t.config.Behavior.Footer.Hide.Enabled() {
|
||
return
|
||
}
|
||
|
||
if t.config.Stream.Enable && t.hasPrinted {
|
||
// Streaming Path
|
||
actualCellsToProcess := t.processVariadic(elements)
|
||
footersAsStrings, err := t.convertCellsToStrings(actualCellsToProcess, t.config.Footer)
|
||
if err != nil {
|
||
t.logger.Errorf("Footer(): Failed to convert footer elements to strings for streaming: %v", err)
|
||
footersAsStrings = []string{} // Use empty on error
|
||
}
|
||
errStream := t.streamStoreFooter(footersAsStrings) // streamStoreFooter handles padding to streamNumCols internally
|
||
if errStream != nil {
|
||
t.logger.Errorf("Error processing streaming footer: %v", errStream)
|
||
}
|
||
return
|
||
}
|
||
|
||
// Batch Path
|
||
processedElements := t.processVariadic(elements)
|
||
t.logger.Debugf("Footer() (Batch): Effective cells to process: %v", processedElements)
|
||
|
||
footersAsStrings, err := t.convertCellsToStrings(processedElements, t.config.Footer)
|
||
if err != nil {
|
||
t.logger.Errorf("Footer() (Batch): Failed to convert to strings: %v", err)
|
||
t.footers = [][]string{} // Set to empty on error
|
||
return
|
||
}
|
||
|
||
preparedFooterLines := t.prepareContent(footersAsStrings, t.config.Footer)
|
||
t.footers = preparedFooterLines // Store directly. Padding to t.maxColumns() will happen in prepareContexts.
|
||
|
||
t.logger.Debugf("Footer set (batch mode), lines stored: %d. First line if exists: %v",
|
||
len(t.footers), func() []string {
|
||
if len(t.footers) > 0 {
|
||
return t.footers[0]
|
||
} else {
|
||
return nil
|
||
}
|
||
}())
|
||
}
|
||
|
||
// Options updates the table's Options using a provided function.
|
||
// Parameter opts is a function that modifies the Table struct.
|
||
// Returns the Table instance for method chaining.
|
||
func (t *Table) Options(opts ...Option) *Table {
|
||
// add logger
|
||
if t.logger == nil {
|
||
t.logger = ll.New("table").Handler(lh.NewTextHandler(t.trace))
|
||
}
|
||
|
||
// loop through options
|
||
for _, opt := range opts {
|
||
opt(t)
|
||
}
|
||
|
||
// force debugging mode if set
|
||
// This should be move away form WithDebug
|
||
if t.config.Debug {
|
||
t.logger.Enable()
|
||
t.logger.Resume()
|
||
} else {
|
||
t.logger.Disable()
|
||
t.logger.Suspend()
|
||
}
|
||
|
||
// Get additional system information for debugging
|
||
goVersion := runtime.Version()
|
||
goOS := runtime.GOOS
|
||
goArch := runtime.GOARCH
|
||
numCPU := runtime.NumCPU()
|
||
|
||
t.logger.Infof("Environment: LC_CTYPE=%s, LANG=%s, TERM=%s", os.Getenv("LC_CTYPE"), os.Getenv("LANG"), os.Getenv("TERM"))
|
||
t.logger.Infof("Go Runtime: Version=%s, OS=%s, Arch=%s, CPUs=%d", goVersion, goOS, goArch, numCPU)
|
||
|
||
// send logger to renderer
|
||
// this will overwrite the default logger
|
||
t.renderer.Logger(t.logger)
|
||
return t
|
||
}
|
||
|
||
// Reset clears all data (headers, rows, footers, caption) and rendering state
|
||
// from the table, allowing the Table instance to be reused for a new table
|
||
// with the same configuration and writer.
|
||
// It does NOT reset the configuration itself (set by NewTable options or Configure)
|
||
// or the underlying io.Writer.
|
||
func (t *Table) Reset() {
|
||
t.logger.Debug("Reset() called. Clearing table data and render state.")
|
||
|
||
// Clear data slices
|
||
t.rows = nil // Or t.rows = make([][][]string, 0)
|
||
t.headers = nil // Or t.headers = make([][]string, 0)
|
||
t.footers = nil // Or t.footers = make([][]string, 0)
|
||
|
||
// Reset width mappers (important for recalculating widths for the new table)
|
||
t.headerWidths = tw.NewMapper[int, int]()
|
||
t.rowWidths = tw.NewMapper[int, int]()
|
||
t.footerWidths = tw.NewMapper[int, int]()
|
||
|
||
// Reset caption
|
||
t.caption = tw.Caption{} // Reset to zero value
|
||
|
||
// Reset rendering state flags
|
||
t.hasPrinted = false // Critical for allowing Render() or stream Start() again
|
||
|
||
// Reset streaming-specific state
|
||
// (Important if the table was used in streaming mode and might be reused in batch or another stream)
|
||
t.streamWidths = tw.NewMapper[int, int]()
|
||
t.streamFooterLines = nil
|
||
t.headerRendered = false
|
||
t.firstRowRendered = false
|
||
t.lastRenderedLineContent = nil
|
||
t.lastRenderedMergeState = tw.NewMapper[int, tw.MergeState]() // Re-initialize
|
||
t.lastRenderedPosition = ""
|
||
t.streamNumCols = 0
|
||
t.streamRowCounter = 0
|
||
|
||
// The stringer and its cache are part of the table's configuration,
|
||
if t.stringerCacheEnabled {
|
||
t.stringerCacheMu.Lock()
|
||
t.stringerCache = make(map[reflect.Type]reflect.Value)
|
||
t.stringerCacheMu.Unlock()
|
||
t.logger.Debug("Reset(): Stringer cache cleared.")
|
||
}
|
||
|
||
// If the renderer has its own state that needs resetting after a table is done,
|
||
// this would be the place to call a renderer.Reset() method if it existed.
|
||
// Most current renderers are stateless per render call or reset in their Start/Close.
|
||
// For instance, HTML and SVG renderers have their own Reset method.
|
||
// It might be good practice to call it if available.
|
||
if r, ok := t.renderer.(interface{ Reset() }); ok {
|
||
t.logger.Debug("Reset(): Calling Reset() on the current renderer.")
|
||
r.Reset()
|
||
}
|
||
|
||
t.logger.Info("Table instance has been reset.")
|
||
}
|
||
|
||
// Render triggers the table rendering process to the configured writer.
|
||
// No parameters are required.
|
||
// Returns an error if rendering fails.
|
||
func (t *Table) Render() error {
|
||
return t.render()
|
||
}
|
||
|
||
// Lines returns the total number of lines rendered.
|
||
// This method is only effective if the WithLineCounter() option was used during
|
||
// table initialization and must be called *after* Render().
|
||
// It actively searches for the default tw.LineCounter among all active counters.
|
||
// It returns -1 if the line counter was not enabled.
|
||
func (t *Table) Lines() int {
|
||
for _, counter := range t.counters {
|
||
if lc, ok := counter.(*tw.LineCounter); ok {
|
||
return lc.Total()
|
||
}
|
||
}
|
||
// use -1 to indicate no line counter is attached
|
||
return -1
|
||
}
|
||
|
||
// Counters returns the slice of all active counter instances.
|
||
// This is useful when multiple counters are enabled.
|
||
// It must be called *after* Render().
|
||
func (t *Table) Counters() []tw.Counter {
|
||
return t.counters
|
||
}
|
||
|
||
// Trimmer trims whitespace from a string based on the Table’s configuration.
|
||
// It conditionally applies strings.TrimSpace to the input string if the TrimSpace behavior
|
||
// is enabled in t.config.Behavior, otherwise returning the string unchanged. This method
|
||
// is used in the logging library to format strings for tabular output, ensuring consistent
|
||
// display in log messages. Thread-safe as it only reads configuration and operates on the
|
||
// input string.
|
||
func (t *Table) Trimmer(str string) string {
|
||
if t.config.Behavior.TrimSpace.Enabled() {
|
||
return strings.TrimSpace(str)
|
||
}
|
||
return str
|
||
}
|
||
|
||
// appendSingle adds a single row to the table's row data.
|
||
// Parameter row is the data to append, converted via stringer if needed.
|
||
// Returns an error if conversion or appending fails.
|
||
func (t *Table) appendSingle(row interface{}) error {
|
||
t.ensureInitialized() // Already here
|
||
|
||
if t.config.Stream.Enable && t.hasPrinted { // If streaming is active
|
||
t.logger.Debugf("appendSingle: Dispatching to streamAppendRow for row: %v", row)
|
||
return t.streamAppendRow(row) // Call the streaming render function
|
||
}
|
||
// Existing batch logic:
|
||
t.logger.Debugf("appendSingle: Processing for batch mode, row: %v", row)
|
||
// toStringLines now uses the new convertCellsToStrings internally, then prepareContent.
|
||
// This is fine for batch.
|
||
lines, err := t.toStringLines(row, t.config.Row)
|
||
if err != nil {
|
||
t.logger.Debugf("Error in toStringLines (batch mode): %v", err)
|
||
return err
|
||
}
|
||
t.rows = append(t.rows, lines) // Add to batch storage
|
||
t.logger.Debugf("Row appended to batch t.rows, total batch rows: %d", len(t.rows))
|
||
return nil
|
||
}
|
||
|
||
// buildAligns constructs a map of column alignments from configuration.
|
||
// Parameter config provides alignment settings for the section.
|
||
// Returns a map of column indices to alignment settings.
|
||
func (t *Table) buildAligns(config tw.CellConfig) map[int]tw.Align {
|
||
// Start with global alignment, preferring deprecated Formatting.Alignment
|
||
effectiveGlobalAlign := config.Formatting.Alignment
|
||
if effectiveGlobalAlign == tw.Empty || effectiveGlobalAlign == tw.Skip {
|
||
effectiveGlobalAlign = config.Alignment.Global
|
||
if config.Formatting.Alignment != tw.Empty && config.Formatting.Alignment != tw.Skip {
|
||
t.logger.Warnf("Using deprecated CellFormatting.Alignment (%s). Migrate to CellConfig.Alignment.Global.", config.Formatting.Alignment)
|
||
}
|
||
}
|
||
|
||
// Use per-column alignments, preferring deprecated ColumnAligns
|
||
effectivePerColumn := config.ColumnAligns
|
||
if len(effectivePerColumn) == 0 && len(config.Alignment.PerColumn) > 0 {
|
||
effectivePerColumn = make([]tw.Align, len(config.Alignment.PerColumn))
|
||
copy(effectivePerColumn, config.Alignment.PerColumn)
|
||
if len(config.ColumnAligns) > 0 {
|
||
t.logger.Warnf("Using deprecated CellConfig.ColumnAligns (%v). Migrate to CellConfig.Alignment.PerColumn.", config.ColumnAligns)
|
||
}
|
||
}
|
||
|
||
// Log input for debugging
|
||
t.logger.Debugf("buildAligns INPUT: deprecated Formatting.Alignment=%s, deprecated ColumnAligns=%v, config.Alignment.Global=%s, config.Alignment.PerColumn=%v",
|
||
config.Formatting.Alignment, config.ColumnAligns, config.Alignment.Global, config.Alignment.PerColumn)
|
||
|
||
numColsToUse := t.getNumColsToUse()
|
||
colAlignsResult := make(map[int]tw.Align)
|
||
for i := 0; i < numColsToUse; i++ {
|
||
currentAlign := effectiveGlobalAlign
|
||
if i < len(effectivePerColumn) && effectivePerColumn[i] != tw.Empty && effectivePerColumn[i] != tw.Skip {
|
||
currentAlign = effectivePerColumn[i]
|
||
}
|
||
// Skip validation here; rely on rendering to handle invalid alignments
|
||
colAlignsResult[i] = currentAlign
|
||
}
|
||
|
||
t.logger.Debugf("Aligns built: %v (length %d)", colAlignsResult, len(colAlignsResult))
|
||
return colAlignsResult
|
||
}
|
||
|
||
// buildPadding constructs a map of column padding settings from configuration.
|
||
// Parameter padding provides padding settings for the section.
|
||
// Returns a map of column indices to padding settings.
|
||
func (t *Table) buildPadding(padding tw.CellPadding) map[int]tw.Padding {
|
||
numColsToUse := t.getNumColsToUse()
|
||
colPadding := make(map[int]tw.Padding)
|
||
for i := 0; i < numColsToUse; i++ {
|
||
if i < len(padding.PerColumn) && padding.PerColumn[i].Paddable() {
|
||
colPadding[i] = padding.PerColumn[i]
|
||
} else {
|
||
colPadding[i] = padding.Global
|
||
}
|
||
}
|
||
t.logger.Debugf("Padding built: %v (length %d)", colPadding, len(colPadding))
|
||
return colPadding
|
||
}
|
||
|
||
// ensureInitialized initializes required fields before use.
|
||
// No parameters are required.
|
||
// No return value.
|
||
func (t *Table) ensureInitialized() {
|
||
if t.headerWidths == nil {
|
||
t.headerWidths = tw.NewMapper[int, int]()
|
||
}
|
||
if t.rowWidths == nil {
|
||
t.rowWidths = tw.NewMapper[int, int]()
|
||
}
|
||
if t.footerWidths == nil {
|
||
t.footerWidths = tw.NewMapper[int, int]()
|
||
}
|
||
if t.renderer == nil {
|
||
t.renderer = renderer.NewBlueprint()
|
||
}
|
||
t.logger.Debug("ensureInitialized called")
|
||
}
|
||
|
||
// finalizeHierarchicalMergeBlock sets Span and End for hierarchical merges.
|
||
// Parameters include ctx, mctx, col, startRow, and endRow.
|
||
// No return value.
|
||
func (t *Table) finalizeHierarchicalMergeBlock(ctx *renderContext, mctx *mergeContext, col, startRow, endRow int) {
|
||
if endRow < startRow {
|
||
ctx.logger.Debugf("Hierarchical merge FINALIZE WARNING: Invalid block col %d, start %d > end %d", col, startRow, endRow)
|
||
return
|
||
}
|
||
if startRow < 0 || endRow < 0 {
|
||
ctx.logger.Debugf("Hierarchical merge FINALIZE WARNING: Negative row indices col %d, start %d, end %d", col, startRow, endRow)
|
||
return
|
||
}
|
||
requiredLen := endRow + 1
|
||
if requiredLen > len(mctx.rowMerges) {
|
||
ctx.logger.Debugf("Hierarchical merge FINALIZE WARNING: rowMerges slice too short (len %d) for endRow %d", len(mctx.rowMerges), endRow)
|
||
return
|
||
}
|
||
if mctx.rowMerges[startRow] == nil {
|
||
mctx.rowMerges[startRow] = make(map[int]tw.MergeState)
|
||
}
|
||
if mctx.rowMerges[endRow] == nil {
|
||
mctx.rowMerges[endRow] = make(map[int]tw.MergeState)
|
||
}
|
||
|
||
finalSpan := (endRow - startRow) + 1
|
||
ctx.logger.Debugf("Finalizing H-merge block: col=%d, startRow=%d, endRow=%d, span=%d", col, startRow, endRow, finalSpan)
|
||
|
||
startState := mctx.rowMerges[startRow][col]
|
||
if startState.Hierarchical.Present && startState.Hierarchical.Start {
|
||
startState.Hierarchical.Span = finalSpan
|
||
startState.Hierarchical.End = finalSpan == 1
|
||
mctx.rowMerges[startRow][col] = startState
|
||
ctx.logger.Debugf(" -> Updated start state: %+v", startState.Hierarchical)
|
||
} else {
|
||
ctx.logger.Debugf("Hierarchical merge FINALIZE WARNING: col %d, startRow %d was not marked as Present/Start? Current state: %+v. Attempting recovery.", col, startRow, startState.Hierarchical)
|
||
startState.Hierarchical.Present = true
|
||
startState.Hierarchical.Start = true
|
||
startState.Hierarchical.Span = finalSpan
|
||
startState.Hierarchical.End = finalSpan == 1
|
||
mctx.rowMerges[startRow][col] = startState
|
||
}
|
||
|
||
if endRow > startRow {
|
||
endState := mctx.rowMerges[endRow][col]
|
||
if endState.Hierarchical.Present && !endState.Hierarchical.Start {
|
||
endState.Hierarchical.End = true
|
||
endState.Hierarchical.Span = finalSpan
|
||
mctx.rowMerges[endRow][col] = endState
|
||
ctx.logger.Debugf(" -> Updated end state: %+v", endState.Hierarchical)
|
||
} else {
|
||
ctx.logger.Debugf("Hierarchical merge FINALIZE WARNING: col %d, endRow %d was not marked as Present/Continuation? Current state: %+v. Attempting recovery.", col, endRow, endState.Hierarchical)
|
||
endState.Hierarchical.Present = true
|
||
endState.Hierarchical.Start = false
|
||
endState.Hierarchical.End = true
|
||
endState.Hierarchical.Span = finalSpan
|
||
mctx.rowMerges[endRow][col] = endState
|
||
}
|
||
} else {
|
||
ctx.logger.Debugf(" -> Span is 1, startRow is also endRow.")
|
||
}
|
||
}
|
||
|
||
// getLevel maps a position to its rendering level.
|
||
// Parameter position specifies the section (Header, Row, Footer).
|
||
// Returns the corresponding tw.Level (Header, Body, Footer).
|
||
func (t *Table) getLevel(position tw.Position) tw.Level {
|
||
switch position {
|
||
case tw.Header:
|
||
return tw.LevelHeader
|
||
case tw.Row:
|
||
return tw.LevelBody
|
||
case tw.Footer:
|
||
return tw.LevelFooter
|
||
default:
|
||
return tw.LevelBody
|
||
}
|
||
}
|
||
|
||
// hasFooterElements checks if the footer has renderable elements.
|
||
// No parameters are required.
|
||
// Returns true if footer has content or padding, false otherwise.
|
||
func (t *Table) hasFooterElements() bool {
|
||
hasContent := len(t.footers) > 0
|
||
hasTopPadding := t.config.Footer.Padding.Global.Top != tw.Empty
|
||
hasBottomPaddingConfig := t.config.Footer.Padding.Global.Bottom != tw.Empty || t.hasPerColumnBottomPadding()
|
||
return hasContent || hasTopPadding || hasBottomPaddingConfig
|
||
}
|
||
|
||
// hasPerColumnBottomPadding checks for per-column bottom padding in footer.
|
||
// No parameters are required.
|
||
// Returns true if any per-column bottom padding is defined.
|
||
func (t *Table) hasPerColumnBottomPadding() bool {
|
||
if t.config.Footer.Padding.PerColumn == nil {
|
||
return false
|
||
}
|
||
for _, pad := range t.config.Footer.Padding.PerColumn {
|
||
if pad.Bottom != tw.Empty {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// Logger retrieves the table's logger instance.
|
||
// No parameters are required.
|
||
// Returns the ll.Logger instance used for debug tracing.
|
||
func (t *Table) Logger() *ll.Logger {
|
||
return t.logger
|
||
}
|
||
|
||
// Renderer retrieves the current renderer instance used by the table.
|
||
// No parameters are required.
|
||
// Returns the tw.Renderer interface instance.
|
||
func (t *Table) Renderer() tw.Renderer {
|
||
t.logger.Debug("Renderer requested")
|
||
return t.renderer
|
||
}
|
||
|
||
// maxColumns calculates the maximum column count across sections.
|
||
// No parameters are required.
|
||
// Returns the highest number of columns found.
|
||
func (t *Table) maxColumns() int {
|
||
m := 0
|
||
if len(t.headers) > 0 && len(t.headers[0]) > m {
|
||
m = len(t.headers[0])
|
||
}
|
||
for _, row := range t.rows {
|
||
if len(row) > 0 && len(row[0]) > m {
|
||
m = len(row[0])
|
||
}
|
||
}
|
||
if len(t.footers) > 0 && len(t.footers[0]) > m {
|
||
m = len(t.footers[0])
|
||
}
|
||
t.logger.Debugf("Max columns: %d", m)
|
||
return m
|
||
}
|
||
|
||
// printTopBottomCaption prints the table's caption at the specified top or bottom position.
|
||
// It wraps the caption text to fit the table width or a user-defined width, aligns it according
|
||
// to the specified alignment, and writes it to the provided writer. If the caption text is empty
|
||
// or the spot is invalid, it logs the issue and returns without printing. The function handles
|
||
// wrapping errors by falling back to splitting on newlines or using the original text.
|
||
func (t *Table) printTopBottomCaption(w io.Writer, actualTableWidth int) {
|
||
t.logger.Debugf("[printCaption Entry] Text=%q, Spot=%v (type %T), Align=%q, UserWidth=%d, ActualTableWidth=%d",
|
||
t.caption.Text, t.caption.Spot, t.caption.Spot, t.caption.Align, t.caption.Width, actualTableWidth)
|
||
|
||
currentCaptionSpot := t.caption.Spot
|
||
isValidSpot := currentCaptionSpot >= tw.SpotTopLeft && currentCaptionSpot <= tw.SpotBottomRight
|
||
if t.caption.Text == "" || !isValidSpot {
|
||
t.logger.Debugf("[printCaption] Aborting: Text empty OR Spot invalid...")
|
||
return
|
||
}
|
||
|
||
var captionWrapWidth int
|
||
if t.caption.Width > 0 {
|
||
captionWrapWidth = t.caption.Width
|
||
t.logger.Debugf("[printCaption] Using user-defined caption.Width %d for wrapping.", captionWrapWidth)
|
||
} else if actualTableWidth <= 4 {
|
||
captionWrapWidth = twwidth.Width(t.caption.Text)
|
||
t.logger.Debugf("[printCaption] Empty table, no user caption.Width: Using natural caption width %d.", captionWrapWidth)
|
||
} else {
|
||
captionWrapWidth = actualTableWidth
|
||
t.logger.Debugf("[printCaption] Non-empty table, no user caption.Width: Using actualTableWidth %d for wrapping.", actualTableWidth)
|
||
}
|
||
|
||
if captionWrapWidth <= 0 {
|
||
captionWrapWidth = 10
|
||
t.logger.Warnf("[printCaption] captionWrapWidth was %d (<=0). Setting to minimum %d.", captionWrapWidth, 10)
|
||
}
|
||
t.logger.Debugf("[printCaption] Final captionWrapWidth to be used by twwarp: %d", captionWrapWidth)
|
||
|
||
wrappedCaptionLines, count := twwarp.WrapString(t.caption.Text, captionWrapWidth)
|
||
if count == 0 {
|
||
t.logger.Errorf("[printCaption] Error from twwarp.WrapString (width %d): %v. Text: %q", captionWrapWidth, count, t.caption.Text)
|
||
if strings.Contains(t.caption.Text, "\n") {
|
||
wrappedCaptionLines = strings.Split(t.caption.Text, "\n")
|
||
} else {
|
||
wrappedCaptionLines = []string{t.caption.Text}
|
||
}
|
||
t.logger.Debugf("[printCaption] Fallback: using %d lines from original text.", len(wrappedCaptionLines))
|
||
}
|
||
|
||
if len(wrappedCaptionLines) == 0 && t.caption.Text != "" {
|
||
t.logger.Warn("[printCaption] Wrapping resulted in zero lines for non-empty text. Using fallback.")
|
||
if strings.Contains(t.caption.Text, "\n") {
|
||
wrappedCaptionLines = strings.Split(t.caption.Text, "\n")
|
||
} else {
|
||
wrappedCaptionLines = []string{t.caption.Text}
|
||
}
|
||
} else if t.caption.Text != "" {
|
||
t.logger.Debugf("[printCaption] Wrapped caption into %d lines: %v", len(wrappedCaptionLines), wrappedCaptionLines)
|
||
}
|
||
|
||
paddingTargetWidth := actualTableWidth
|
||
if t.caption.Width > 0 {
|
||
paddingTargetWidth = t.caption.Width
|
||
} else if actualTableWidth <= 4 {
|
||
paddingTargetWidth = captionWrapWidth
|
||
}
|
||
t.logger.Debugf("[printCaption] Final paddingTargetWidth for tw.Pad: %d", paddingTargetWidth)
|
||
|
||
for i, line := range wrappedCaptionLines {
|
||
align := t.caption.Align
|
||
if align == "" || align == tw.AlignDefault || align == tw.AlignNone {
|
||
switch t.caption.Spot {
|
||
case tw.SpotTopLeft, tw.SpotBottomLeft:
|
||
align = tw.AlignLeft
|
||
case tw.SpotTopRight, tw.SpotBottomRight:
|
||
align = tw.AlignRight
|
||
default:
|
||
align = tw.AlignCenter
|
||
}
|
||
t.logger.Debugf("[printCaption] Line %d: Alignment defaulted to %s based on Spot %v", i, align, t.caption.Spot)
|
||
}
|
||
paddedLine := tw.Pad(line, " ", paddingTargetWidth, align)
|
||
t.logger.Debugf("[printCaption] Printing line %d: InputLine=%q, Align=%s, PaddingTargetWidth=%d, PaddedLine=%q",
|
||
i, line, align, paddingTargetWidth, paddedLine)
|
||
w.Write([]byte(paddedLine))
|
||
w.Write([]byte(tw.NewLine))
|
||
}
|
||
|
||
t.logger.Debugf("[printCaption] Finished printing all caption lines.")
|
||
}
|
||
|
||
// prepareContent processes cell content with formatting and wrapping.
|
||
// Parameters include cells to process and config for formatting rules.
|
||
// Returns a slice of string slices representing processed lines.
|
||
func (t *Table) prepareContent(cells []string, config tw.CellConfig) [][]string {
|
||
isStreaming := t.config.Stream.Enable && t.hasPrinted
|
||
t.logger.Debugf("prepareContent: Processing cells=%v (streaming: %v)", cells, isStreaming)
|
||
initialInputCellCount := len(cells)
|
||
result := make([][]string, 0)
|
||
|
||
effectiveNumCols := initialInputCellCount
|
||
if isStreaming {
|
||
if t.streamNumCols > 0 {
|
||
effectiveNumCols = t.streamNumCols
|
||
t.logger.Debugf("prepareContent: Streaming mode, using fixed streamNumCols: %d", effectiveNumCols)
|
||
if len(cells) != effectiveNumCols {
|
||
t.logger.Warnf("prepareContent: Streaming mode, input cell count (%d) does not match streamNumCols (%d). Input cells will be padded/truncated.", len(cells), effectiveNumCols)
|
||
if len(cells) < effectiveNumCols {
|
||
paddedCells := make([]string, effectiveNumCols)
|
||
copy(paddedCells, cells)
|
||
for i := len(cells); i < effectiveNumCols; i++ {
|
||
paddedCells[i] = tw.Empty
|
||
}
|
||
cells = paddedCells
|
||
} else if len(cells) > effectiveNumCols {
|
||
cells = cells[:effectiveNumCols]
|
||
}
|
||
}
|
||
} else {
|
||
t.logger.Warnf("prepareContent: Streaming mode enabled but streamNumCols is 0. Using input cell count %d. Stream widths may not be available.", effectiveNumCols)
|
||
}
|
||
}
|
||
|
||
if t.config.MaxWidth > 0 && !t.config.Widths.Constrained() {
|
||
if effectiveNumCols > 0 {
|
||
derivedSectionGlobalMaxWidth := int(math.Floor(float64(t.config.MaxWidth) / float64(effectiveNumCols)))
|
||
config.ColMaxWidths.Global = derivedSectionGlobalMaxWidth
|
||
t.logger.Debugf("prepareContent: Table MaxWidth %d active and t.config.Widths not constrained. "+
|
||
"Derived section ColMaxWidths.Global: %d for %d columns. This will be used by calculateContentMaxWidth if no higher priority constraints exist.",
|
||
t.config.MaxWidth, config.ColMaxWidths.Global, effectiveNumCols)
|
||
}
|
||
}
|
||
|
||
for i := 0; i < effectiveNumCols; i++ {
|
||
cellContent := ""
|
||
if i < len(cells) {
|
||
cellContent = cells[i]
|
||
} else {
|
||
cellContent = tw.Empty
|
||
}
|
||
|
||
cellContent = t.Trimmer(cellContent)
|
||
|
||
colPad := config.Padding.Global
|
||
if i < len(config.Padding.PerColumn) && config.Padding.PerColumn[i].Paddable() {
|
||
colPad = config.Padding.PerColumn[i]
|
||
}
|
||
|
||
padLeftWidth := twwidth.Width(colPad.Left)
|
||
padRightWidth := twwidth.Width(colPad.Right)
|
||
|
||
effectiveContentMaxWidth := t.calculateContentMaxWidth(i, config, padLeftWidth, padRightWidth, isStreaming)
|
||
|
||
if config.Formatting.AutoFormat.Enabled() {
|
||
cellContent = tw.Title(strings.Join(tw.SplitCamelCase(cellContent), tw.Space))
|
||
}
|
||
|
||
lines := strings.Split(cellContent, "\n")
|
||
finalLinesForCell := make([]string, 0)
|
||
for _, line := range lines {
|
||
if effectiveContentMaxWidth > 0 {
|
||
switch config.Formatting.AutoWrap {
|
||
case tw.WrapNormal:
|
||
var wrapped []string
|
||
if t.config.Behavior.TrimSpace.Enabled() {
|
||
wrapped, _ = twwarp.WrapString(line, effectiveContentMaxWidth)
|
||
} else {
|
||
wrapped, _ = twwarp.WrapStringWithSpaces(line, effectiveContentMaxWidth)
|
||
}
|
||
finalLinesForCell = append(finalLinesForCell, wrapped...)
|
||
case tw.WrapTruncate:
|
||
if twwidth.Width(line) > effectiveContentMaxWidth {
|
||
ellipsisWidth := twwidth.Width(tw.CharEllipsis)
|
||
if effectiveContentMaxWidth >= ellipsisWidth {
|
||
finalLinesForCell = append(finalLinesForCell, twwidth.Truncate(line, effectiveContentMaxWidth-ellipsisWidth, tw.CharEllipsis))
|
||
} else {
|
||
finalLinesForCell = append(finalLinesForCell, twwidth.Truncate(line, effectiveContentMaxWidth, ""))
|
||
}
|
||
} else {
|
||
finalLinesForCell = append(finalLinesForCell, line)
|
||
}
|
||
case tw.WrapBreak:
|
||
wrapped := make([]string, 0)
|
||
currentLine := line
|
||
breakCharWidth := twwidth.Width(tw.CharBreak)
|
||
for twwidth.Width(currentLine) > effectiveContentMaxWidth {
|
||
targetWidth := max(effectiveContentMaxWidth-breakCharWidth, 0)
|
||
breakPoint := tw.BreakPoint(currentLine, targetWidth)
|
||
runes := []rune(currentLine)
|
||
if breakPoint <= 0 || breakPoint > len(runes) {
|
||
t.logger.Warnf("prepareContent: WrapBreak - Invalid BreakPoint %d for line '%s' at width %d. Attempting manual break.", breakPoint, currentLine, targetWidth)
|
||
actualBreakRuneCount := 0
|
||
tempWidth := 0
|
||
for charIdx, r := range runes {
|
||
runeStr := string(r)
|
||
rw := twwidth.Width(runeStr)
|
||
if tempWidth+rw > targetWidth && charIdx > 0 {
|
||
break
|
||
}
|
||
tempWidth += rw
|
||
actualBreakRuneCount = charIdx + 1
|
||
if tempWidth >= targetWidth && charIdx == 0 {
|
||
break
|
||
}
|
||
}
|
||
if actualBreakRuneCount == 0 && len(runes) > 0 {
|
||
actualBreakRuneCount = 1
|
||
}
|
||
if actualBreakRuneCount > 0 && actualBreakRuneCount <= len(runes) {
|
||
wrapped = append(wrapped, string(runes[:actualBreakRuneCount])+tw.CharBreak)
|
||
currentLine = string(runes[actualBreakRuneCount:])
|
||
} else {
|
||
t.logger.Warnf("prepareContent: WrapBreak - Cannot break line '%s'. Adding as is.", currentLine)
|
||
wrapped = append(wrapped, currentLine)
|
||
currentLine = ""
|
||
break
|
||
}
|
||
} else {
|
||
wrapped = append(wrapped, string(runes[:breakPoint])+tw.CharBreak)
|
||
currentLine = string(runes[breakPoint:])
|
||
}
|
||
}
|
||
if twwidth.Width(currentLine) > 0 {
|
||
wrapped = append(wrapped, currentLine)
|
||
}
|
||
if len(wrapped) == 0 && twwidth.Width(line) > 0 && len(finalLinesForCell) == 0 {
|
||
finalLinesForCell = append(finalLinesForCell, line)
|
||
} else {
|
||
finalLinesForCell = append(finalLinesForCell, wrapped...)
|
||
}
|
||
default:
|
||
finalLinesForCell = append(finalLinesForCell, line)
|
||
}
|
||
} else {
|
||
finalLinesForCell = append(finalLinesForCell, line)
|
||
}
|
||
}
|
||
|
||
for len(result) < len(finalLinesForCell) {
|
||
newRow := make([]string, effectiveNumCols)
|
||
for j := range newRow {
|
||
newRow[j] = tw.Empty
|
||
}
|
||
result = append(result, newRow)
|
||
}
|
||
|
||
for j := 0; j < len(result); j++ {
|
||
cellLineContent := tw.Empty
|
||
if j < len(finalLinesForCell) {
|
||
cellLineContent = finalLinesForCell[j]
|
||
}
|
||
if i < len(result[j]) {
|
||
result[j][i] = cellLineContent
|
||
} else {
|
||
t.logger.Warnf("prepareContent: Column index %d out of bounds (%d) during result matrix population. EffectiveNumCols: %d. This indicates a logic error.",
|
||
i, len(result[j]), effectiveNumCols)
|
||
}
|
||
}
|
||
}
|
||
|
||
t.logger.Debugf("prepareContent: Content prepared, result %d lines.", len(result))
|
||
return result
|
||
}
|
||
|
||
// prepareContexts initializes rendering and merge contexts.
|
||
// No parameters are required.
|
||
// Returns renderContext, mergeContext, and an error if initialization fails.
|
||
func (t *Table) prepareContexts() (*renderContext, *mergeContext, error) {
|
||
numOriginalCols := t.maxColumns()
|
||
t.logger.Debugf("prepareContexts: Original number of columns: %d", numOriginalCols)
|
||
|
||
ctx := &renderContext{
|
||
table: t,
|
||
renderer: t.renderer,
|
||
cfg: t.renderer.Config(),
|
||
numCols: numOriginalCols,
|
||
widths: map[tw.Position]tw.Mapper[int, int]{
|
||
tw.Header: tw.NewMapper[int, int](),
|
||
tw.Row: tw.NewMapper[int, int](),
|
||
tw.Footer: tw.NewMapper[int, int](),
|
||
},
|
||
logger: t.logger,
|
||
}
|
||
|
||
isEmpty, visibleCount := t.getEmptyColumnInfo(numOriginalCols)
|
||
ctx.emptyColumns = isEmpty
|
||
ctx.visibleColCount = visibleCount
|
||
|
||
mctx := &mergeContext{
|
||
headerMerges: make(map[int]tw.MergeState),
|
||
rowMerges: make([]map[int]tw.MergeState, len(t.rows)),
|
||
footerMerges: make(map[int]tw.MergeState),
|
||
horzMerges: make(map[tw.Position]map[int]bool),
|
||
}
|
||
for i := range mctx.rowMerges {
|
||
mctx.rowMerges[i] = make(map[int]tw.MergeState)
|
||
}
|
||
|
||
ctx.headerLines = t.headers
|
||
ctx.rowLines = t.rows
|
||
ctx.footerLines = t.footers
|
||
|
||
if err := t.calculateAndNormalizeWidths(ctx); err != nil {
|
||
t.logger.Debugf("Error during initial width calculation: %v", err)
|
||
return nil, nil, err
|
||
}
|
||
t.logger.Debugf("Initial normalized widths (before hiding): H=%v, R=%v, F=%v",
|
||
ctx.widths[tw.Header], ctx.widths[tw.Row], ctx.widths[tw.Footer])
|
||
|
||
preparedHeaderLines, headerMerges, _ := t.prepareWithMerges(ctx.headerLines, t.config.Header, tw.Header)
|
||
ctx.headerLines = preparedHeaderLines
|
||
mctx.headerMerges = headerMerges
|
||
|
||
processedRowLines := make([][][]string, len(ctx.rowLines))
|
||
for i, row := range ctx.rowLines {
|
||
if mctx.rowMerges[i] == nil {
|
||
mctx.rowMerges[i] = make(map[int]tw.MergeState)
|
||
}
|
||
processedRowLines[i], mctx.rowMerges[i], _ = t.prepareWithMerges(row, t.config.Row, tw.Row)
|
||
}
|
||
ctx.rowLines = processedRowLines
|
||
|
||
t.applyHorizontalMergeWidths(tw.Header, ctx, mctx.headerMerges)
|
||
|
||
if t.config.Row.Formatting.MergeMode&tw.MergeVertical != 0 {
|
||
t.applyVerticalMerges(ctx, mctx)
|
||
}
|
||
if t.config.Row.Formatting.MergeMode&tw.MergeHierarchical != 0 {
|
||
t.applyHierarchicalMerges(ctx, mctx)
|
||
}
|
||
|
||
t.prepareFooter(ctx, mctx)
|
||
t.logger.Debugf("Footer prepared. Widths before hiding: H=%v, R=%v, F=%v",
|
||
ctx.widths[tw.Header], ctx.widths[tw.Row], ctx.widths[tw.Footer])
|
||
|
||
if t.config.Behavior.AutoHide.Enabled() {
|
||
t.logger.Debugf("Applying AutoHide: Adjusting widths for empty columns.")
|
||
if ctx.emptyColumns == nil {
|
||
t.logger.Debugf("Warning: ctx.emptyColumns is nil during width adjustment.")
|
||
} else if len(ctx.emptyColumns) != ctx.numCols {
|
||
t.logger.Debugf("Warning: Length mismatch between emptyColumns (%d) and numCols (%d). Skipping adjustment.", len(ctx.emptyColumns), ctx.numCols)
|
||
} else {
|
||
for colIdx := 0; colIdx < ctx.numCols; colIdx++ {
|
||
if ctx.emptyColumns[colIdx] {
|
||
t.logger.Debugf("AutoHide: Hiding column %d by setting width to 0.", colIdx)
|
||
ctx.widths[tw.Header].Set(colIdx, 0)
|
||
ctx.widths[tw.Row].Set(colIdx, 0)
|
||
ctx.widths[tw.Footer].Set(colIdx, 0)
|
||
}
|
||
}
|
||
t.logger.Debugf("Widths after AutoHide adjustment: H=%v, R=%v, F=%v",
|
||
ctx.widths[tw.Header], ctx.widths[tw.Row], ctx.widths[tw.Footer])
|
||
}
|
||
} else {
|
||
t.logger.Debugf("AutoHide is disabled, skipping width adjustment.")
|
||
}
|
||
t.logger.Debugf("prepareContexts completed all stages.")
|
||
return ctx, mctx, nil
|
||
}
|
||
|
||
// prepareFooter processes footer content and applies merges.
|
||
// Parameters ctx and mctx hold rendering and merge state.
|
||
// No return value.
|
||
func (t *Table) prepareFooter(ctx *renderContext, mctx *mergeContext) {
|
||
if len(t.footers) == 0 {
|
||
ctx.logger.Debugf("Skipping footer preparation - no footer data")
|
||
if ctx.widths[tw.Footer] == nil {
|
||
ctx.widths[tw.Footer] = tw.NewMapper[int, int]()
|
||
}
|
||
numCols := ctx.numCols
|
||
for i := 0; i < numCols; i++ {
|
||
ctx.widths[tw.Footer].Set(i, ctx.widths[tw.Row].Get(i))
|
||
}
|
||
t.logger.Debug("Initialized empty footer widths based on row widths: %v", ctx.widths[tw.Footer])
|
||
ctx.footerPrepared = true
|
||
return
|
||
}
|
||
|
||
t.logger.Debugf("Preparing footer with merge mode: %d", t.config.Footer.Formatting.MergeMode)
|
||
preparedLines, mergeStates, _ := t.prepareWithMerges(t.footers, t.config.Footer, tw.Footer)
|
||
t.footers = preparedLines
|
||
mctx.footerMerges = mergeStates
|
||
ctx.footerLines = t.footers
|
||
t.logger.Debugf("Base footer widths (normalized from rows/header): %v", ctx.widths[tw.Footer])
|
||
t.applyHorizontalMergeWidths(tw.Footer, ctx, mctx.footerMerges)
|
||
ctx.footerPrepared = true
|
||
t.logger.Debugf("Footer preparation completed. Final footer widths: %v", ctx.widths[tw.Footer])
|
||
}
|
||
|
||
// prepareWithMerges processes content and detects horizontal merges.
|
||
// Parameters include content, config, and position (Header, Row, Footer).
|
||
// Returns processed lines, merge states, and horizontal merge map.
|
||
func (t *Table) prepareWithMerges(content [][]string, config tw.CellConfig, position tw.Position) ([][]string, map[int]tw.MergeState, map[int]bool) {
|
||
t.logger.Debugf("PrepareWithMerges START: position=%s, mergeMode=%d", position, config.Formatting.MergeMode)
|
||
if len(content) == 0 {
|
||
t.logger.Debugf("PrepareWithMerges END: No content.")
|
||
return content, nil, nil
|
||
}
|
||
|
||
numCols := 0
|
||
if len(content) > 0 && len(content[0]) > 0 { // Assumes content[0] exists and has items
|
||
numCols = len(content[0])
|
||
} else { // Fallback if first line is empty or content is empty
|
||
for _, line := range content { // Find max columns from any line
|
||
if len(line) > numCols {
|
||
numCols = len(line)
|
||
}
|
||
}
|
||
if numCols == 0 { // If still 0, try table-wide max (batch mode context)
|
||
numCols = t.maxColumns()
|
||
}
|
||
}
|
||
|
||
if numCols == 0 {
|
||
t.logger.Debugf("PrepareWithMerges END: numCols is zero.")
|
||
return content, nil, nil
|
||
}
|
||
|
||
horzMergeMap := make(map[int]bool) // Tracks if a column is part of any horizontal merge for this logical row
|
||
mergeMap := make(map[int]tw.MergeState) // Final merge states for this logical row
|
||
|
||
// Ensure all lines in 'content' are padded to numCols for consistent processing
|
||
// This result is what will be modified and returned.
|
||
result := make([][]string, len(content))
|
||
for i := range content {
|
||
result[i] = padLine(content[i], numCols)
|
||
}
|
||
|
||
if config.Formatting.MergeMode&tw.MergeHorizontal != 0 {
|
||
t.logger.Debugf("Checking for horizontal merges (logical cell comparison) for %d visual lines, %d columns", len(content), numCols)
|
||
|
||
// Special handling for footer lead merge (often for "TOTAL" spanning empty cells)
|
||
// This logic only applies if it's a footer and typically to the first (often only) visual line.
|
||
if position == tw.Footer && len(content) > 0 {
|
||
lineIdx := 0 // Assume footer lead merge applies to the first visual line primarily
|
||
originalLine := padLine(content[lineIdx], numCols) // Use original content for decision
|
||
currentLineResult := result[lineIdx] // Modify the result line
|
||
|
||
firstContentIdx := -1
|
||
var firstContent string
|
||
for c := 0; c < numCols; c++ {
|
||
if c >= len(originalLine) {
|
||
break
|
||
}
|
||
trimmedVal := t.Trimmer(originalLine[c])
|
||
|
||
if trimmedVal != "" && trimmedVal != "-" { // "-" is often a placeholder not to merge over
|
||
firstContentIdx = c
|
||
firstContent = originalLine[c] // Store the raw content for placement
|
||
break
|
||
} else if trimmedVal == "-" { // Stop if we hit a hard non-mergeable placeholder
|
||
break
|
||
}
|
||
}
|
||
|
||
if firstContentIdx > 0 { // If content starts after the first column
|
||
span := firstContentIdx + 1 // Merge from col 0 up to and including firstContentIdx
|
||
startCol := 0
|
||
|
||
allEmptyBefore := true
|
||
for c := 0; c < firstContentIdx; c++ {
|
||
originalLine[c] = t.Trimmer(originalLine[c])
|
||
if c >= len(originalLine) || originalLine[c] != "" {
|
||
allEmptyBefore = false
|
||
break
|
||
}
|
||
}
|
||
|
||
if allEmptyBefore {
|
||
t.logger.Debugf("Footer lead-merge applied line %d: content '%s' from col %d moved to col %d, span %d",
|
||
lineIdx, firstContent, firstContentIdx, startCol, span)
|
||
|
||
if startCol < len(currentLineResult) {
|
||
currentLineResult[startCol] = firstContent // Place the original content
|
||
}
|
||
for k := startCol + 1; k < startCol+span; k++ { // Clear out other cells in the span
|
||
if k < len(currentLineResult) {
|
||
currentLineResult[k] = tw.Empty
|
||
}
|
||
}
|
||
|
||
// Update mergeMap for all visual lines of this logical row
|
||
for visualLine := 0; visualLine < len(result); visualLine++ {
|
||
// Only apply the data move to the line where it was detected,
|
||
// but the merge state should apply to the logical cell (all its visual lines).
|
||
if visualLine != lineIdx { // For other visual lines, just clear the cells in the span
|
||
if startCol < len(result[visualLine]) {
|
||
result[visualLine][startCol] = tw.Empty // Typically empty for other lines in a lead merge
|
||
}
|
||
for k := startCol + 1; k < startCol+span; k++ {
|
||
if k < len(result[visualLine]) {
|
||
result[visualLine][k] = tw.Empty
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Set merge state for the starting column
|
||
startState := mergeMap[startCol]
|
||
startState.Horizontal = tw.MergeStateOption{Present: true, Span: span, Start: true, End: (span == 1)}
|
||
mergeMap[startCol] = startState
|
||
horzMergeMap[startCol] = true // Mark this column as processed by a merge
|
||
|
||
// Set merge state for subsequent columns in the span
|
||
for k := startCol + 1; k < startCol+span; k++ {
|
||
colState := mergeMap[k]
|
||
colState.Horizontal = tw.MergeStateOption{Present: true, Span: span, Start: false, End: k == startCol+span-1}
|
||
mergeMap[k] = colState
|
||
horzMergeMap[k] = true // Mark as processed
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Standard horizontal merge logic based on full logical cell content
|
||
col := 0
|
||
for col < numCols {
|
||
if horzMergeMap[col] { // If already part of a footer lead-merge, skip
|
||
col++
|
||
continue
|
||
}
|
||
|
||
// Get full content of logical cell 'col'
|
||
var currentLogicalCellContentBuilder strings.Builder
|
||
for lineIdx := 0; lineIdx < len(content); lineIdx++ {
|
||
if col < len(content[lineIdx]) {
|
||
currentLogicalCellContentBuilder.WriteString(content[lineIdx][col])
|
||
}
|
||
}
|
||
|
||
currentLogicalCellTrimmed := t.Trimmer(currentLogicalCellContentBuilder.String())
|
||
if currentLogicalCellTrimmed == "" || currentLogicalCellTrimmed == "-" {
|
||
col++
|
||
continue
|
||
}
|
||
|
||
span := 1
|
||
for nextCol := col + 1; nextCol < numCols; nextCol++ {
|
||
if horzMergeMap[nextCol] { // Don't merge into an already merged (e.g. footer lead) column
|
||
break
|
||
}
|
||
var nextLogicalCellContentBuilder strings.Builder
|
||
for lineIdx := 0; lineIdx < len(content); lineIdx++ {
|
||
if nextCol < len(content[lineIdx]) {
|
||
nextLogicalCellContentBuilder.WriteString(content[lineIdx][nextCol])
|
||
}
|
||
}
|
||
|
||
nextLogicalCellTrimmed := t.Trimmer(nextLogicalCellContentBuilder.String())
|
||
if currentLogicalCellTrimmed == nextLogicalCellTrimmed && nextLogicalCellTrimmed != "-" {
|
||
span++
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
|
||
if span > 1 {
|
||
t.logger.Debugf("Standard horizontal merge (logical cell): startCol %d, span %d for content '%s'", col, span, currentLogicalCellTrimmed)
|
||
startState := mergeMap[col]
|
||
startState.Horizontal = tw.MergeStateOption{Present: true, Span: span, Start: true, End: (span == 1)}
|
||
mergeMap[col] = startState
|
||
horzMergeMap[col] = true
|
||
|
||
// For all visual lines, clear out the content of the merged-over cells
|
||
for lineIdx := 0; lineIdx < len(result); lineIdx++ {
|
||
for k := col + 1; k < col+span; k++ {
|
||
if k < len(result[lineIdx]) {
|
||
result[lineIdx][k] = tw.Empty
|
||
}
|
||
}
|
||
}
|
||
|
||
// Set merge state for subsequent columns in the span
|
||
for k := col + 1; k < col+span; k++ {
|
||
colState := mergeMap[k]
|
||
colState.Horizontal = tw.MergeStateOption{Present: true, Span: span, Start: false, End: k == col+span-1}
|
||
mergeMap[k] = colState
|
||
horzMergeMap[k] = true
|
||
}
|
||
col += span
|
||
} else {
|
||
col++
|
||
}
|
||
}
|
||
}
|
||
|
||
t.logger.Debugf("PrepareWithMerges END: position=%s, lines=%d, mergeMapH: %v", position, len(result), func() map[int]tw.MergeStateOption {
|
||
m := make(map[int]tw.MergeStateOption)
|
||
for k, v := range mergeMap {
|
||
m[k] = v.Horizontal
|
||
}
|
||
return m
|
||
}())
|
||
return result, mergeMap, horzMergeMap
|
||
}
|
||
|
||
// render generates the table output using the configured renderer.
|
||
// No parameters are required.
|
||
// Returns an error if rendering fails in any section.
|
||
func (t *Table) render() error {
|
||
t.ensureInitialized()
|
||
|
||
// Save the original writer and schedule its restoration upon function exit.
|
||
// This guarantees the table's writer is restored even if errors occur.
|
||
originalWriter := t.writer
|
||
defer func() {
|
||
t.writer = originalWriter
|
||
}()
|
||
|
||
// If a counter is active, wrap the writer in a MultiWriter.
|
||
if len(t.counters) > 0 {
|
||
// The slice must be of type io.Writer.
|
||
// Start it with the original destination writer.
|
||
allWriters := []io.Writer{originalWriter}
|
||
|
||
// Append each counter to the slice of writers.
|
||
for _, c := range t.counters {
|
||
allWriters = append(allWriters, c)
|
||
}
|
||
|
||
// Create a MultiWriter that broadcasts to the original writer AND all counters.
|
||
t.writer = io.MultiWriter(allWriters...)
|
||
}
|
||
|
||
if t.config.Stream.Enable {
|
||
t.logger.Warn("Render() called in streaming mode. Use Start/Append/Close methods instead.")
|
||
return errors.New("render called in streaming mode; use Start/Append/Close")
|
||
}
|
||
|
||
// Calculate and cache the column count for this specific batch render pass.
|
||
t.batchRenderNumCols = t.maxColumns()
|
||
t.isBatchRenderNumColsSet = true
|
||
defer func() {
|
||
t.isBatchRenderNumColsSet = false
|
||
t.logger.Debugf("Render(): Cleared isBatchRenderNumColsSet to false (batchRenderNumCols was %d).", t.batchRenderNumCols)
|
||
}()
|
||
|
||
hasCaption := t.caption.Text != "" && t.caption.Spot != tw.SpotNone
|
||
isTopOrBottomCaption := hasCaption &&
|
||
(t.caption.Spot >= tw.SpotTopLeft && t.caption.Spot <= tw.SpotBottomRight)
|
||
|
||
var tableStringBuffer *strings.Builder
|
||
targetWriter := t.writer // Can be the original writer or the MultiWriter.
|
||
|
||
// If a caption is present, the main table content must be rendered to an
|
||
// in-memory buffer first to calculate its final width.
|
||
if isTopOrBottomCaption {
|
||
tableStringBuffer = &strings.Builder{}
|
||
targetWriter = tableStringBuffer
|
||
t.logger.Debugf("Top/Bottom caption detected. Rendering table core to buffer first.")
|
||
} else {
|
||
t.logger.Debugf("No caption detected. Rendering table core directly to writer.")
|
||
}
|
||
|
||
// Point the table's writer to the target (either the final destination or the buffer).
|
||
t.writer = targetWriter
|
||
ctx, mctx, err := t.prepareContexts()
|
||
if err != nil {
|
||
t.logger.Errorf("prepareContexts failed: %v", err)
|
||
return errors.Newf("failed to prepare table contexts").Wrap(err)
|
||
}
|
||
|
||
if err := ctx.renderer.Start(t.writer); err != nil {
|
||
t.logger.Errorf("Renderer Start() error: %v", err)
|
||
return errors.Newf("renderer start failed").Wrap(err)
|
||
}
|
||
|
||
renderError := false
|
||
var firstRenderErr error
|
||
renderFuncs := []func(*renderContext, *mergeContext) error{
|
||
t.renderHeader,
|
||
t.renderRow,
|
||
t.renderFooter,
|
||
}
|
||
for i, renderFn := range renderFuncs {
|
||
sectionName := []string{"Header", "Row", "Footer"}[i]
|
||
if renderErr := renderFn(ctx, mctx); renderErr != nil {
|
||
t.logger.Errorf("Renderer section error (%s): %v", sectionName, renderErr)
|
||
if !renderError {
|
||
firstRenderErr = errors.Newf("failed to render %s section", sectionName).Wrap(renderErr)
|
||
}
|
||
renderError = true
|
||
break
|
||
}
|
||
}
|
||
|
||
if closeErr := ctx.renderer.Close(); closeErr != nil {
|
||
t.logger.Errorf("Renderer Close() error: %v", closeErr)
|
||
if !renderError {
|
||
firstRenderErr = errors.Newf("renderer close failed").Wrap(closeErr)
|
||
}
|
||
renderError = true
|
||
}
|
||
|
||
// Restore the writer to the original for the caption-handling logic.
|
||
// This is necessary because the caption must be written to the final
|
||
// destination, not the temporary buffer used for the table body.
|
||
t.writer = originalWriter
|
||
|
||
if renderError {
|
||
return firstRenderErr
|
||
}
|
||
|
||
// Caption Handling & Final Output
|
||
if isTopOrBottomCaption {
|
||
renderedTableContent := tableStringBuffer.String()
|
||
t.logger.Debugf("[Render] Table core buffer length: %d", len(renderedTableContent))
|
||
|
||
// Handle edge case where table is empty but should have borders.
|
||
shouldHaveBorders := t.renderer != nil && (t.renderer.Config().Borders.Top.Enabled() || t.renderer.Config().Borders.Bottom.Enabled())
|
||
if len(renderedTableContent) == 0 && shouldHaveBorders {
|
||
var sb strings.Builder
|
||
if t.renderer.Config().Borders.Top.Enabled() {
|
||
sb.WriteString("+--+")
|
||
sb.WriteString(t.newLine)
|
||
}
|
||
if t.renderer.Config().Borders.Bottom.Enabled() {
|
||
sb.WriteString("+--+")
|
||
}
|
||
renderedTableContent = sb.String()
|
||
t.logger.Warnf("[Render] Table buffer was empty despite enabled borders. Manually generated minimal output: %q", renderedTableContent)
|
||
}
|
||
|
||
actualTableWidth := 0
|
||
trimmedBuffer := strings.TrimRight(renderedTableContent, "\r\n \t")
|
||
for _, line := range strings.Split(trimmedBuffer, "\n") {
|
||
w := twwidth.Width(line)
|
||
if w > actualTableWidth {
|
||
actualTableWidth = w
|
||
}
|
||
}
|
||
t.logger.Debugf("[Render] Calculated actual table width: %d (from content: %q)", actualTableWidth, renderedTableContent)
|
||
|
||
isTopCaption := t.caption.Spot >= tw.SpotTopLeft && t.caption.Spot <= tw.SpotTopRight
|
||
|
||
if isTopCaption {
|
||
t.logger.Debugf("[Render] Printing Top Caption.")
|
||
t.printTopBottomCaption(t.writer, actualTableWidth)
|
||
}
|
||
|
||
if len(renderedTableContent) > 0 {
|
||
t.logger.Debugf("[Render] Printing table content (length %d) to final writer.", len(renderedTableContent))
|
||
t.writer.Write([]byte(renderedTableContent))
|
||
if !isTopCaption && t.caption.Text != "" && !strings.HasSuffix(renderedTableContent, t.newLine) {
|
||
t.writer.Write([]byte(tw.NewLine))
|
||
t.logger.Debugf("[Render] Added trailing newline after table content before bottom caption.")
|
||
}
|
||
} else {
|
||
t.logger.Debugf("[Render] No table content (original buffer or generated) to print.")
|
||
}
|
||
|
||
if !isTopCaption {
|
||
t.logger.Debugf("[Render] Calling printTopBottomCaption for Bottom Caption. Width: %d", actualTableWidth)
|
||
t.printTopBottomCaption(t.writer, actualTableWidth)
|
||
t.logger.Debugf("[Render] Returned from printTopBottomCaption for Bottom Caption.")
|
||
}
|
||
}
|
||
|
||
t.hasPrinted = true
|
||
t.logger.Info("Render() completed.")
|
||
return nil
|
||
}
|
||
|
||
// renderFooter renders the table's footer section with borders and padding.
|
||
// Parameters ctx and mctx hold rendering and merge state.
|
||
// Returns an error if rendering fails.
|
||
func (t *Table) renderFooter(ctx *renderContext, mctx *mergeContext) error {
|
||
if !ctx.footerPrepared {
|
||
t.prepareFooter(ctx, mctx)
|
||
}
|
||
|
||
f := ctx.renderer
|
||
cfg := ctx.cfg
|
||
|
||
hasContent := len(ctx.footerLines) > 0
|
||
hasTopPadding := t.config.Footer.Padding.Global.Top != tw.Empty
|
||
hasBottomPaddingConfig := t.config.Footer.Padding.Global.Bottom != tw.Empty || t.hasPerColumnBottomPadding()
|
||
hasAnyFooterElement := hasContent || hasTopPadding || hasBottomPaddingConfig
|
||
|
||
if !hasAnyFooterElement {
|
||
hasContentAbove := len(ctx.rowLines) > 0 || len(ctx.headerLines) > 0
|
||
if hasContentAbove && cfg.Borders.Bottom.Enabled() && cfg.Settings.Lines.ShowBottom.Enabled() {
|
||
ctx.logger.Debugf("Footer is empty, rendering table bottom border based on last row/header")
|
||
var lastLineAboveCtx *helperContext
|
||
var lastLineAligns map[int]tw.Align
|
||
var lastLinePadding map[int]tw.Padding
|
||
|
||
if len(ctx.rowLines) > 0 {
|
||
lastRowIdx := len(ctx.rowLines) - 1
|
||
lastRowLineIdx := -1
|
||
var lastRowLine []string
|
||
if lastRowIdx >= 0 && len(ctx.rowLines[lastRowIdx]) > 0 {
|
||
lastRowLineIdx = len(ctx.rowLines[lastRowIdx]) - 1
|
||
lastRowLine = padLine(ctx.rowLines[lastRowIdx][lastRowLineIdx], ctx.numCols)
|
||
} else {
|
||
lastRowLine = make([]string, ctx.numCols)
|
||
}
|
||
lastLineAboveCtx = &helperContext{
|
||
position: tw.Row,
|
||
rowIdx: lastRowIdx,
|
||
lineIdx: lastRowLineIdx,
|
||
line: lastRowLine,
|
||
location: tw.LocationEnd,
|
||
}
|
||
lastLineAligns = t.buildAligns(t.config.Row)
|
||
lastLinePadding = t.buildPadding(t.config.Row.Padding)
|
||
} else {
|
||
lastHeaderLineIdx := -1
|
||
var lastHeaderLine []string
|
||
if len(ctx.headerLines) > 0 {
|
||
lastHeaderLineIdx = len(ctx.headerLines) - 1
|
||
lastHeaderLine = padLine(ctx.headerLines[lastHeaderLineIdx], ctx.numCols)
|
||
} else {
|
||
lastHeaderLine = make([]string, ctx.numCols)
|
||
}
|
||
lastLineAboveCtx = &helperContext{
|
||
position: tw.Header,
|
||
rowIdx: 0,
|
||
lineIdx: lastHeaderLineIdx,
|
||
line: lastHeaderLine,
|
||
location: tw.LocationEnd,
|
||
}
|
||
lastLineAligns = t.buildAligns(t.config.Header)
|
||
lastLinePadding = t.buildPadding(t.config.Header.Padding)
|
||
}
|
||
|
||
resp := t.buildCellContexts(ctx, mctx, lastLineAboveCtx, lastLineAligns, lastLinePadding)
|
||
ctx.logger.Debugf("Bottom border: Using Widths=%v", ctx.widths[tw.Row])
|
||
f.Line(tw.Formatting{
|
||
Row: tw.RowContext{
|
||
Widths: ctx.widths[tw.Row],
|
||
Current: resp.cells,
|
||
Previous: resp.prevCells,
|
||
Position: lastLineAboveCtx.position,
|
||
Location: tw.LocationEnd,
|
||
ColMaxWidths: t.getColMaxWidths(tw.Footer),
|
||
},
|
||
Level: tw.LevelFooter,
|
||
IsSubRow: false,
|
||
})
|
||
} else {
|
||
ctx.logger.Debugf("Footer is empty and no content above or borders disabled, skipping footer render")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
ctx.logger.Debugf("Rendering footer section (has elements)")
|
||
hasContentAbove := len(ctx.rowLines) > 0 || len(ctx.headerLines) > 0
|
||
colAligns := t.buildAligns(t.config.Footer)
|
||
colPadding := t.buildPadding(t.config.Footer.Padding)
|
||
hctx := &helperContext{position: tw.Footer}
|
||
// Declare paddingLineContentForContext with a default value
|
||
paddingLineContentForContext := make([]string, ctx.numCols)
|
||
|
||
if hasContentAbove && cfg.Settings.Lines.ShowFooterLine.Enabled() && !hasTopPadding && len(ctx.footerLines) > 0 {
|
||
ctx.logger.Debugf("Rendering footer separator line")
|
||
var lastLineAboveCtx *helperContext
|
||
var lastLineAligns map[int]tw.Align
|
||
var lastLinePadding map[int]tw.Padding
|
||
var lastLinePosition tw.Position
|
||
|
||
if len(ctx.rowLines) > 0 {
|
||
lastRowIdx := len(ctx.rowLines) - 1
|
||
lastRowLineIdx := -1
|
||
var lastRowLine []string
|
||
if lastRowIdx >= 0 && len(ctx.rowLines[lastRowIdx]) > 0 {
|
||
lastRowLineIdx = len(ctx.rowLines[lastRowIdx]) - 1
|
||
lastRowLine = padLine(ctx.rowLines[lastRowIdx][lastRowLineIdx], ctx.numCols)
|
||
} else {
|
||
lastRowLine = make([]string, ctx.numCols)
|
||
}
|
||
lastLineAboveCtx = &helperContext{
|
||
position: tw.Row,
|
||
rowIdx: lastRowIdx,
|
||
lineIdx: lastRowLineIdx,
|
||
line: lastRowLine,
|
||
location: tw.LocationMiddle,
|
||
}
|
||
lastLineAligns = t.buildAligns(t.config.Row)
|
||
lastLinePadding = t.buildPadding(t.config.Row.Padding)
|
||
lastLinePosition = tw.Row
|
||
} else {
|
||
lastHeaderLineIdx := -1
|
||
var lastHeaderLine []string
|
||
if len(ctx.headerLines) > 0 {
|
||
lastHeaderLineIdx = len(ctx.headerLines) - 1
|
||
lastHeaderLine = padLine(ctx.headerLines[lastHeaderLineIdx], ctx.numCols)
|
||
} else {
|
||
lastHeaderLine = make([]string, ctx.numCols)
|
||
}
|
||
lastLineAboveCtx = &helperContext{
|
||
position: tw.Header,
|
||
rowIdx: 0,
|
||
lineIdx: lastHeaderLineIdx,
|
||
line: lastHeaderLine,
|
||
location: tw.LocationMiddle,
|
||
}
|
||
lastLineAligns = t.buildAligns(t.config.Header)
|
||
lastLinePadding = t.buildPadding(t.config.Header.Padding)
|
||
lastLinePosition = tw.Header
|
||
}
|
||
|
||
resp := t.buildCellContexts(ctx, mctx, lastLineAboveCtx, lastLineAligns, lastLinePadding)
|
||
var nextCells map[int]tw.CellContext
|
||
if hasContent {
|
||
nextCells = make(map[int]tw.CellContext)
|
||
for j, cellData := range padLine(ctx.footerLines[0], ctx.numCols) {
|
||
mergeState := tw.MergeState{}
|
||
if mctx.footerMerges != nil {
|
||
mergeState = mctx.footerMerges[j]
|
||
}
|
||
nextCells[j] = tw.CellContext{Data: cellData, Merge: mergeState, Width: ctx.widths[tw.Footer].Get(j)}
|
||
}
|
||
}
|
||
ctx.logger.Debugf("Footer separator: Using Widths=%v", ctx.widths[tw.Row])
|
||
f.Line(tw.Formatting{
|
||
Row: tw.RowContext{
|
||
Widths: ctx.widths[tw.Row],
|
||
Current: resp.cells,
|
||
Previous: resp.prevCells,
|
||
Next: nextCells,
|
||
Position: lastLinePosition,
|
||
Location: tw.LocationMiddle,
|
||
ColMaxWidths: t.getColMaxWidths(tw.Footer),
|
||
},
|
||
Level: tw.LevelFooter,
|
||
IsSubRow: false,
|
||
HasFooter: true,
|
||
})
|
||
}
|
||
|
||
if hasTopPadding {
|
||
hctx.rowIdx = 0
|
||
hctx.lineIdx = -1
|
||
if !hasContentAbove || !cfg.Settings.Lines.ShowFooterLine.Enabled() {
|
||
hctx.location = tw.LocationFirst
|
||
} else {
|
||
hctx.location = tw.LocationMiddle
|
||
}
|
||
hctx.line = t.buildPaddingLineContents(t.config.Footer.Padding.Global.Top, ctx.widths[tw.Footer], ctx.numCols, mctx.footerMerges)
|
||
ctx.logger.Debugf("Calling renderPadding for Footer Top Padding line: %v (loc: %v)", hctx.line, hctx.location)
|
||
if err := t.renderPadding(ctx, mctx, hctx, t.config.Footer.Padding.Global.Top); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
lastRenderedLineIdx := -2
|
||
if hasTopPadding {
|
||
lastRenderedLineIdx = -1
|
||
}
|
||
for i, line := range ctx.footerLines {
|
||
hctx.rowIdx = 0
|
||
hctx.lineIdx = i
|
||
hctx.line = padLine(line, ctx.numCols)
|
||
isFirstContentLine := i == 0
|
||
isLastContentLine := i == len(ctx.footerLines)-1
|
||
if isFirstContentLine && !hasTopPadding && (!hasContentAbove || !cfg.Settings.Lines.ShowFooterLine.Enabled()) {
|
||
hctx.location = tw.LocationFirst
|
||
} else if isLastContentLine && !hasBottomPaddingConfig {
|
||
hctx.location = tw.LocationEnd
|
||
} else {
|
||
hctx.location = tw.LocationMiddle
|
||
}
|
||
ctx.logger.Debugf("Rendering footer content line %d with location %v", i, hctx.location)
|
||
if err := t.renderLine(ctx, mctx, hctx, colAligns, colPadding); err != nil {
|
||
return err
|
||
}
|
||
lastRenderedLineIdx = i
|
||
}
|
||
|
||
if hasBottomPaddingConfig {
|
||
paddingLineContentForContext = make([]string, ctx.numCols)
|
||
formattedPaddingCells := make([]string, ctx.numCols)
|
||
representativePadChar := " "
|
||
ctx.logger.Debugf("Constructing Footer Bottom Padding line content strings")
|
||
for j := 0; j < ctx.numCols; j++ {
|
||
colWd := ctx.widths[tw.Footer].Get(j)
|
||
mergeState := tw.MergeState{}
|
||
if mctx.footerMerges != nil {
|
||
if state, ok := mctx.footerMerges[j]; ok {
|
||
mergeState = state
|
||
}
|
||
}
|
||
if mergeState.Horizontal.Present && !mergeState.Horizontal.Start {
|
||
paddingLineContentForContext[j] = ""
|
||
formattedPaddingCells[j] = ""
|
||
continue
|
||
}
|
||
padChar := " "
|
||
if j < len(t.config.Footer.Padding.PerColumn) && t.config.Footer.Padding.PerColumn[j].Bottom != tw.Empty {
|
||
padChar = t.config.Footer.Padding.PerColumn[j].Bottom
|
||
} else if t.config.Footer.Padding.Global.Bottom != tw.Empty {
|
||
padChar = t.config.Footer.Padding.Global.Bottom
|
||
}
|
||
paddingLineContentForContext[j] = padChar
|
||
if j == 0 || representativePadChar == " " {
|
||
representativePadChar = padChar
|
||
}
|
||
padWidth := max(twwidth.Width(padChar), 1)
|
||
repeatCount := 0
|
||
if colWd > 0 && padWidth > 0 {
|
||
repeatCount = colWd / padWidth
|
||
}
|
||
if colWd > 0 && repeatCount < 1 && padChar != " " {
|
||
repeatCount = 1
|
||
}
|
||
if colWd == 0 {
|
||
repeatCount = 0
|
||
}
|
||
rawPaddingContent := strings.Repeat(padChar, repeatCount)
|
||
currentWd := twwidth.Width(rawPaddingContent)
|
||
if currentWd < colWd {
|
||
rawPaddingContent += strings.Repeat(" ", colWd-currentWd)
|
||
}
|
||
if currentWd > colWd && colWd > 0 {
|
||
rawPaddingContent = twwidth.Truncate(rawPaddingContent, colWd)
|
||
}
|
||
if colWd == 0 {
|
||
rawPaddingContent = ""
|
||
}
|
||
formattedPaddingCells[j] = rawPaddingContent
|
||
}
|
||
ctx.logger.Debugf("Manually rendering Footer Bottom Padding line (char like '%s')", representativePadChar)
|
||
var paddingLineOutput strings.Builder
|
||
if cfg.Borders.Left.Enabled() {
|
||
paddingLineOutput.WriteString(cfg.Symbols.Column())
|
||
}
|
||
for colIdx := 0; colIdx < ctx.numCols; {
|
||
if colIdx > 0 && cfg.Settings.Separators.BetweenColumns.Enabled() {
|
||
shouldAddSeparator := true
|
||
if prevMergeState, ok := mctx.footerMerges[colIdx-1]; ok {
|
||
if prevMergeState.Horizontal.Present && !prevMergeState.Horizontal.End {
|
||
shouldAddSeparator = false
|
||
}
|
||
}
|
||
if shouldAddSeparator {
|
||
paddingLineOutput.WriteString(cfg.Symbols.Column())
|
||
}
|
||
}
|
||
if colIdx < len(formattedPaddingCells) {
|
||
paddingLineOutput.WriteString(formattedPaddingCells[colIdx])
|
||
}
|
||
currentMergeState := tw.MergeState{}
|
||
if mctx.footerMerges != nil {
|
||
if state, ok := mctx.footerMerges[colIdx]; ok {
|
||
currentMergeState = state
|
||
}
|
||
}
|
||
if currentMergeState.Horizontal.Present && currentMergeState.Horizontal.Start {
|
||
colIdx += currentMergeState.Horizontal.Span
|
||
} else {
|
||
colIdx++
|
||
}
|
||
}
|
||
if cfg.Borders.Right.Enabled() {
|
||
paddingLineOutput.WriteString(cfg.Symbols.Column())
|
||
}
|
||
paddingLineOutput.WriteString(t.newLine)
|
||
t.writer.Write([]byte(paddingLineOutput.String()))
|
||
ctx.logger.Debugf("Manually rendered Footer Bottom Padding line: %s", strings.TrimSuffix(paddingLineOutput.String(), t.newLine))
|
||
hctx.rowIdx = 0
|
||
hctx.lineIdx = len(ctx.footerLines)
|
||
hctx.line = paddingLineContentForContext
|
||
hctx.location = tw.LocationEnd
|
||
lastRenderedLineIdx = hctx.lineIdx
|
||
}
|
||
|
||
if cfg.Borders.Bottom.Enabled() && cfg.Settings.Lines.ShowBottom.Enabled() {
|
||
ctx.logger.Debugf("Rendering final table bottom border")
|
||
if lastRenderedLineIdx == len(ctx.footerLines) {
|
||
hctx.rowIdx = 0
|
||
hctx.lineIdx = lastRenderedLineIdx
|
||
hctx.line = paddingLineContentForContext
|
||
hctx.location = tw.LocationEnd
|
||
ctx.logger.Debugf("Setting border context based on bottom padding line")
|
||
} else if lastRenderedLineIdx >= 0 {
|
||
hctx.rowIdx = 0
|
||
hctx.lineIdx = lastRenderedLineIdx
|
||
hctx.line = padLine(ctx.footerLines[hctx.lineIdx], ctx.numCols)
|
||
hctx.location = tw.LocationEnd
|
||
ctx.logger.Debugf("Setting border context based on last content line idx %d", hctx.lineIdx)
|
||
} else if lastRenderedLineIdx == -1 {
|
||
hctx.rowIdx = 0
|
||
hctx.lineIdx = -1
|
||
hctx.line = paddingLineContentForContext
|
||
hctx.location = tw.LocationEnd
|
||
ctx.logger.Debugf("Setting border context based on top padding line")
|
||
} else {
|
||
hctx.rowIdx = 0
|
||
hctx.lineIdx = -2
|
||
hctx.line = make([]string, ctx.numCols)
|
||
hctx.location = tw.LocationEnd
|
||
ctx.logger.Debugf("Warning: Cannot determine context for bottom border")
|
||
}
|
||
resp := t.buildCellContexts(ctx, mctx, hctx, colAligns, colPadding)
|
||
ctx.logger.Debugf("Bottom border: Using Widths=%v", ctx.widths[tw.Row])
|
||
f.Line(tw.Formatting{
|
||
Row: tw.RowContext{
|
||
Widths: ctx.widths[tw.Row],
|
||
Current: resp.cells,
|
||
Previous: resp.prevCells,
|
||
Position: tw.Footer,
|
||
Location: tw.LocationEnd,
|
||
ColMaxWidths: t.getColMaxWidths(tw.Footer),
|
||
},
|
||
Level: tw.LevelFooter,
|
||
IsSubRow: false,
|
||
})
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// renderHeader renders the table's header section with borders and padding.
|
||
// Parameters ctx and mctx hold rendering and merge state.
|
||
// Returns an error if rendering fails.
|
||
func (t *Table) renderHeader(ctx *renderContext, mctx *mergeContext) error {
|
||
if len(ctx.headerLines) == 0 {
|
||
return nil
|
||
}
|
||
ctx.logger.Debug("Rendering header section")
|
||
|
||
f := ctx.renderer
|
||
cfg := ctx.cfg
|
||
colAligns := t.buildAligns(t.config.Header)
|
||
colPadding := t.buildPadding(t.config.Header.Padding)
|
||
hctx := &helperContext{position: tw.Header}
|
||
|
||
if cfg.Borders.Top.Enabled() && cfg.Settings.Lines.ShowTop.Enabled() {
|
||
ctx.logger.Debug("Rendering table top border")
|
||
nextCells := make(map[int]tw.CellContext)
|
||
if len(ctx.headerLines) > 0 {
|
||
for j, cell := range ctx.headerLines[0] {
|
||
nextCells[j] = tw.CellContext{Data: cell, Merge: mctx.headerMerges[j]}
|
||
}
|
||
}
|
||
f.Line(tw.Formatting{
|
||
Row: tw.RowContext{
|
||
Widths: ctx.widths[tw.Header],
|
||
Next: nextCells,
|
||
Position: tw.Header,
|
||
Location: tw.LocationFirst,
|
||
},
|
||
Level: tw.LevelHeader,
|
||
IsSubRow: false,
|
||
})
|
||
}
|
||
|
||
if t.config.Header.Padding.Global.Top != tw.Empty {
|
||
hctx.location = tw.LocationFirst
|
||
hctx.line = t.buildPaddingLineContents(t.config.Header.Padding.Global.Top, ctx.widths[tw.Header], ctx.numCols, mctx.headerMerges)
|
||
if err := t.renderPadding(ctx, mctx, hctx, t.config.Header.Padding.Global.Top); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
for i, line := range ctx.headerLines {
|
||
hctx.rowIdx = 0
|
||
hctx.lineIdx = i
|
||
hctx.line = padLine(line, ctx.numCols)
|
||
hctx.location = t.determineLocation(i, len(ctx.headerLines), t.config.Header.Padding.Global.Top, t.config.Header.Padding.Global.Bottom)
|
||
|
||
if t.config.Header.Callbacks.Global != nil {
|
||
ctx.logger.Debug("Executing global header callback for line %d", i)
|
||
t.config.Header.Callbacks.Global()
|
||
}
|
||
for colIdx, cb := range t.config.Header.Callbacks.PerColumn {
|
||
if colIdx < ctx.numCols && cb != nil {
|
||
ctx.logger.Debug("Executing per-column header callback for line %d, col %d", i, colIdx)
|
||
cb()
|
||
}
|
||
}
|
||
|
||
if err := t.renderLine(ctx, mctx, hctx, colAligns, colPadding); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
if t.config.Header.Padding.Global.Bottom != tw.Empty {
|
||
hctx.location = tw.LocationEnd
|
||
hctx.line = t.buildPaddingLineContents(t.config.Header.Padding.Global.Bottom, ctx.widths[tw.Header], ctx.numCols, mctx.headerMerges)
|
||
if err := t.renderPadding(ctx, mctx, hctx, t.config.Header.Padding.Global.Bottom); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
if cfg.Settings.Lines.ShowHeaderLine.Enabled() && (len(ctx.rowLines) > 0 || len(ctx.footerLines) > 0) {
|
||
ctx.logger.Debug("Rendering header separator line")
|
||
resp := t.buildCellContexts(ctx, mctx, hctx, colAligns, colPadding)
|
||
|
||
var nextSectionCells map[int]tw.CellContext
|
||
var nextSectionWidths tw.Mapper[int, int]
|
||
|
||
if len(ctx.rowLines) > 0 {
|
||
nextSectionWidths = ctx.widths[tw.Row]
|
||
rowColAligns := t.buildAligns(t.config.Row)
|
||
rowColPadding := t.buildPadding(t.config.Row.Padding)
|
||
firstRowHctx := &helperContext{
|
||
position: tw.Row,
|
||
rowIdx: 0,
|
||
lineIdx: 0,
|
||
}
|
||
if len(ctx.rowLines[0]) > 0 {
|
||
firstRowHctx.line = padLine(ctx.rowLines[0][0], ctx.numCols)
|
||
} else {
|
||
firstRowHctx.line = make([]string, ctx.numCols)
|
||
}
|
||
firstRowResp := t.buildCellContexts(ctx, mctx, firstRowHctx, rowColAligns, rowColPadding)
|
||
nextSectionCells = firstRowResp.cells
|
||
} else if len(ctx.footerLines) > 0 {
|
||
nextSectionWidths = ctx.widths[tw.Row]
|
||
footerColAligns := t.buildAligns(t.config.Footer)
|
||
footerColPadding := t.buildPadding(t.config.Footer.Padding)
|
||
firstFooterHctx := &helperContext{
|
||
position: tw.Footer,
|
||
rowIdx: 0,
|
||
lineIdx: 0,
|
||
}
|
||
if len(ctx.footerLines) > 0 {
|
||
firstFooterHctx.line = padLine(ctx.footerLines[0], ctx.numCols)
|
||
} else {
|
||
firstFooterHctx.line = make([]string, ctx.numCols)
|
||
}
|
||
firstFooterResp := t.buildCellContexts(ctx, mctx, firstFooterHctx, footerColAligns, footerColPadding)
|
||
nextSectionCells = firstFooterResp.cells
|
||
} else {
|
||
nextSectionWidths = ctx.widths[tw.Header]
|
||
nextSectionCells = nil
|
||
}
|
||
|
||
f.Line(tw.Formatting{
|
||
Row: tw.RowContext{
|
||
Widths: nextSectionWidths,
|
||
Current: resp.cells,
|
||
Previous: resp.prevCells,
|
||
Next: nextSectionCells,
|
||
Position: tw.Header,
|
||
Location: tw.LocationMiddle,
|
||
},
|
||
Level: tw.LevelBody,
|
||
IsSubRow: false,
|
||
})
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// renderLine renders a single line with callbacks and normalized widths.
|
||
// Parameters include ctx, mctx, hctx, aligns, and padding for rendering.
|
||
// Returns an error if rendering fails.
|
||
func (t *Table) renderLine(ctx *renderContext, mctx *mergeContext, hctx *helperContext, aligns map[int]tw.Align, padding map[int]tw.Padding) error {
|
||
resp := t.buildCellContexts(ctx, mctx, hctx, aligns, padding)
|
||
f := ctx.renderer
|
||
|
||
isPaddingLine := false
|
||
sectionConfig := t.config.Row
|
||
switch hctx.position {
|
||
case tw.Header:
|
||
sectionConfig = t.config.Header
|
||
isPaddingLine = (hctx.lineIdx == -1 && sectionConfig.Padding.Global.Top != tw.Empty) ||
|
||
(hctx.lineIdx == len(ctx.headerLines) && sectionConfig.Padding.Global.Bottom != tw.Empty)
|
||
case tw.Footer:
|
||
sectionConfig = t.config.Footer
|
||
isPaddingLine = (hctx.lineIdx == -1 && sectionConfig.Padding.Global.Top != tw.Empty) ||
|
||
(hctx.lineIdx == len(ctx.footerLines) && (sectionConfig.Padding.Global.Bottom != tw.Empty || t.hasPerColumnBottomPadding()))
|
||
case tw.Row:
|
||
if hctx.rowIdx >= 0 && hctx.rowIdx < len(ctx.rowLines) {
|
||
isPaddingLine = (hctx.lineIdx == -1 && sectionConfig.Padding.Global.Top != tw.Empty) ||
|
||
(hctx.lineIdx == len(ctx.rowLines[hctx.rowIdx]) && sectionConfig.Padding.Global.Bottom != tw.Empty)
|
||
}
|
||
}
|
||
|
||
sectionWidths := ctx.widths[hctx.position]
|
||
normalizedWidths := ctx.widths[tw.Row]
|
||
|
||
formatting := tw.Formatting{
|
||
Row: tw.RowContext{
|
||
Widths: sectionWidths,
|
||
ColMaxWidths: t.getColMaxWidths(hctx.position),
|
||
Current: resp.cells,
|
||
Previous: resp.prevCells,
|
||
Next: resp.nextCells,
|
||
Position: hctx.position,
|
||
Location: hctx.location,
|
||
},
|
||
Level: t.getLevel(hctx.position),
|
||
IsSubRow: hctx.lineIdx > 0 || isPaddingLine,
|
||
NormalizedWidths: normalizedWidths,
|
||
}
|
||
|
||
if hctx.position == tw.Row {
|
||
formatting.HasFooter = len(ctx.footerLines) > 0
|
||
}
|
||
|
||
switch hctx.position {
|
||
case tw.Header:
|
||
f.Header([][]string{hctx.line}, formatting)
|
||
case tw.Row:
|
||
f.Row(hctx.line, formatting)
|
||
case tw.Footer:
|
||
f.Footer([][]string{hctx.line}, formatting)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// renderPadding renders padding lines for a section.
|
||
// Parameters include ctx, mctx, hctx, and padChar for padding content.
|
||
// Returns an error if rendering fails.
|
||
func (t *Table) renderPadding(ctx *renderContext, mctx *mergeContext, hctx *helperContext, padChar string) error {
|
||
ctx.logger.Debug("Rendering padding line for %s (using char like '%s')", hctx.position, padChar)
|
||
|
||
colAligns := t.buildAligns(t.config.Row)
|
||
colPadding := t.buildPadding(t.config.Row.Padding)
|
||
|
||
switch hctx.position {
|
||
case tw.Header:
|
||
colAligns = t.buildAligns(t.config.Header)
|
||
colPadding = t.buildPadding(t.config.Header.Padding)
|
||
case tw.Footer:
|
||
colAligns = t.buildAligns(t.config.Footer)
|
||
colPadding = t.buildPadding(t.config.Footer.Padding)
|
||
}
|
||
|
||
return t.renderLine(ctx, mctx, hctx, colAligns, colPadding)
|
||
}
|
||
|
||
// renderRow renders the table's row section with borders and padding.
|
||
// Parameters ctx and mctx hold rendering and merge state.
|
||
// Returns an error if rendering fails.
|
||
func (t *Table) renderRow(ctx *renderContext, mctx *mergeContext) error {
|
||
if len(ctx.rowLines) == 0 {
|
||
return nil
|
||
}
|
||
ctx.logger.Debugf("Rendering row section (total rows: %d)", len(ctx.rowLines))
|
||
|
||
f := ctx.renderer
|
||
cfg := ctx.cfg
|
||
colAligns := t.buildAligns(t.config.Row)
|
||
colPadding := t.buildPadding(t.config.Row.Padding)
|
||
hctx := &helperContext{position: tw.Row}
|
||
|
||
footerIsEmptyOrNonExistent := !t.hasFooterElements()
|
||
if len(ctx.headerLines) == 0 && footerIsEmptyOrNonExistent && cfg.Borders.Top.Enabled() && cfg.Settings.Lines.ShowTop.Enabled() {
|
||
ctx.logger.Debug("Rendering table top border (rows only table)")
|
||
nextCells := make(map[int]tw.CellContext)
|
||
if len(ctx.rowLines) > 0 && len(ctx.rowLines[0]) > 0 && len(mctx.rowMerges) > 0 {
|
||
firstLine := ctx.rowLines[0][0]
|
||
firstMerges := mctx.rowMerges[0]
|
||
for j, cell := range padLine(firstLine, ctx.numCols) {
|
||
mergeState := tw.MergeState{}
|
||
if firstMerges != nil {
|
||
mergeState = firstMerges[j]
|
||
}
|
||
nextCells[j] = tw.CellContext{Data: cell, Merge: mergeState, Width: ctx.widths[tw.Row].Get(j)}
|
||
}
|
||
}
|
||
f.Line(tw.Formatting{
|
||
Row: tw.RowContext{
|
||
Widths: ctx.widths[tw.Row],
|
||
Next: nextCells,
|
||
Position: tw.Row,
|
||
Location: tw.LocationFirst,
|
||
},
|
||
Level: tw.LevelHeader,
|
||
IsSubRow: false,
|
||
})
|
||
}
|
||
|
||
for i, lines := range ctx.rowLines {
|
||
rowHasTopPadding := t.config.Row.Padding.Global.Top != tw.Empty
|
||
if rowHasTopPadding {
|
||
hctx.rowIdx = i
|
||
hctx.lineIdx = -1
|
||
if i == 0 && len(ctx.headerLines) == 0 {
|
||
hctx.location = tw.LocationFirst
|
||
} else {
|
||
hctx.location = tw.LocationMiddle
|
||
}
|
||
hctx.line = t.buildPaddingLineContents(t.config.Row.Padding.Global.Top, ctx.widths[tw.Row], ctx.numCols, mctx.rowMerges[i])
|
||
ctx.logger.Debug("Calling renderPadding for Row Top Padding (row %d): %v (loc: %v)", i, hctx.line, hctx.location)
|
||
if err := t.renderPadding(ctx, mctx, hctx, t.config.Row.Padding.Global.Top); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
footerExists := t.hasFooterElements()
|
||
rowHasBottomPadding := t.config.Row.Padding.Global.Bottom != tw.Empty
|
||
isLastRow := i == len(ctx.rowLines)-1
|
||
|
||
for j, visualLineData := range lines {
|
||
hctx.rowIdx = i
|
||
hctx.lineIdx = j
|
||
hctx.line = padLine(visualLineData, ctx.numCols)
|
||
|
||
if t.config.Behavior.TrimLine.Enabled() {
|
||
if j > 0 {
|
||
visualLineHasActualContent := false
|
||
for kCellIdx, cellContentInVisualLine := range hctx.line {
|
||
if t.Trimmer(cellContentInVisualLine) != "" {
|
||
visualLineHasActualContent = true
|
||
ctx.logger.Debug("Visual line [%d][%d] has content in cell %d: '%s'. Not skipping.", i, j, kCellIdx, cellContentInVisualLine)
|
||
break
|
||
}
|
||
}
|
||
|
||
if !visualLineHasActualContent {
|
||
ctx.logger.Debug("Skipping visual line [%d][%d] as it's entirely blank after trimming. Line: %q", i, j, hctx.line)
|
||
continue
|
||
}
|
||
}
|
||
}
|
||
|
||
isFirstRow := i == 0
|
||
isLastLineOfRow := j == len(lines)-1
|
||
|
||
if isFirstRow && j == 0 && !rowHasTopPadding && len(ctx.headerLines) == 0 {
|
||
hctx.location = tw.LocationFirst
|
||
} else if isLastRow && isLastLineOfRow && !rowHasBottomPadding && !footerExists {
|
||
hctx.location = tw.LocationEnd
|
||
} else {
|
||
hctx.location = tw.LocationMiddle
|
||
}
|
||
|
||
ctx.logger.Debugf("Rendering row %d line %d with location %v. Content: %q", i, j, hctx.location, hctx.line)
|
||
if err := t.renderLine(ctx, mctx, hctx, colAligns, colPadding); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
if rowHasBottomPadding {
|
||
hctx.rowIdx = i
|
||
hctx.lineIdx = len(lines)
|
||
if isLastRow && !footerExists {
|
||
hctx.location = tw.LocationEnd
|
||
} else {
|
||
hctx.location = tw.LocationMiddle
|
||
}
|
||
hctx.line = t.buildPaddingLineContents(t.config.Row.Padding.Global.Bottom, ctx.widths[tw.Row], ctx.numCols, mctx.rowMerges[i])
|
||
ctx.logger.Debug("Calling renderPadding for Row Bottom Padding (row %d): %v (loc: %v)", i, hctx.line, hctx.location)
|
||
if err := t.renderPadding(ctx, mctx, hctx, t.config.Row.Padding.Global.Bottom); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
if cfg.Settings.Separators.BetweenRows.Enabled() && !isLastRow {
|
||
ctx.logger.Debug("Rendering between-rows separator after logical row %d", i)
|
||
respCurrent := t.buildCellContexts(ctx, mctx, hctx, colAligns, colPadding)
|
||
|
||
var nextCellsForSeparator map[int]tw.CellContext = nil
|
||
nextRowIdx := i + 1
|
||
if nextRowIdx < len(ctx.rowLines) && nextRowIdx < len(mctx.rowMerges) {
|
||
hctxNext := &helperContext{position: tw.Row, rowIdx: nextRowIdx, location: tw.LocationMiddle}
|
||
nextRowActualLines := ctx.rowLines[nextRowIdx]
|
||
nextRowMerges := mctx.rowMerges[nextRowIdx]
|
||
|
||
if t.config.Row.Padding.Global.Top != tw.Empty {
|
||
hctxNext.lineIdx = -1
|
||
hctxNext.line = t.buildPaddingLineContents(t.config.Row.Padding.Global.Top, ctx.widths[tw.Row], ctx.numCols, nextRowMerges)
|
||
} else if len(nextRowActualLines) > 0 {
|
||
hctxNext.lineIdx = 0
|
||
hctxNext.line = padLine(nextRowActualLines[0], ctx.numCols)
|
||
} else {
|
||
hctxNext.lineIdx = 0
|
||
hctxNext.line = make([]string, ctx.numCols)
|
||
}
|
||
respNext := t.buildCellContexts(ctx, mctx, hctxNext, colAligns, colPadding)
|
||
nextCellsForSeparator = respNext.cells
|
||
} else {
|
||
ctx.logger.Debug("Separator context: No next logical row for separator after row %d.", i)
|
||
}
|
||
|
||
f.Line(tw.Formatting{
|
||
Row: tw.RowContext{
|
||
Widths: ctx.widths[tw.Row],
|
||
Current: respCurrent.cells,
|
||
Previous: respCurrent.prevCells,
|
||
Next: nextCellsForSeparator,
|
||
Position: tw.Row,
|
||
Location: tw.LocationMiddle,
|
||
ColMaxWidths: t.getColMaxWidths(tw.Row),
|
||
},
|
||
Level: tw.LevelBody,
|
||
IsSubRow: false,
|
||
HasFooter: footerExists,
|
||
})
|
||
}
|
||
}
|
||
return nil
|
||
}
|