build(deps): bump github.com/olekukonko/tablewriter from 1.0.9 to 1.1.0

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>
This commit is contained in:
dependabot[bot]
2025-09-29 18:29:04 +00:00
committed by Ralf Haferkamp
parent d05d5bdc6f
commit f4eaa8bd5b
25 changed files with 387 additions and 283 deletions

55
vendor/github.com/olekukonko/tablewriter/.golangci.yml generated vendored Normal file
View File

@@ -0,0 +1,55 @@
# See for configurations: https://golangci-lint.run/usage/configuration/
version: 2
# See: https://golangci-lint.run/usage/formatters/
formatters:
default: none
enable:
- gofmt # https://pkg.go.dev/cmd/gofmt
- gofumpt # https://github.com/mvdan/gofumpt
settings:
gofmt:
simplify: true # Simplify code: gofmt with `-s` option.
gofumpt:
# Module path which contains the source code being formatted.
# Default: ""
module-path: github.com/olekukonko/tablewriter # Should match with module in go.mod
# Choose whether to use the extra rules.
# Default: false
extra-rules: true
# See: https://golangci-lint.run/usage/linters/
linters:
default: none
enable:
- staticcheck
- govet
- gocritic
# - unused # TODO: There are many unused functions, should I directly remove those ?
- ineffassign
- unconvert
- mirror
- usestdlibvars
- loggercheck
- exptostd
- godot
- perfsprint
# See: https://golangci-lint.run/usage/false-positives/
exclusion:
# paths:
# rules:
settings:
staticcheck:
checks:
- all
- "-SA1019" # disabled because it warns about deprecated: kept for compatibility will be removed soon
- "-ST1019" # disabled because it warns about deprecated: kept for compatibility will be removed soon
- "-ST1021" # disabled because it warns to have comment on exported packages
- "-ST1000" # disabled because it warns to have comment on exported functions
- "-ST1020" # disabled because it warns to have at least one file in a package should have a package comment
godot:
period: false

View File

@@ -28,7 +28,7 @@ go get github.com/olekukonko/tablewriter@v0.0.5
#### Latest Version
The latest stable version
```bash
go get github.com/olekukonko/tablewriter@v1.0.9
go get github.com/olekukonko/tablewriter@v1.1.0
```
**Warning:** Version `v1.0.0` contains missing functionality and should not be used.
@@ -62,7 +62,7 @@ func main() {
data := [][]string{
{"Package", "Version", "Status"},
{"tablewriter", "v0.0.5", "legacy"},
{"tablewriter", "v1.0.9", "latest"},
{"tablewriter", "v1.1.0", "latest"},
}
table := tablewriter.NewWriter(os.Stdout)
@@ -77,7 +77,7 @@ func main() {
│ PACKAGE │ VERSION │ STATUS │
├─────────────┼─────────┼────────┤
│ tablewriter │ v0.0.5 │ legacy │
│ tablewriter │ v1.0.9 │ latest │
│ tablewriter │ v1.1.0 │ latest │
└─────────────┴─────────┴────────┘
```
@@ -1081,6 +1081,8 @@ func (t Time) Format() string {
- `AutoFormat` changes See [#261](https://github.com/olekukonko/tablewriter/issues/261)
## What is new
- `Counting` changes See [#294](https://github.com/olekukonko/tablewriter/issues/294)
## Command-Line Tool

View File

@@ -14,6 +14,7 @@ type Config struct {
Stream tw.StreamConfig
Behavior tw.Behavior
Widths tw.CellWidth
Counter tw.Counter
}
// ConfigBuilder provides a fluent interface for building Config
@@ -199,11 +200,7 @@ func (b *ConfigBuilder) WithHeaderMergeMode(mergeMode int) *ConfigBuilder {
// WithMaxWidth sets the maximum width for the entire table (0 means unlimited).
// Negative values are treated as 0.
func (b *ConfigBuilder) WithMaxWidth(width int) *ConfigBuilder {
if width < 0 {
b.config.MaxWidth = 0
} else {
b.config.MaxWidth = width
}
b.config.MaxWidth = max(width, 0)
return b
}

View File

@@ -1,11 +1,12 @@
package tablewriter
import (
"reflect"
"github.com/mattn/go-runewidth"
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/tablewriter/tw"
"reflect"
)
// Option defines a function type for configuring a Table instance.
@@ -498,6 +499,17 @@ func WithTrimSpace(state tw.State) Option {
}
}
// WithTrimLine sets whether empty visual lines within a cell are trimmed.
// Logs the change if debugging is enabled.
func WithTrimLine(state tw.State) Option {
return func(target *Table) {
target.config.Behavior.TrimLine = state
if target.logger != nil {
target.logger.Debugf("Option: WithTrimLine applied to Table: %v", state)
}
}
}
// WithHeaderAutoFormat enables or disables automatic formatting for header cells.
// Logs the change if debugging is enabled.
func WithHeaderAutoFormat(state tw.State) Option {
@@ -607,10 +619,8 @@ func WithRendition(rendition tw.Rendition) Option {
if target.logger != nil {
target.logger.Debugf("Option: WithRendition: Applied to renderer via Renditioning.SetRendition(): %+v", rendition)
}
} else {
if target.logger != nil {
target.logger.Warnf("Option: WithRendition: Current renderer type %T does not implement tw.Renditioning. Rendition may not be applied as expected.", target.renderer)
}
} else if target.logger != nil {
target.logger.Warnf("Option: WithRendition: Current renderer type %T does not implement tw.Renditioning. Rendition may not be applied as expected.", target.renderer)
}
}
}
@@ -654,15 +664,38 @@ func WithSymbols(symbols tw.Symbols) Option {
if target.logger != nil {
target.logger.Debugf("Option: WithRendition: Applied to renderer via Renditioning.SetRendition(): %+v", cfg)
}
} else {
if target.logger != nil {
target.logger.Warnf("Option: WithRendition: Current renderer type %T does not implement tw.Renditioning. Rendition may not be applied as expected.", target.renderer)
}
} else if target.logger != nil {
target.logger.Warnf("Option: WithRendition: Current renderer type %T does not implement tw.Renditioning. Rendition may not be applied as expected.", target.renderer)
}
}
}
}
// WithCounters enables line counting by wrapping the table's writer.
// If a custom counter (that implements tw.Counter) is provided, it will be used.
// If the provided counter is nil, a default tw.LineCounter will be used.
// The final count can be retrieved via the table.Lines() method after Render() is called.
func WithCounters(counters ...tw.Counter) Option {
return func(target *Table) {
// Iterate through the provided counters and add any non-nil ones.
for _, c := range counters {
if c != nil {
target.counters = append(target.counters, c)
}
}
}
}
// WithLineCounter enables the default line counter.
// A new instance of tw.LineCounter is added to the table's list of counters.
// The total count can be retrieved via the table.Lines() method after Render() is called.
func WithLineCounter() Option {
return func(target *Table) {
// Important: Create a new instance so tables don't share counters.
target.counters = append(target.counters, &tw.LineCounter{})
}
}
// defaultConfig returns a default Config with sensible settings for headers, rows, footers, and behavior.
func defaultConfig() Config {
return Config{
@@ -717,6 +750,7 @@ func defaultConfig() Config {
Behavior: tw.Behavior{
AutoHide: tw.Off,
TrimSpace: tw.On,
TrimLine: tw.On,
Structs: tw.Struct{
AutoHeader: tw.Off,
Tags: []string{"json", "db"},

View File

@@ -2,10 +2,11 @@ package twwidth
import (
"bytes"
"github.com/mattn/go-runewidth"
"regexp"
"strings"
"sync"
"github.com/mattn/go-runewidth"
)
// condition holds the global runewidth configuration, including East Asian width settings.
@@ -35,15 +36,15 @@ var widthCache map[cacheKey]int
// including CSI (Control Sequence Introducer) and OSC (Operating System Command) sequences.
// The returned regex can be used to strip ANSI codes from strings.
func Filter() *regexp.Regexp {
var regESC = "\x1b" // ASCII escape character
var regBEL = "\x07" // ASCII bell character
regESC := "\x1b" // ASCII escape character
regBEL := "\x07" // ASCII bell character
// ANSI string terminator: either ESC+\ or BEL
var regST = "(" + regexp.QuoteMeta(regESC+"\\") + "|" + regexp.QuoteMeta(regBEL) + ")"
regST := "(" + regexp.QuoteMeta(regESC+"\\") + "|" + regexp.QuoteMeta(regBEL) + ")"
// Control Sequence Introducer (CSI): ESC[ followed by parameters and a final byte
var regCSI = regexp.QuoteMeta(regESC+"[") + "[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]"
regCSI := regexp.QuoteMeta(regESC+"[") + "[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]"
// Operating System Command (OSC): ESC] followed by arbitrary content until a terminator
var regOSC = regexp.QuoteMeta(regESC+"]") + ".*?" + regST
regOSC := regexp.QuoteMeta(regESC+"]") + ".*?" + regST
// Combine CSI and OSC patterns into a single regex
return regexp.MustCompile("(" + regCSI + "|" + regOSC + ")")
@@ -257,11 +258,12 @@ func Truncate(s string, maxWidth int, suffix ...string) string {
terminated := false
if seqLen >= 2 {
introducer := seqBytes[1]
if introducer == '[' {
switch introducer {
case '[':
if seqLen >= 3 && r >= 0x40 && r <= 0x7E {
terminated = true
}
} else if introducer == ']' {
case ']':
if r == '\x07' {
terminated = true
} else if seqLen > 1 && seqBytes[seqLen-2] == '\x1b' && r == '\\' { // Check for ST: \x1b\

View File

@@ -1,11 +1,12 @@
package renderer
import (
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"io"
"strings"
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/tablewriter/tw"
)
@@ -283,7 +284,7 @@ func (f *Blueprint) formatCell(content string, width int, padding tw.Padding, al
leftPadChar := padding.Left
rightPadChar := padding.Right
//if f.config.Settings.Cushion.Enabled() || f.config.Settings.Cushion.Default() {
// if f.config.Settings.Cushion.Enabled() || f.config.Settings.Cushion.Default() {
// if leftPadChar == tw.Empty {
// leftPadChar = tw.Space
// }
@@ -297,10 +298,7 @@ func (f *Blueprint) formatCell(content string, width int, padding tw.Padding, al
padRightWidth := twwidth.Width(rightPadChar)
// Calculate available width for content
availableContentWidth := width - padLeftWidth - padRightWidth
if availableContentWidth < 0 {
availableContentWidth = 0
}
availableContentWidth := max(width-padLeftWidth-padRightWidth, 0)
f.logger.Debugf("Available content width: %d", availableContentWidth)
// Truncate content if it exceeds available width
@@ -311,10 +309,7 @@ func (f *Blueprint) formatCell(content string, width int, padding tw.Padding, al
}
// Calculate total padding needed
totalPaddingWidth := width - runeWidth
if totalPaddingWidth < 0 {
totalPaddingWidth = 0
}
totalPaddingWidth := max(width-runeWidth, 0)
f.logger.Debugf("Total padding width: %d", totalPaddingWidth)
var result strings.Builder
@@ -435,7 +430,7 @@ func (f *Blueprint) renderLine(ctx tw.Formatting) {
prevWidth := ctx.Row.Widths.Get(colIndex - 1)
prevCellCtx, prevOk := ctx.Row.Current[colIndex-1]
prevIsHMergeEnd := prevOk && prevCellCtx.Merge.Horizontal.Present && prevCellCtx.Merge.Horizontal.End
if (prevWidth > 0 || prevIsHMergeEnd) && (!ok || !(cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start)) {
if (prevWidth > 0 || prevIsHMergeEnd) && (!ok || (!cellCtx.Merge.Horizontal.Present || cellCtx.Merge.Horizontal.Start)) {
shouldAddSeparator = true
}
}
@@ -454,10 +449,7 @@ func (f *Blueprint) renderLine(ctx tw.Formatting) {
if ctx.Row.Position == tw.Row {
dynamicTotalWidth := 0
for k := 0; k < span && colIndex+k < numCols; k++ {
normWidth := ctx.NormalizedWidths.Get(colIndex + k)
if normWidth < 0 {
normWidth = 0
}
normWidth := max(ctx.NormalizedWidths.Get(colIndex+k), 0)
dynamicTotalWidth += normWidth
if k > 0 && separatorDisplayWidth > 0 && ctx.NormalizedWidths.Get(colIndex+k) > 0 {
dynamicTotalWidth += separatorDisplayWidth
@@ -501,21 +493,24 @@ func (f *Blueprint) renderLine(ctx tw.Formatting) {
// Set cell padding and alignment
padding := cellCtx.Padding
align := cellCtx.Align
if align == tw.AlignNone {
if ctx.Row.Position == tw.Header {
switch align {
case tw.AlignNone:
switch ctx.Row.Position {
case tw.Header:
align = tw.AlignCenter
} else if ctx.Row.Position == tw.Footer {
case tw.Footer:
align = tw.AlignRight
} else {
default:
align = tw.AlignLeft
}
f.logger.Debugf("renderLine: col %d (data: '%s') using renderer default align '%s' for position %s.", colIndex, cellCtx.Data, align, ctx.Row.Position)
} else if align == tw.Skip {
if ctx.Row.Position == tw.Header {
case tw.Skip:
switch ctx.Row.Position {
case tw.Header:
align = tw.AlignCenter
} else if ctx.Row.Position == tw.Footer {
case tw.Footer:
align = tw.AlignRight
} else {
default:
align = tw.AlignLeft
}
f.logger.Debugf("renderLine: col %d (data: '%s') cellCtx.Align was Skip/empty, falling back to basic default '%s'.", colIndex, cellCtx.Data, align)
@@ -587,7 +582,6 @@ func (f *Blueprint) renderLine(ctx tw.Formatting) {
func (f *Blueprint) Rendition(config tw.Rendition) {
f.config = mergeRendition(f.config, config)
f.logger.Debugf("Blueprint.Rendition updated. New config: %+v", f.config)
}
// Ensure Blueprint implements tw.Renditioning

View File

@@ -1,12 +1,13 @@
package renderer
import (
"io"
"strings"
"github.com/fatih/color"
"github.com/olekukonko/ll"
"github.com/olekukonko/ll/lh"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"io"
"strings"
"github.com/olekukonko/tablewriter/tw"
)
@@ -378,22 +379,19 @@ func (c *Colorized) formatCell(content string, width int, padding tw.Padding, al
// Set default padding characters
padLeftCharStr := padding.Left
if padLeftCharStr == tw.Empty {
padLeftCharStr = tw.Space
}
// if padLeftCharStr == tw.Empty {
// padLeftCharStr = tw.Space
//}
padRightCharStr := padding.Right
if padRightCharStr == tw.Empty {
padRightCharStr = tw.Space
}
// if padRightCharStr == tw.Empty {
// padRightCharStr = tw.Space
//}
// Calculate padding widths
definedPadLeftWidth := twwidth.Width(padLeftCharStr)
definedPadRightWidth := twwidth.Width(padRightCharStr)
// Calculate available width for content and alignment
availableForContentAndAlign := width - definedPadLeftWidth - definedPadRightWidth
if availableForContentAndAlign < 0 {
availableForContentAndAlign = 0
}
availableForContentAndAlign := max(width-definedPadLeftWidth-definedPadRightWidth, 0)
// Truncate content if it exceeds available width
if contentVisualWidth > availableForContentAndAlign {
@@ -403,10 +401,7 @@ func (c *Colorized) formatCell(content string, width int, padding tw.Padding, al
}
// Calculate remaining space for alignment
remainingSpaceForAlignment := availableForContentAndAlign - contentVisualWidth
if remainingSpaceForAlignment < 0 {
remainingSpaceForAlignment = 0
}
remainingSpaceForAlignment := max(availableForContentAndAlign-contentVisualWidth, 0)
// Apply alignment padding
leftAlignmentPadSpaces := tw.Empty
@@ -539,7 +534,7 @@ func (c *Colorized) renderLine(ctx tw.Formatting, line []string, tint Tint) {
shouldAddSeparator := false
if i > 0 && c.config.Settings.Separators.BetweenColumns.Enabled() {
cellCtx, ok := ctx.Row.Current[i]
if !ok || !(cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start) {
if !ok || (!cellCtx.Merge.Horizontal.Present || cellCtx.Merge.Horizontal.Start) {
shouldAddSeparator = true
}
}
@@ -574,10 +569,7 @@ func (c *Colorized) renderLine(ctx tw.Formatting, line []string, tint Tint) {
dynamicTotalWidth := 0
for k := 0; k < span && i+k < numCols; k++ {
colToSum := i + k
normWidth := ctx.NormalizedWidths.Get(colToSum)
if normWidth < 0 {
normWidth = 0
}
normWidth := max(ctx.NormalizedWidths.Get(colToSum), 0)
dynamicTotalWidth += normWidth
if k > 0 && separatorDisplayWidth > 0 {
dynamicTotalWidth += separatorDisplayWidth
@@ -633,7 +625,7 @@ func (c *Colorized) renderLine(ctx tw.Formatting, line []string, tint Tint) {
}
// Override alignment for footer merges or TOTAL pattern
if (ctx.Row.Position == tw.Footer && isHMergeStart) || isTotalPattern {
if align != tw.AlignRight {
if align == tw.AlignNone {
c.logger.Debugf("renderLine: Applying AlignRight override for Footer HMerge/TOTAL pattern at col %d. Original/default align was: %s", i, align)
align = tw.AlignRight
}

View File

@@ -2,6 +2,7 @@ package renderer
import (
"fmt"
"github.com/fatih/color"
"github.com/olekukonko/tablewriter/tw"
)
@@ -82,7 +83,6 @@ func defaultColorized() ColorizedConfig {
// defaultOceanRendererConfig returns a base tw.Rendition for the Ocean renderer.
func defaultOceanRendererConfig() tw.Rendition {
return tw.Rendition{
Borders: tw.Border{
Left: tw.On, Right: tw.On, Top: tw.On, Bottom: tw.On,
@@ -190,7 +190,7 @@ func mergeSettings(defaults, overrides tw.Settings) tw.Settings {
defaults.CompactMode = overrides.CompactMode
}
//if overrides.Cushion != tw.Unknown {
// if overrides.Cushion != tw.Unknown {
// defaults.Cushion = overrides.Cushion
//}

View File

@@ -3,11 +3,12 @@ package renderer
import (
"errors"
"fmt"
"github.com/olekukonko/ll"
"html"
"io"
"strings"
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/tw"
)
@@ -82,7 +83,7 @@ func (h *HTML) Config() tw.Rendition {
}
// debugLog appends a formatted message to the debug trace if debugging is enabled.
//func (h *HTML) debugLog(format string, a ...interface{}) {
// func (h *HTML) debugLog(format string, a ...interface{}) {
// if h.debug {
// msg := fmt.Sprintf(format, a...)
// h.trace = append(h.trace, fmt.Sprintf("[HTML] %s", msg))

View File

@@ -1,11 +1,12 @@
package renderer
import (
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"io"
"strings"
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/tablewriter/tw"
)
@@ -95,7 +96,6 @@ func (m *Markdown) Row(row []string, ctx tw.Formatting) {
m.resolveAlignment(ctx)
m.logger.Debugf("Rendering row with data=%v, widths=%v, previous=%v, current=%v, next=%v", row, ctx.Row.Widths, ctx.Row.Previous, ctx.Row.Current, ctx.Row.Next)
m.renderMarkdownLine(row, ctx, false)
}
// Footer renders the Markdown table footer.
@@ -155,7 +155,7 @@ func (m *Markdown) resolveAlignment(ctx tw.Formatting) tw.Alignment {
// formatCell formats a Markdown cell's content with padding and alignment, ensuring at least 3 characters wide.
func (m *Markdown) formatCell(content string, width int, align tw.Align, padding tw.Padding) string {
//if m.config.Settings.TrimWhitespace.Enabled() {
// if m.config.Settings.TrimWhitespace.Enabled() {
// content = strings.TrimSpace(content)
//}
contentVisualWidth := twwidth.Width(content)
@@ -177,10 +177,7 @@ func (m *Markdown) formatCell(content string, width int, align tw.Align, padding
targetWidth := tw.Max(width, minWidth)
// Calculate padding
totalPaddingNeeded := targetWidth - contentVisualWidth
if totalPaddingNeeded < 0 {
totalPaddingNeeded = 0
}
totalPaddingNeeded := max(targetWidth-contentVisualWidth, 0)
var leftPadStr, rightPadStr string
switch align {
@@ -220,13 +217,14 @@ func (m *Markdown) formatCell(content string, width int, align tw.Align, padding
adjNeeded := targetWidth - finalWidth
if adjNeeded > 0 {
adjStr := strings.Repeat(tw.Space, adjNeeded)
if align == tw.AlignRight {
switch align {
case tw.AlignRight:
result = adjStr + result
} else if align == tw.AlignCenter {
case tw.AlignCenter:
leftAdj := adjNeeded / 2
rightAdj := adjNeeded - leftAdj
result = strings.Repeat(tw.Space, leftAdj) + result + strings.Repeat(tw.Space, rightAdj)
} else {
default:
result += adjStr
}
} else {
@@ -346,10 +344,7 @@ func (m *Markdown) renderMarkdownLine(line []string, ctx tw.Formatting, isHeader
span = cellCtx.Merge.Horizontal.Span
totalWidth := 0
for k := 0; k < span && colIndex+k < numCols; k++ {
colWidth := ctx.NormalizedWidths.Get(colIndex + k)
if colWidth < 0 {
colWidth = 0
}
colWidth := max(ctx.NormalizedWidths.Get(colIndex+k), 0)
totalWidth += colWidth
if k > 0 && separatorWidth > 0 {
totalWidth += separatorWidth
@@ -388,17 +383,18 @@ func (m *Markdown) renderMarkdownLine(line []string, ctx tw.Formatting, isHeader
}
// For rows, use the header's alignment if specified
rowAlign := align
if headerCellCtx, headerOK := ctx.Row.Previous[colIndex]; headerOK && isHeaderSep == false {
if headerCellCtx, headerOK := ctx.Row.Previous[colIndex]; headerOK && !isHeaderSep {
if headerCellCtx.Align != tw.AlignNone && headerCellCtx.Align != tw.Empty {
rowAlign = headerCellCtx.Align
}
}
if rowAlign == tw.AlignNone || rowAlign == tw.Empty {
if ctx.Row.Position == tw.Header {
switch ctx.Row.Position {
case tw.Header:
rowAlign = tw.AlignCenter
} else if ctx.Row.Position == tw.Footer {
case tw.Footer:
rowAlign = tw.AlignRight
} else {
default:
rowAlign = tw.AlignLeft
}
m.logger.Debugf("renderMarkdownLine: Col %d using default align '%s'", colIndex, rowAlign)

View File

@@ -1,17 +1,18 @@
package renderer
import (
"github.com/olekukonko/tablewriter/pkg/twwidth"
"io"
"slices"
"strings"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/tw"
)
// OceanConfig defines configuration specific to the Ocean renderer.
type OceanConfig struct {
}
type OceanConfig struct{}
// Ocean is a streaming table renderer that writes ASCII tables.
type Ocean struct {
@@ -327,12 +328,9 @@ func (o *Ocean) renderContentLine(ctx tw.Formatting, lineData []string) {
idxInMergeSpan := colIdx + k
// Check if idxInMergeSpan is a defined column in fixedWidths
foundInFixedWidths := false
for _, sortedCIdx_inner := range sortedColIndices {
if sortedCIdx_inner == idxInMergeSpan {
currentMergeTotalRenderWidth += o.fixedWidths.Get(idxInMergeSpan)
foundInFixedWidths = true
break
}
if slices.Contains(sortedColIndices, idxInMergeSpan) {
currentMergeTotalRenderWidth += o.fixedWidths.Get(idxInMergeSpan)
foundInFixedWidths = true
}
if !foundInFixedWidths && idxInMergeSpan <= sortedColIndices[len(sortedColIndices)-1] {
o.logger.Debugf("Col %d in HMerge span not found in fixedWidths, assuming 0-width contribution.", idxInMergeSpan)
@@ -407,20 +405,14 @@ func (o *Ocean) formatCellContent(content string, cellVisualWidth int, padding t
padLeftDisplayWidth := twwidth.Width(padLeftChar)
padRightDisplayWidth := twwidth.Width(padRightChar)
spaceForContentAndAlignment := cellVisualWidth - padLeftDisplayWidth - padRightDisplayWidth
if spaceForContentAndAlignment < 0 {
spaceForContentAndAlignment = 0
}
spaceForContentAndAlignment := max(cellVisualWidth-padLeftDisplayWidth-padRightDisplayWidth, 0)
if contentDisplayWidth > spaceForContentAndAlignment {
content = twwidth.Truncate(content, spaceForContentAndAlignment)
contentDisplayWidth = twwidth.Width(content)
}
remainingSpace := spaceForContentAndAlignment - contentDisplayWidth
if remainingSpace < 0 {
remainingSpace = 0
}
remainingSpace := max(spaceForContentAndAlignment-contentDisplayWidth, 0)
var PL, PR string
switch align {

View File

@@ -2,11 +2,12 @@ package renderer
import (
"fmt"
"github.com/olekukonko/ll"
"html"
"io"
"strings"
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/tw"
)
@@ -512,10 +513,7 @@ func (s *SVG) renderVisualLine(visualLineData []string, ctx tw.Formatting, posit
}
s.dataRowCounter++
} else {
parentDataRowStripeIndex := s.dataRowCounter - 1
if parentDataRowStripeIndex < 0 {
parentDataRowStripeIndex = 0
}
parentDataRowStripeIndex := max(s.dataRowCounter-1, 0)
if s.config.RowAltBG != tw.Empty && parentDataRowStripeIndex%2 != 0 {
bgColor = s.config.RowAltBG
} else {
@@ -623,9 +621,10 @@ func (s *SVG) renderVisualLine(visualLineData []string, ctx tw.Formatting, posit
}
}
textX := currentX + s.config.Padding
if cellTextAnchor == "middle" {
switch cellTextAnchor {
case "middle":
textX = currentX + s.config.Padding + (rectWidth-2*s.config.Padding)/2.0
} else if cellTextAnchor == "end" {
case "end":
textX = currentX + rectWidth - s.config.Padding
}
textY := s.currentY + rectHeight/2.0
@@ -686,7 +685,7 @@ func (s *SVG) Start(w io.Writer) error {
func (s *SVG) debug(format string, a ...interface{}) {
if s.config.Debug {
msg := fmt.Sprintf(format, a...)
s.trace = append(s.trace, fmt.Sprintf("[SVG] %s", msg))
s.trace = append(s.trace, "[SVG] "+msg)
}
}

View File

@@ -1,10 +1,11 @@
package tablewriter
import (
"math"
"github.com/olekukonko/errors"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/tablewriter/tw"
"math"
)
// Close finalizes the table stream.
@@ -92,8 +93,8 @@ func (t *Table) Start() error {
return errors.Newf("renderer does not support streaming")
}
//t.renderer.Start(t.writer)
//t.renderer.Logger(t.logger)
// t.renderer.Start(t.writer)
// t.renderer.Logger(t.logger)
if t.hasPrinted {
// Prevent calling Start() multiple times on the same stream instance.
@@ -119,7 +120,7 @@ func (t *Table) Start() error {
t.streamWidths = t.config.Widths.PerColumn.Clone()
// Determine numCols from the highest index in PerColumn map
maxColIdx := -1
t.streamWidths.Each(func(col int, width int) {
t.streamWidths.Each(func(col, width int) {
if col > maxColIdx {
maxColIdx = col
}
@@ -331,7 +332,7 @@ func (t *Table) streamAppendRow(row interface{}) error {
t.logger.Debug("streamAppendRow: Separator line rendered. Updated lastRenderedPosition to 'separator'.")
} else {
details := ""
if !(shouldDrawHeaderRowSeparator || shouldDrawRowRowSeparator) {
if !shouldDrawHeaderRowSeparator && !shouldDrawRowRowSeparator {
details = "neither header/row nor row/row separator was flagged true"
} else if t.lastRenderedPosition == tw.Position("separator") {
details = "lastRenderedPosition is already 'separator'"
@@ -475,7 +476,7 @@ func (t *Table) streamCalculateWidths(sampling []string, config tw.CellConfig) i
determinedNumCols := 0
if t.config.Widths.PerColumn != nil && t.config.Widths.PerColumn.Len() > 0 {
maxColIdx := -1
t.config.Widths.PerColumn.Each(func(col int, width int) {
t.config.Widths.PerColumn.Each(func(col, width int) {
if col > maxColIdx {
maxColIdx = col
}
@@ -600,7 +601,7 @@ func (t *Table) streamCalculateWidths(sampling []string, config tw.CellConfig) i
if t.config.Widths.Global > 0 && t.streamNumCols > 0 {
t.logger.Debug("streamCalculateWidths: Applying global stream width constraint %d", t.config.Widths.Global)
currentTotalColumnWidthsSum := 0
t.streamWidths.Each(func(_ int, w int) {
t.streamWidths.Each(func(_, w int) {
currentTotalColumnWidthsSum += w
})
@@ -665,7 +666,7 @@ func (t *Table) streamCalculateWidths(sampling []string, config tw.CellConfig) i
// Distribute remainingSpace (positive or negative) among non-zero width columns
if remainingSpace != 0 && t.streamNumCols > 0 {
colsToAdjust := []int{}
t.streamWidths.Each(func(col int, w int) {
t.streamWidths.Each(func(col, w int) {
if w > 0 { // Only consider columns that currently have width
colsToAdjust = append(colsToAdjust, col)
}
@@ -689,7 +690,7 @@ func (t *Table) streamCalculateWidths(sampling []string, config tw.CellConfig) i
}
// Final sanitization
t.streamWidths.Each(func(col int, width int) {
t.streamWidths.Each(func(col, width int) {
if width < 0 {
t.streamWidths.Set(col, 0)
}
@@ -858,7 +859,7 @@ func (t *Table) streamRenderFooter(processedFooterLines [][]string) error {
// If this is the last line of the last content block (footer), and no bottom border will be drawn,
// its Location should be End.
isLastLineOfTableContent := (i == totalFooterLines-1) &&
!(cfg.Borders.Bottom.Enabled() && cfg.Settings.Lines.ShowBottom.Enabled())
(!cfg.Borders.Bottom.Enabled() || !cfg.Settings.Lines.ShowBottom.Enabled())
if isLastLineOfTableContent {
resp.location = tw.LocationEnd
t.logger.Debug("streamRenderFooter: Setting LocationEnd for last footer line as no bottom border will follow.")

View File

@@ -2,13 +2,6 @@ package tablewriter
import (
"bytes"
"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"
"io"
"math"
"os"
@@ -16,11 +9,20 @@ import (
"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
@@ -410,7 +412,6 @@ func (t *Table) Footer(elements ...any) {
// 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))
@@ -423,7 +424,7 @@ func (t *Table) Options(opts ...Option) *Table {
// force debugging mode if set
// This should be move away form WithDebug
if t.config.Debug == true {
if t.config.Debug {
t.logger.Enable()
t.logger.Resume()
} else {
@@ -510,6 +511,28 @@ 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 Tables 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
@@ -945,10 +968,7 @@ func (t *Table) prepareContent(cells []string, config tw.CellConfig) [][]string
currentLine := line
breakCharWidth := twwidth.Width(tw.CharBreak)
for twwidth.Width(currentLine) > effectiveContentMaxWidth {
targetWidth := effectiveContentMaxWidth - breakCharWidth
if targetWidth < 0 {
targetWidth = 0
}
targetWidth := max(effectiveContentMaxWidth-breakCharWidth, 0)
breakPoint := tw.BreakPoint(currentLine, targetWidth)
runes := []rune(currentLine)
if breakPoint <= 0 || breakPoint > len(runes) {
@@ -1362,23 +1382,40 @@ func (t *Table) prepareWithMerges(content [][]string, config tw.CellConfig, posi
// 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 numCols for THIS batch render pass
t.batchRenderNumCols = t.maxColumns() // Calculate ONCE
t.isBatchRenderNumColsSet = true // Mark the cache as active for this render pass
t.logger.Debugf("Render(): Set batchRenderNumCols to %d and isBatchRenderNumColsSet to true.", t.batchRenderNumCols)
// 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.batchRenderNumCols = 0; // Optional: reset to 0, or leave as is.
// Since isBatchRenderNumColsSet is false, its value won't be used by getNumColsToUse.
t.logger.Debugf("Render(): Cleared isBatchRenderNumColsSet to false (batchRenderNumCols was %d).", t.batchRenderNumCols)
}()
@@ -1387,9 +1424,10 @@ func (t *Table) render() error {
(t.caption.Spot >= tw.SpotTopLeft && t.caption.Spot <= tw.SpotBottomRight)
var tableStringBuffer *strings.Builder
targetWriter := t.writer
originalWriter := t.writer // Save original writer for restoration if needed
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
@@ -1398,17 +1436,15 @@ func (t *Table) render() error {
t.logger.Debugf("No caption detected. Rendering table core directly to writer.")
}
//Render Table Core
// 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.writer = originalWriter
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.writer = originalWriter
t.logger.Errorf("Renderer Start() error: %v", err)
return errors.Newf("renderer start failed").Wrap(err)
}
@@ -1440,18 +1476,21 @@ func (t *Table) render() error {
renderError = true
}
t.writer = originalWriter // Restore original writer
// 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 // Return error from core rendering if any
return firstRenderErr
}
//Caption Handling & Final Output ---
// Caption Handling & Final Output
if isTopOrBottomCaption {
renderedTableContent := tableStringBuffer.String()
t.logger.Debugf("[Render] Table core buffer length: %d", len(renderedTableContent))
// Check if the buffer is empty AND borders are enabled
// 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
@@ -1503,7 +1542,7 @@ func (t *Table) render() error {
t.hasPrinted = true
t.logger.Info("Render() completed.")
return nil // Success
return nil
}
// renderFooter renders the table's footer section with borders and padding.
@@ -1677,7 +1716,7 @@ func (t *Table) renderFooter(ctx *renderContext, mctx *mergeContext) error {
if hasTopPadding {
hctx.rowIdx = 0
hctx.lineIdx = -1
if !(hasContentAbove && cfg.Settings.Lines.ShowFooterLine.Enabled()) {
if !hasContentAbove || !cfg.Settings.Lines.ShowFooterLine.Enabled() {
hctx.location = tw.LocationFirst
} else {
hctx.location = tw.LocationMiddle
@@ -1699,7 +1738,7 @@ func (t *Table) renderFooter(ctx *renderContext, mctx *mergeContext) error {
hctx.line = padLine(line, ctx.numCols)
isFirstContentLine := i == 0
isLastContentLine := i == len(ctx.footerLines)-1
if isFirstContentLine && !hasTopPadding && !(hasContentAbove && cfg.Settings.Lines.ShowFooterLine.Enabled()) {
if isFirstContentLine && !hasTopPadding && (!hasContentAbove || !cfg.Settings.Lines.ShowFooterLine.Enabled()) {
hctx.location = tw.LocationFirst
} else if isLastContentLine && !hasBottomPaddingConfig {
hctx.location = tw.LocationEnd
@@ -1716,7 +1755,7 @@ func (t *Table) renderFooter(ctx *renderContext, mctx *mergeContext) error {
if hasBottomPaddingConfig {
paddingLineContentForContext = make([]string, ctx.numCols)
formattedPaddingCells := make([]string, ctx.numCols)
var representativePadChar string = " "
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)
@@ -1741,10 +1780,7 @@ func (t *Table) renderFooter(ctx *renderContext, mctx *mergeContext) error {
if j == 0 || representativePadChar == " " {
representativePadChar = padChar
}
padWidth := twwidth.Width(padChar)
if padWidth < 1 {
padWidth = 1
}
padWidth := max(twwidth.Width(padChar), 1)
repeatCount := 0
if colWd > 0 && padWidth > 0 {
repeatCount = colWd / padWidth
@@ -2139,19 +2175,21 @@ func (t *Table) renderRow(ctx *renderContext, mctx *mergeContext) error {
hctx.lineIdx = j
hctx.line = padLine(visualLineData, ctx.numCols)
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 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
if !visualLineHasActualContent {
ctx.logger.Debug("Skipping visual line [%d][%d] as it's entirely blank after trimming. Line: %q", i, j, hctx.line)
continue
}
}
}

View File

@@ -12,7 +12,6 @@ type CellFormatting struct {
// Deprecated: kept for compatibility
// will be removed soon
Alignment Align // Text alignment within the cell (e.g., Left, Right, Center)
}
// CellPadding defines padding settings for table cells.

View File

@@ -3,12 +3,18 @@
package tw
import (
"math"
"strconv"
"strings"
"unicode"
"unicode/utf8"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"math" // For mathematical operations like ceiling
"strconv" // For string-to-number conversions
"strings" // For string manipulation utilities
"unicode" // For Unicode character classification
"unicode/utf8" // For UTF-8 rune handling
// For mathematical operations like ceiling
// For string-to-number conversions
// For string manipulation utilities
// For Unicode character classification
// For UTF-8 rune handling
)
// Title normalizes and uppercases a label string for use in headers.
@@ -75,7 +81,7 @@ func PadLeft(s, pad string, width int) string {
// Pad aligns a string within a specified width using a padding character.
// It truncates if the string is wider than the target width.
func Pad(s string, padChar string, totalWidth int, alignment Align) string {
func Pad(s, padChar string, totalWidth int, alignment Align) string {
sDisplayWidth := twwidth.Width(s)
if sDisplayWidth > totalWidth {
return twwidth.Truncate(s, totalWidth) // Only truncate if necessary

View File

@@ -114,21 +114,17 @@ func (m Mapper[K, V]) Values() []V {
// Each iterates over each key-value pair in the map and calls the provided function.
// Does nothing if the map is nil.
func (m Mapper[K, V]) Each(fn func(K, V)) {
if m != nil {
for k, v := range m {
fn(k, v)
}
for k, v := range m {
fn(k, v)
}
}
// Filter returns a new Mapper containing only the key-value pairs that satisfy the predicate.
func (m Mapper[K, V]) Filter(fn func(K, V) bool) Mapper[K, V] {
result := NewMapper[K, V]()
if m != nil {
for k, v := range m {
if fn(k, v) {
result[k] = v
}
for k, v := range m {
if fn(k, v) {
result[k] = v
}
}
return result
@@ -137,10 +133,8 @@ func (m Mapper[K, V]) Filter(fn func(K, V) bool) Mapper[K, V] {
// MapValues returns a new Mapper with the same keys but values transformed by the provided function.
func (m Mapper[K, V]) MapValues(fn func(V) V) Mapper[K, V] {
result := NewMapper[K, V]()
if m != nil {
for k, v := range m {
result[k] = fn(v)
}
for k, v := range m {
result[k] = fn(v)
}
return result
}
@@ -148,10 +142,8 @@ func (m Mapper[K, V]) MapValues(fn func(V) V) Mapper[K, V] {
// Clone returns a shallow copy of the Mapper.
func (m Mapper[K, V]) Clone() Mapper[K, V] {
result := NewMapper[K, V]()
if m != nil {
for k, v := range m {
result[k] = v
}
for k, v := range m {
result[k] = v
}
return result
}

View File

@@ -10,9 +10,6 @@ var (
SeparatorsNone = Separators{ShowHeader: Off, ShowFooter: Off, BetweenRows: Off, BetweenColumns: Off}
)
var (
// PaddingDefault represents standard single-space padding on left/right
// Equivalent to Padding{Left: " ", Right: " ", Overwrite: true}
PaddingDefault = Padding{Left: " ", Right: " ", Overwrite: true}
)
// PaddingDefault represents standard single-space padding on left/right
// Equivalent to Padding{Left: " ", Right: " ", Overwrite: true}
var PaddingDefault = Padding{Left: " ", Right: " ", Overwrite: true}

View File

@@ -1,8 +1,9 @@
package tw
import (
"github.com/olekukonko/ll"
"io"
"github.com/olekukonko/ll"
)
// Renderer defines the interface for rendering tables to an io.Writer.

View File

@@ -1,5 +1,7 @@
package tw
import "slices"
// Slicer is a generic slice type that provides additional methods for slice manipulation.
type Slicer[T any] []T
@@ -69,21 +71,17 @@ func (s Slicer[T]) Last() T {
// Each iterates over each element in the slice and calls the provided function.
// Does nothing if the slice is nil.
func (s Slicer[T]) Each(fn func(T)) {
if s != nil {
for _, v := range s {
fn(v)
}
for _, v := range s {
fn(v)
}
}
// Filter returns a new Slicer containing only elements that satisfy the predicate.
func (s Slicer[T]) Filter(fn func(T) bool) Slicer[T] {
result := NewSlicer[T]()
if s != nil {
for _, v := range s {
if fn(v) {
result = result.Append(v)
}
for _, v := range s {
if fn(v) {
result = result.Append(v)
}
}
return result
@@ -92,33 +90,22 @@ func (s Slicer[T]) Filter(fn func(T) bool) Slicer[T] {
// Map returns a new Slicer with each element transformed by the provided function.
func (s Slicer[T]) Map(fn func(T) T) Slicer[T] {
result := NewSlicer[T]()
if s != nil {
for _, v := range s {
result = result.Append(fn(v))
}
for _, v := range s {
result = result.Append(fn(v))
}
return result
}
// Contains returns true if the slice contains an element that satisfies the predicate.
func (s Slicer[T]) Contains(fn func(T) bool) bool {
if s != nil {
for _, v := range s {
if fn(v) {
return true
}
}
}
return false
return slices.ContainsFunc(s, fn)
}
// Find returns the first element that satisfies the predicate, along with a boolean indicating if it was found.
func (s Slicer[T]) Find(fn func(T) bool) (T, bool) {
if s != nil {
for _, v := range s {
if fn(v) {
return v, true
}
for _, v := range s {
if fn(v) {
return v, true
}
}
var zero T

View File

@@ -3,9 +3,12 @@
package tw
import (
"fmt"
"github.com/olekukonko/errors"
"bytes"
"io"
"strconv"
"strings"
"github.com/olekukonko/errors"
) // Custom error handling library
// Position defines where formatting applies in the table (e.g., header, footer, or rows).
@@ -31,6 +34,13 @@ type Formatter interface {
Format() string // Returns the formatted string representation
}
// Counter defines an interface that combines io.Writer with a method to retrieve a total.
// This is used by the WithCounter option to allow for counting lines, bytes, etc.
type Counter interface {
io.Writer // It must be a writer to be used in io.MultiWriter.
Total() int
}
// Align specifies the text alignment within a table cell.
type Align string
@@ -52,7 +62,7 @@ func (a Alignment) String() string {
if i > 0 {
str.WriteString("; ")
}
str.WriteString(fmt.Sprint(i))
str.WriteString(strconv.Itoa(i))
str.WriteString("=")
str.WriteString(string(a))
}
@@ -157,6 +167,7 @@ type Struct struct {
type Behavior struct {
AutoHide State // AutoHide determines whether empty columns are hidden. Ignored in streaming mode.
TrimSpace State // TrimSpace enables trimming of leading and trailing spaces from cell content.
TrimLine State // TrimLine determines whether empty visual lines within a cell are collapsed.
Header Control // Header specifies control settings for the table header.
Footer Control // Footer specifies control settings for the table footer.
@@ -212,3 +223,21 @@ func (p Padding) Empty() bool {
func (p Padding) Paddable() bool {
return !p.Empty() || p.Overwrite
}
// LineCounter is the default implementation of the Counter interface.
// It counts the number of newline characters written to it.
type LineCounter struct {
count int
}
// Write implements the io.Writer interface, counting newlines in the input.
// It uses a pointer receiver to modify the internal count.
func (lc *LineCounter) Write(p []byte) (n int, err error) {
lc.count += bytes.Count(p, []byte{'\n'})
return len(p), nil
}
// Total implements the Counter interface, returning the final count.
func (lc *LineCounter) Total() int {
return lc.count
}

View File

@@ -3,14 +3,15 @@ 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"
"github.com/olekukonko/errors"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/tablewriter/tw"
)
// applyHierarchicalMerges applies hierarchical merges to row content.
@@ -542,10 +543,7 @@ func (t *Table) buildCoreCellContexts(line []string, merges map[int]tw.MergeStat
// 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
}
padWidth := max(twwidth.Width(padChar), 1)
for j := 0; j < numCols; j++ {
mergeState := tw.MergeState{}
if merges != nil {
@@ -582,9 +580,9 @@ func (t *Table) calculateAndNormalizeWidths(ctx *renderContext) error {
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]()
// 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 {
@@ -720,7 +718,7 @@ func (t *Table) calculateAndNormalizeWidths(ctx *renderContext) error {
twwidth.Width(headerCellPadding.Left) +
twwidth.Width(headerCellPadding.Right)
currentSumOfColumnWidths := 0
workingWidths.Each(func(_ int, w int) { currentSumOfColumnWidths += w })
workingWidths.Each(func(_, w int) { currentSumOfColumnWidths += w })
numSeparatorsInFullSpan := 0
if ctx.numCols > 1 {
if t.renderer != nil && t.renderer.Config().Settings.Separators.BetweenColumns.Enabled() {
@@ -733,7 +731,7 @@ func (t *Table) calculateAndNormalizeWidths(ctx *renderContext) error {
mergedContentString, actualMergedHeaderContentPhysicalWidth, totalCurrentSpanPhysicalWidth)
shortfall := actualMergedHeaderContentPhysicalWidth - totalCurrentSpanPhysicalWidth
numNonZeroCols := 0
workingWidths.Each(func(_ int, w int) {
workingWidths.Each(func(_, w int) {
if w > 0 {
numNonZeroCols++
}
@@ -744,7 +742,7 @@ func (t *Table) calculateAndNormalizeWidths(ctx *renderContext) error {
if numNonZeroCols > 0 && shortfall > 0 {
extraPerColumn := int(math.Ceil(float64(shortfall) / float64(numNonZeroCols)))
finalSumAfterExpansion := 0
workingWidths.Each(func(colIdx int, currentW int) {
workingWidths.Each(func(colIdx, currentW int) {
if currentW > 0 || (numNonZeroCols == ctx.numCols && ctx.numCols > 0) {
newWidth := currentW + extraPerColumn
workingWidths.Set(colIdx, newWidth)
@@ -782,7 +780,7 @@ func (t *Table) calculateAndNormalizeWidths(ctx *renderContext) error {
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 })
finalWidths.Each(func(_, 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())
@@ -790,16 +788,13 @@ func (t *Table) calculateAndNormalizeWidths(ctx *renderContext) error {
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
}
targetTotalColumnContentWidth := max(t.config.Widths.Global-numSeparators, 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)
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 {
@@ -820,7 +815,7 @@ func (t *Table) calculateAndNormalizeWidths(ctx *renderContext) error {
}
tempSum := 0
scaledHardMinimums := tw.NewMapper[int, int]()
hardMinimums.Each(func(colIdx int, currentMinW int) {
hardMinimums.Each(func(colIdx, currentMinW int) {
scaledMinW := int(math.Round(float64(currentMinW) * scaleFactorMin))
if scaledMinW < 1 && targetTotalColumnContentWidth > 0 {
scaledMinW = 1
@@ -894,31 +889,29 @@ func (t *Table) calculateAndNormalizeWidths(ctx *renderContext) error {
if errorInDist < 0 {
adj = -1
}
if !(adj < 0 && w+adj < hardMinimums.Get(colToAdjust)) {
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)
} 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) {
finalWidths.Each(func(idx, w int) {
if w < 1 && targetTotalColumnContentWidth > 0 {
finalWidths.Set(idx, 1)
} else if w < 0 {
@@ -945,10 +938,7 @@ func (t *Table) calculateContentMaxWidth(colIdx int, config tw.CellConfig, padLe
if isStreaming {
// Existing streaming logic remains unchanged
totalColumnWidthFromStream := t.streamWidths.Get(colIdx)
if totalColumnWidthFromStream < 0 {
totalColumnWidthFromStream = 0
}
totalColumnWidthFromStream := max(t.streamWidths.Get(colIdx), 0)
effectiveContentMaxWidth = totalColumnWidthFromStream - padLeftWidth - padRightWidth
if effectiveContentMaxWidth < 1 && totalColumnWidthFromStream > (padLeftWidth+padRightWidth) {
effectiveContentMaxWidth = 1
@@ -1252,7 +1242,7 @@ func (t *Table) convertCellsToStrings(rowInput interface{}, cellCfg tw.CellConfi
var err error
switch v := rowInput.(type) {
//Directly supported slice types
// Directly supported slice types
case []string:
cells = v
case []interface{}: // Catches variadic simple types grouped by Append
@@ -1338,7 +1328,7 @@ func (t *Table) convertCellsToStrings(rowInput interface{}, cellCfg tw.CellConfi
cells[i] = val.String()
}
//Cases for single items that are NOT slices
// 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),
@@ -1578,7 +1568,7 @@ func (t *Table) processVariadic(elements []any) []any {
}
// toStringLines converts raw cells to formatted lines for table output
func (t *Table) toStringLines(row interface{}, config tw.CellConfig) ([][]string, error) {
func (t *Table) toStringLines(row any, config tw.CellConfig) ([][]string, error) {
cells, err := t.convertCellsToStrings(row, config)
if err != nil {
return nil, err