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-30 13:07:46 +02:00
committed by Ralf Haferkamp
parent d05d5bdc6f
commit f4eaa8bd5b
25 changed files with 387 additions and 283 deletions
+1 -1
View File
@@ -59,7 +59,7 @@ require (
github.com/nats-io/nats-server/v2 v2.12.0 github.com/nats-io/nats-server/v2 v2.12.0
github.com/nats-io/nats.go v1.46.0 github.com/nats-io/nats.go v1.46.0
github.com/oklog/run v1.2.0 github.com/oklog/run v1.2.0
github.com/olekukonko/tablewriter v1.0.9 github.com/olekukonko/tablewriter v1.1.0
github.com/onsi/ginkgo v1.16.5 github.com/onsi/ginkgo v1.16.5
github.com/onsi/ginkgo/v2 v2.25.3 github.com/onsi/ginkgo/v2 v2.25.3
github.com/onsi/gomega v1.38.2 github.com/onsi/gomega v1.38.2
+2 -2
View File
@@ -914,8 +914,8 @@ github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXI
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8= github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+55
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
+5 -3
View File
@@ -28,7 +28,7 @@ go get github.com/olekukonko/tablewriter@v0.0.5
#### Latest Version #### Latest Version
The latest stable version The latest stable version
```bash ```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. **Warning:** Version `v1.0.0` contains missing functionality and should not be used.
@@ -62,7 +62,7 @@ func main() {
data := [][]string{ data := [][]string{
{"Package", "Version", "Status"}, {"Package", "Version", "Status"},
{"tablewriter", "v0.0.5", "legacy"}, {"tablewriter", "v0.0.5", "legacy"},
{"tablewriter", "v1.0.9", "latest"}, {"tablewriter", "v1.1.0", "latest"},
} }
table := tablewriter.NewWriter(os.Stdout) table := tablewriter.NewWriter(os.Stdout)
@@ -77,7 +77,7 @@ func main() {
│ PACKAGE │ VERSION │ STATUS │ │ PACKAGE │ VERSION │ STATUS │
├─────────────┼─────────┼────────┤ ├─────────────┼─────────┼────────┤
│ tablewriter │ v0.0.5 │ legacy │ │ 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) - `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 ## Command-Line Tool
+2 -5
View File
@@ -14,6 +14,7 @@ type Config struct {
Stream tw.StreamConfig Stream tw.StreamConfig
Behavior tw.Behavior Behavior tw.Behavior
Widths tw.CellWidth Widths tw.CellWidth
Counter tw.Counter
} }
// ConfigBuilder provides a fluent interface for building Config // 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). // WithMaxWidth sets the maximum width for the entire table (0 means unlimited).
// Negative values are treated as 0. // Negative values are treated as 0.
func (b *ConfigBuilder) WithMaxWidth(width int) *ConfigBuilder { func (b *ConfigBuilder) WithMaxWidth(width int) *ConfigBuilder {
if width < 0 { b.config.MaxWidth = max(width, 0)
b.config.MaxWidth = 0
} else {
b.config.MaxWidth = width
}
return b return b
} }
+43 -9
View File
@@ -1,11 +1,12 @@
package tablewriter package tablewriter
import ( import (
"reflect"
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"github.com/olekukonko/ll" "github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/pkg/twwidth" "github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/tablewriter/tw" "github.com/olekukonko/tablewriter/tw"
"reflect"
) )
// Option defines a function type for configuring a Table instance. // 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. // WithHeaderAutoFormat enables or disables automatic formatting for header cells.
// Logs the change if debugging is enabled. // Logs the change if debugging is enabled.
func WithHeaderAutoFormat(state tw.State) Option { func WithHeaderAutoFormat(state tw.State) Option {
@@ -607,10 +619,8 @@ func WithRendition(rendition tw.Rendition) Option {
if target.logger != nil { if target.logger != nil {
target.logger.Debugf("Option: WithRendition: Applied to renderer via Renditioning.SetRendition(): %+v", rendition) target.logger.Debugf("Option: WithRendition: Applied to renderer via Renditioning.SetRendition(): %+v", rendition)
} }
} else { } else if target.logger != nil {
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)
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 { if target.logger != nil {
target.logger.Debugf("Option: WithRendition: Applied to renderer via Renditioning.SetRendition(): %+v", cfg) target.logger.Debugf("Option: WithRendition: Applied to renderer via Renditioning.SetRendition(): %+v", cfg)
} }
} else { } else if target.logger != nil {
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)
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. // defaultConfig returns a default Config with sensible settings for headers, rows, footers, and behavior.
func defaultConfig() Config { func defaultConfig() Config {
return Config{ return Config{
@@ -717,6 +750,7 @@ func defaultConfig() Config {
Behavior: tw.Behavior{ Behavior: tw.Behavior{
AutoHide: tw.Off, AutoHide: tw.Off,
TrimSpace: tw.On, TrimSpace: tw.On,
TrimLine: tw.On,
Structs: tw.Struct{ Structs: tw.Struct{
AutoHeader: tw.Off, AutoHeader: tw.Off,
Tags: []string{"json", "db"}, Tags: []string{"json", "db"},
+10 -8
View File
@@ -2,10 +2,11 @@ package twwidth
import ( import (
"bytes" "bytes"
"github.com/mattn/go-runewidth"
"regexp" "regexp"
"strings" "strings"
"sync" "sync"
"github.com/mattn/go-runewidth"
) )
// condition holds the global runewidth configuration, including East Asian width settings. // 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. // including CSI (Control Sequence Introducer) and OSC (Operating System Command) sequences.
// The returned regex can be used to strip ANSI codes from strings. // The returned regex can be used to strip ANSI codes from strings.
func Filter() *regexp.Regexp { func Filter() *regexp.Regexp {
var regESC = "\x1b" // ASCII escape character regESC := "\x1b" // ASCII escape character
var regBEL = "\x07" // ASCII bell character regBEL := "\x07" // ASCII bell character
// ANSI string terminator: either ESC+\ or BEL // 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 // 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 // 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 // Combine CSI and OSC patterns into a single regex
return regexp.MustCompile("(" + regCSI + "|" + regOSC + ")") return regexp.MustCompile("(" + regCSI + "|" + regOSC + ")")
@@ -257,11 +258,12 @@ func Truncate(s string, maxWidth int, suffix ...string) string {
terminated := false terminated := false
if seqLen >= 2 { if seqLen >= 2 {
introducer := seqBytes[1] introducer := seqBytes[1]
if introducer == '[' { switch introducer {
case '[':
if seqLen >= 3 && r >= 0x40 && r <= 0x7E { if seqLen >= 3 && r >= 0x40 && r <= 0x7E {
terminated = true terminated = true
} }
} else if introducer == ']' { case ']':
if r == '\x07' { if r == '\x07' {
terminated = true terminated = true
} else if seqLen > 1 && seqBytes[seqLen-2] == '\x1b' && r == '\\' { // Check for ST: \x1b\ } else if seqLen > 1 && seqBytes[seqLen-2] == '\x1b' && r == '\\' { // Check for ST: \x1b\
+19 -25
View File
@@ -1,11 +1,12 @@
package renderer package renderer
import ( import (
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"io" "io"
"strings" "strings"
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/tablewriter/tw" "github.com/olekukonko/tablewriter/tw"
) )
@@ -283,7 +284,7 @@ func (f *Blueprint) formatCell(content string, width int, padding tw.Padding, al
leftPadChar := padding.Left leftPadChar := padding.Left
rightPadChar := padding.Right 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 { // if leftPadChar == tw.Empty {
// leftPadChar = tw.Space // leftPadChar = tw.Space
// } // }
@@ -297,10 +298,7 @@ func (f *Blueprint) formatCell(content string, width int, padding tw.Padding, al
padRightWidth := twwidth.Width(rightPadChar) padRightWidth := twwidth.Width(rightPadChar)
// Calculate available width for content // Calculate available width for content
availableContentWidth := width - padLeftWidth - padRightWidth availableContentWidth := max(width-padLeftWidth-padRightWidth, 0)
if availableContentWidth < 0 {
availableContentWidth = 0
}
f.logger.Debugf("Available content width: %d", availableContentWidth) f.logger.Debugf("Available content width: %d", availableContentWidth)
// Truncate content if it exceeds available width // 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 // Calculate total padding needed
totalPaddingWidth := width - runeWidth totalPaddingWidth := max(width-runeWidth, 0)
if totalPaddingWidth < 0 {
totalPaddingWidth = 0
}
f.logger.Debugf("Total padding width: %d", totalPaddingWidth) f.logger.Debugf("Total padding width: %d", totalPaddingWidth)
var result strings.Builder var result strings.Builder
@@ -435,7 +430,7 @@ func (f *Blueprint) renderLine(ctx tw.Formatting) {
prevWidth := ctx.Row.Widths.Get(colIndex - 1) prevWidth := ctx.Row.Widths.Get(colIndex - 1)
prevCellCtx, prevOk := ctx.Row.Current[colIndex-1] prevCellCtx, prevOk := ctx.Row.Current[colIndex-1]
prevIsHMergeEnd := prevOk && prevCellCtx.Merge.Horizontal.Present && prevCellCtx.Merge.Horizontal.End 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 shouldAddSeparator = true
} }
} }
@@ -454,10 +449,7 @@ func (f *Blueprint) renderLine(ctx tw.Formatting) {
if ctx.Row.Position == tw.Row { if ctx.Row.Position == tw.Row {
dynamicTotalWidth := 0 dynamicTotalWidth := 0
for k := 0; k < span && colIndex+k < numCols; k++ { for k := 0; k < span && colIndex+k < numCols; k++ {
normWidth := ctx.NormalizedWidths.Get(colIndex + k) normWidth := max(ctx.NormalizedWidths.Get(colIndex+k), 0)
if normWidth < 0 {
normWidth = 0
}
dynamicTotalWidth += normWidth dynamicTotalWidth += normWidth
if k > 0 && separatorDisplayWidth > 0 && ctx.NormalizedWidths.Get(colIndex+k) > 0 { if k > 0 && separatorDisplayWidth > 0 && ctx.NormalizedWidths.Get(colIndex+k) > 0 {
dynamicTotalWidth += separatorDisplayWidth dynamicTotalWidth += separatorDisplayWidth
@@ -501,21 +493,24 @@ func (f *Blueprint) renderLine(ctx tw.Formatting) {
// Set cell padding and alignment // Set cell padding and alignment
padding := cellCtx.Padding padding := cellCtx.Padding
align := cellCtx.Align align := cellCtx.Align
if align == tw.AlignNone { switch align {
if ctx.Row.Position == tw.Header { case tw.AlignNone:
switch ctx.Row.Position {
case tw.Header:
align = tw.AlignCenter align = tw.AlignCenter
} else if ctx.Row.Position == tw.Footer { case tw.Footer:
align = tw.AlignRight align = tw.AlignRight
} else { default:
align = tw.AlignLeft 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) 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 { case tw.Skip:
if ctx.Row.Position == tw.Header { switch ctx.Row.Position {
case tw.Header:
align = tw.AlignCenter align = tw.AlignCenter
} else if ctx.Row.Position == tw.Footer { case tw.Footer:
align = tw.AlignRight align = tw.AlignRight
} else { default:
align = tw.AlignLeft 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) 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) { func (f *Blueprint) Rendition(config tw.Rendition) {
f.config = mergeRendition(f.config, config) f.config = mergeRendition(f.config, config)
f.logger.Debugf("Blueprint.Rendition updated. New config: %+v", f.config) f.logger.Debugf("Blueprint.Rendition updated. New config: %+v", f.config)
} }
// Ensure Blueprint implements tw.Renditioning // Ensure Blueprint implements tw.Renditioning
+14 -22
View File
@@ -1,12 +1,13 @@
package renderer package renderer
import ( import (
"io"
"strings"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/olekukonko/ll" "github.com/olekukonko/ll"
"github.com/olekukonko/ll/lh" "github.com/olekukonko/ll/lh"
"github.com/olekukonko/tablewriter/pkg/twwidth" "github.com/olekukonko/tablewriter/pkg/twwidth"
"io"
"strings"
"github.com/olekukonko/tablewriter/tw" "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 // Set default padding characters
padLeftCharStr := padding.Left padLeftCharStr := padding.Left
if padLeftCharStr == tw.Empty { // if padLeftCharStr == tw.Empty {
padLeftCharStr = tw.Space // padLeftCharStr = tw.Space
} //}
padRightCharStr := padding.Right padRightCharStr := padding.Right
if padRightCharStr == tw.Empty { // if padRightCharStr == tw.Empty {
padRightCharStr = tw.Space // padRightCharStr = tw.Space
} //}
// Calculate padding widths // Calculate padding widths
definedPadLeftWidth := twwidth.Width(padLeftCharStr) definedPadLeftWidth := twwidth.Width(padLeftCharStr)
definedPadRightWidth := twwidth.Width(padRightCharStr) definedPadRightWidth := twwidth.Width(padRightCharStr)
// Calculate available width for content and alignment // Calculate available width for content and alignment
availableForContentAndAlign := width - definedPadLeftWidth - definedPadRightWidth availableForContentAndAlign := max(width-definedPadLeftWidth-definedPadRightWidth, 0)
if availableForContentAndAlign < 0 {
availableForContentAndAlign = 0
}
// Truncate content if it exceeds available width // Truncate content if it exceeds available width
if contentVisualWidth > availableForContentAndAlign { if contentVisualWidth > availableForContentAndAlign {
@@ -403,10 +401,7 @@ func (c *Colorized) formatCell(content string, width int, padding tw.Padding, al
} }
// Calculate remaining space for alignment // Calculate remaining space for alignment
remainingSpaceForAlignment := availableForContentAndAlign - contentVisualWidth remainingSpaceForAlignment := max(availableForContentAndAlign-contentVisualWidth, 0)
if remainingSpaceForAlignment < 0 {
remainingSpaceForAlignment = 0
}
// Apply alignment padding // Apply alignment padding
leftAlignmentPadSpaces := tw.Empty leftAlignmentPadSpaces := tw.Empty
@@ -539,7 +534,7 @@ func (c *Colorized) renderLine(ctx tw.Formatting, line []string, tint Tint) {
shouldAddSeparator := false shouldAddSeparator := false
if i > 0 && c.config.Settings.Separators.BetweenColumns.Enabled() { if i > 0 && c.config.Settings.Separators.BetweenColumns.Enabled() {
cellCtx, ok := ctx.Row.Current[i] 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 shouldAddSeparator = true
} }
} }
@@ -574,10 +569,7 @@ func (c *Colorized) renderLine(ctx tw.Formatting, line []string, tint Tint) {
dynamicTotalWidth := 0 dynamicTotalWidth := 0
for k := 0; k < span && i+k < numCols; k++ { for k := 0; k < span && i+k < numCols; k++ {
colToSum := i + k colToSum := i + k
normWidth := ctx.NormalizedWidths.Get(colToSum) normWidth := max(ctx.NormalizedWidths.Get(colToSum), 0)
if normWidth < 0 {
normWidth = 0
}
dynamicTotalWidth += normWidth dynamicTotalWidth += normWidth
if k > 0 && separatorDisplayWidth > 0 { if k > 0 && separatorDisplayWidth > 0 {
dynamicTotalWidth += separatorDisplayWidth 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 // Override alignment for footer merges or TOTAL pattern
if (ctx.Row.Position == tw.Footer && isHMergeStart) || isTotalPattern { 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) 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 align = tw.AlignRight
} }
+2 -2
View File
@@ -2,6 +2,7 @@ package renderer
import ( import (
"fmt" "fmt"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/olekukonko/tablewriter/tw" "github.com/olekukonko/tablewriter/tw"
) )
@@ -82,7 +83,6 @@ func defaultColorized() ColorizedConfig {
// defaultOceanRendererConfig returns a base tw.Rendition for the Ocean renderer. // defaultOceanRendererConfig returns a base tw.Rendition for the Ocean renderer.
func defaultOceanRendererConfig() tw.Rendition { func defaultOceanRendererConfig() tw.Rendition {
return tw.Rendition{ return tw.Rendition{
Borders: tw.Border{ Borders: tw.Border{
Left: tw.On, Right: tw.On, Top: tw.On, Bottom: tw.On, 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 defaults.CompactMode = overrides.CompactMode
} }
//if overrides.Cushion != tw.Unknown { // if overrides.Cushion != tw.Unknown {
// defaults.Cushion = overrides.Cushion // defaults.Cushion = overrides.Cushion
//} //}
+3 -2
View File
@@ -3,11 +3,12 @@ package renderer
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/olekukonko/ll"
"html" "html"
"io" "io"
"strings" "strings"
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/tw" "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. // 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 { // if h.debug {
// msg := fmt.Sprintf(format, a...) // msg := fmt.Sprintf(format, a...)
// h.trace = append(h.trace, fmt.Sprintf("[HTML] %s", msg)) // h.trace = append(h.trace, fmt.Sprintf("[HTML] %s", msg))
+15 -19
View File
@@ -1,11 +1,12 @@
package renderer package renderer
import ( import (
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"io" "io"
"strings" "strings"
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/tablewriter/tw" "github.com/olekukonko/tablewriter/tw"
) )
@@ -95,7 +96,6 @@ func (m *Markdown) Row(row []string, ctx tw.Formatting) {
m.resolveAlignment(ctx) 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.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) m.renderMarkdownLine(row, ctx, false)
} }
// Footer renders the Markdown table footer. // 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. // 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 { 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) // content = strings.TrimSpace(content)
//} //}
contentVisualWidth := twwidth.Width(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) targetWidth := tw.Max(width, minWidth)
// Calculate padding // Calculate padding
totalPaddingNeeded := targetWidth - contentVisualWidth totalPaddingNeeded := max(targetWidth-contentVisualWidth, 0)
if totalPaddingNeeded < 0 {
totalPaddingNeeded = 0
}
var leftPadStr, rightPadStr string var leftPadStr, rightPadStr string
switch align { switch align {
@@ -220,13 +217,14 @@ func (m *Markdown) formatCell(content string, width int, align tw.Align, padding
adjNeeded := targetWidth - finalWidth adjNeeded := targetWidth - finalWidth
if adjNeeded > 0 { if adjNeeded > 0 {
adjStr := strings.Repeat(tw.Space, adjNeeded) adjStr := strings.Repeat(tw.Space, adjNeeded)
if align == tw.AlignRight { switch align {
case tw.AlignRight:
result = adjStr + result result = adjStr + result
} else if align == tw.AlignCenter { case tw.AlignCenter:
leftAdj := adjNeeded / 2 leftAdj := adjNeeded / 2
rightAdj := adjNeeded - leftAdj rightAdj := adjNeeded - leftAdj
result = strings.Repeat(tw.Space, leftAdj) + result + strings.Repeat(tw.Space, rightAdj) result = strings.Repeat(tw.Space, leftAdj) + result + strings.Repeat(tw.Space, rightAdj)
} else { default:
result += adjStr result += adjStr
} }
} else { } else {
@@ -346,10 +344,7 @@ func (m *Markdown) renderMarkdownLine(line []string, ctx tw.Formatting, isHeader
span = cellCtx.Merge.Horizontal.Span span = cellCtx.Merge.Horizontal.Span
totalWidth := 0 totalWidth := 0
for k := 0; k < span && colIndex+k < numCols; k++ { for k := 0; k < span && colIndex+k < numCols; k++ {
colWidth := ctx.NormalizedWidths.Get(colIndex + k) colWidth := max(ctx.NormalizedWidths.Get(colIndex+k), 0)
if colWidth < 0 {
colWidth = 0
}
totalWidth += colWidth totalWidth += colWidth
if k > 0 && separatorWidth > 0 { if k > 0 && separatorWidth > 0 {
totalWidth += separatorWidth 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 // For rows, use the header's alignment if specified
rowAlign := align 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 { if headerCellCtx.Align != tw.AlignNone && headerCellCtx.Align != tw.Empty {
rowAlign = headerCellCtx.Align rowAlign = headerCellCtx.Align
} }
} }
if rowAlign == tw.AlignNone || rowAlign == tw.Empty { if rowAlign == tw.AlignNone || rowAlign == tw.Empty {
if ctx.Row.Position == tw.Header { switch ctx.Row.Position {
case tw.Header:
rowAlign = tw.AlignCenter rowAlign = tw.AlignCenter
} else if ctx.Row.Position == tw.Footer { case tw.Footer:
rowAlign = tw.AlignRight rowAlign = tw.AlignRight
} else { default:
rowAlign = tw.AlignLeft rowAlign = tw.AlignLeft
} }
m.logger.Debugf("renderMarkdownLine: Col %d using default align '%s'", colIndex, rowAlign) m.logger.Debugf("renderMarkdownLine: Col %d using default align '%s'", colIndex, rowAlign)
+9 -17
View File
@@ -1,17 +1,18 @@
package renderer package renderer
import ( import (
"github.com/olekukonko/tablewriter/pkg/twwidth"
"io" "io"
"slices"
"strings" "strings"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/ll" "github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/tw" "github.com/olekukonko/tablewriter/tw"
) )
// OceanConfig defines configuration specific to the Ocean renderer. // OceanConfig defines configuration specific to the Ocean renderer.
type OceanConfig struct { type OceanConfig struct{}
}
// Ocean is a streaming table renderer that writes ASCII tables. // Ocean is a streaming table renderer that writes ASCII tables.
type Ocean struct { type Ocean struct {
@@ -327,12 +328,9 @@ func (o *Ocean) renderContentLine(ctx tw.Formatting, lineData []string) {
idxInMergeSpan := colIdx + k idxInMergeSpan := colIdx + k
// Check if idxInMergeSpan is a defined column in fixedWidths // Check if idxInMergeSpan is a defined column in fixedWidths
foundInFixedWidths := false foundInFixedWidths := false
for _, sortedCIdx_inner := range sortedColIndices { if slices.Contains(sortedColIndices, idxInMergeSpan) {
if sortedCIdx_inner == idxInMergeSpan { currentMergeTotalRenderWidth += o.fixedWidths.Get(idxInMergeSpan)
currentMergeTotalRenderWidth += o.fixedWidths.Get(idxInMergeSpan) foundInFixedWidths = true
foundInFixedWidths = true
break
}
} }
if !foundInFixedWidths && idxInMergeSpan <= sortedColIndices[len(sortedColIndices)-1] { if !foundInFixedWidths && idxInMergeSpan <= sortedColIndices[len(sortedColIndices)-1] {
o.logger.Debugf("Col %d in HMerge span not found in fixedWidths, assuming 0-width contribution.", idxInMergeSpan) 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) padLeftDisplayWidth := twwidth.Width(padLeftChar)
padRightDisplayWidth := twwidth.Width(padRightChar) padRightDisplayWidth := twwidth.Width(padRightChar)
spaceForContentAndAlignment := cellVisualWidth - padLeftDisplayWidth - padRightDisplayWidth spaceForContentAndAlignment := max(cellVisualWidth-padLeftDisplayWidth-padRightDisplayWidth, 0)
if spaceForContentAndAlignment < 0 {
spaceForContentAndAlignment = 0
}
if contentDisplayWidth > spaceForContentAndAlignment { if contentDisplayWidth > spaceForContentAndAlignment {
content = twwidth.Truncate(content, spaceForContentAndAlignment) content = twwidth.Truncate(content, spaceForContentAndAlignment)
contentDisplayWidth = twwidth.Width(content) contentDisplayWidth = twwidth.Width(content)
} }
remainingSpace := spaceForContentAndAlignment - contentDisplayWidth remainingSpace := max(spaceForContentAndAlignment-contentDisplayWidth, 0)
if remainingSpace < 0 {
remainingSpace = 0
}
var PL, PR string var PL, PR string
switch align { switch align {
+7 -8
View File
@@ -2,11 +2,12 @@ package renderer
import ( import (
"fmt" "fmt"
"github.com/olekukonko/ll"
"html" "html"
"io" "io"
"strings" "strings"
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter/tw" "github.com/olekukonko/tablewriter/tw"
) )
@@ -512,10 +513,7 @@ func (s *SVG) renderVisualLine(visualLineData []string, ctx tw.Formatting, posit
} }
s.dataRowCounter++ s.dataRowCounter++
} else { } else {
parentDataRowStripeIndex := s.dataRowCounter - 1 parentDataRowStripeIndex := max(s.dataRowCounter-1, 0)
if parentDataRowStripeIndex < 0 {
parentDataRowStripeIndex = 0
}
if s.config.RowAltBG != tw.Empty && parentDataRowStripeIndex%2 != 0 { if s.config.RowAltBG != tw.Empty && parentDataRowStripeIndex%2 != 0 {
bgColor = s.config.RowAltBG bgColor = s.config.RowAltBG
} else { } else {
@@ -623,9 +621,10 @@ func (s *SVG) renderVisualLine(visualLineData []string, ctx tw.Formatting, posit
} }
} }
textX := currentX + s.config.Padding textX := currentX + s.config.Padding
if cellTextAnchor == "middle" { switch cellTextAnchor {
case "middle":
textX = currentX + s.config.Padding + (rectWidth-2*s.config.Padding)/2.0 textX = currentX + s.config.Padding + (rectWidth-2*s.config.Padding)/2.0
} else if cellTextAnchor == "end" { case "end":
textX = currentX + rectWidth - s.config.Padding textX = currentX + rectWidth - s.config.Padding
} }
textY := s.currentY + rectHeight/2.0 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{}) { func (s *SVG) debug(format string, a ...interface{}) {
if s.config.Debug { if s.config.Debug {
msg := fmt.Sprintf(format, a...) msg := fmt.Sprintf(format, a...)
s.trace = append(s.trace, fmt.Sprintf("[SVG] %s", msg)) s.trace = append(s.trace, "[SVG] "+msg)
} }
} }
+11 -10
View File
@@ -1,10 +1,11 @@
package tablewriter package tablewriter
import ( import (
"math"
"github.com/olekukonko/errors" "github.com/olekukonko/errors"
"github.com/olekukonko/tablewriter/pkg/twwidth" "github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/tablewriter/tw" "github.com/olekukonko/tablewriter/tw"
"math"
) )
// Close finalizes the table stream. // Close finalizes the table stream.
@@ -92,8 +93,8 @@ func (t *Table) Start() error {
return errors.Newf("renderer does not support streaming") return errors.Newf("renderer does not support streaming")
} }
//t.renderer.Start(t.writer) // t.renderer.Start(t.writer)
//t.renderer.Logger(t.logger) // t.renderer.Logger(t.logger)
if t.hasPrinted { if t.hasPrinted {
// Prevent calling Start() multiple times on the same stream instance. // 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() t.streamWidths = t.config.Widths.PerColumn.Clone()
// Determine numCols from the highest index in PerColumn map // Determine numCols from the highest index in PerColumn map
maxColIdx := -1 maxColIdx := -1
t.streamWidths.Each(func(col int, width int) { t.streamWidths.Each(func(col, width int) {
if col > maxColIdx { if col > maxColIdx {
maxColIdx = col maxColIdx = col
} }
@@ -331,7 +332,7 @@ func (t *Table) streamAppendRow(row interface{}) error {
t.logger.Debug("streamAppendRow: Separator line rendered. Updated lastRenderedPosition to 'separator'.") t.logger.Debug("streamAppendRow: Separator line rendered. Updated lastRenderedPosition to 'separator'.")
} else { } else {
details := "" details := ""
if !(shouldDrawHeaderRowSeparator || shouldDrawRowRowSeparator) { if !shouldDrawHeaderRowSeparator && !shouldDrawRowRowSeparator {
details = "neither header/row nor row/row separator was flagged true" details = "neither header/row nor row/row separator was flagged true"
} else if t.lastRenderedPosition == tw.Position("separator") { } else if t.lastRenderedPosition == tw.Position("separator") {
details = "lastRenderedPosition is already 'separator'" details = "lastRenderedPosition is already 'separator'"
@@ -475,7 +476,7 @@ func (t *Table) streamCalculateWidths(sampling []string, config tw.CellConfig) i
determinedNumCols := 0 determinedNumCols := 0
if t.config.Widths.PerColumn != nil && t.config.Widths.PerColumn.Len() > 0 { if t.config.Widths.PerColumn != nil && t.config.Widths.PerColumn.Len() > 0 {
maxColIdx := -1 maxColIdx := -1
t.config.Widths.PerColumn.Each(func(col int, width int) { t.config.Widths.PerColumn.Each(func(col, width int) {
if col > maxColIdx { if col > maxColIdx {
maxColIdx = col 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 { if t.config.Widths.Global > 0 && t.streamNumCols > 0 {
t.logger.Debug("streamCalculateWidths: Applying global stream width constraint %d", t.config.Widths.Global) t.logger.Debug("streamCalculateWidths: Applying global stream width constraint %d", t.config.Widths.Global)
currentTotalColumnWidthsSum := 0 currentTotalColumnWidthsSum := 0
t.streamWidths.Each(func(_ int, w int) { t.streamWidths.Each(func(_, w int) {
currentTotalColumnWidthsSum += w 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 // Distribute remainingSpace (positive or negative) among non-zero width columns
if remainingSpace != 0 && t.streamNumCols > 0 { if remainingSpace != 0 && t.streamNumCols > 0 {
colsToAdjust := []int{} 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 if w > 0 { // Only consider columns that currently have width
colsToAdjust = append(colsToAdjust, col) colsToAdjust = append(colsToAdjust, col)
} }
@@ -689,7 +690,7 @@ func (t *Table) streamCalculateWidths(sampling []string, config tw.CellConfig) i
} }
// Final sanitization // Final sanitization
t.streamWidths.Each(func(col int, width int) { t.streamWidths.Each(func(col, width int) {
if width < 0 { if width < 0 {
t.streamWidths.Set(col, 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, // If this is the last line of the last content block (footer), and no bottom border will be drawn,
// its Location should be End. // its Location should be End.
isLastLineOfTableContent := (i == totalFooterLines-1) && isLastLineOfTableContent := (i == totalFooterLines-1) &&
!(cfg.Borders.Bottom.Enabled() && cfg.Settings.Lines.ShowBottom.Enabled()) (!cfg.Borders.Bottom.Enabled() || !cfg.Settings.Lines.ShowBottom.Enabled())
if isLastLineOfTableContent { if isLastLineOfTableContent {
resp.location = tw.LocationEnd resp.location = tw.LocationEnd
t.logger.Debug("streamRenderFooter: Setting LocationEnd for last footer line as no bottom border will follow.") t.logger.Debug("streamRenderFooter: Setting LocationEnd for last footer line as no bottom border will follow.")
+87 -49
View File
@@ -2,13 +2,6 @@ package tablewriter
import ( import (
"bytes" "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" "io"
"math" "math"
"os" "os"
@@ -16,11 +9,20 @@ import (
"runtime" "runtime"
"strings" "strings"
"sync" "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. // Table represents a table instance with content and rendering capabilities.
type Table struct { type Table struct {
writer io.Writer // Destination for table output writer io.Writer // Destination for table output
counters []tw.Counter // Counters for indices
rows [][][]string // Row data, supporting multi-line cells rows [][][]string // Row data, supporting multi-line cells
headers [][]string // Header content headers [][]string // Header content
footers [][]string // Footer 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. // Parameter opts is a function that modifies the Table struct.
// Returns the Table instance for method chaining. // Returns the Table instance for method chaining.
func (t *Table) Options(opts ...Option) *Table { func (t *Table) Options(opts ...Option) *Table {
// add logger // add logger
if t.logger == nil { if t.logger == nil {
t.logger = ll.New("table").Handler(lh.NewTextHandler(t.trace)) 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 // force debugging mode if set
// This should be move away form WithDebug // This should be move away form WithDebug
if t.config.Debug == true { if t.config.Debug {
t.logger.Enable() t.logger.Enable()
t.logger.Resume() t.logger.Resume()
} else { } else {
@@ -510,6 +511,28 @@ func (t *Table) Render() error {
return t.render() 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. // Trimmer trims whitespace from a string based on the Tables configuration.
// It conditionally applies strings.TrimSpace to the input string if the TrimSpace behavior // 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 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 currentLine := line
breakCharWidth := twwidth.Width(tw.CharBreak) breakCharWidth := twwidth.Width(tw.CharBreak)
for twwidth.Width(currentLine) > effectiveContentMaxWidth { for twwidth.Width(currentLine) > effectiveContentMaxWidth {
targetWidth := effectiveContentMaxWidth - breakCharWidth targetWidth := max(effectiveContentMaxWidth-breakCharWidth, 0)
if targetWidth < 0 {
targetWidth = 0
}
breakPoint := tw.BreakPoint(currentLine, targetWidth) breakPoint := tw.BreakPoint(currentLine, targetWidth)
runes := []rune(currentLine) runes := []rune(currentLine)
if breakPoint <= 0 || breakPoint > len(runes) { if breakPoint <= 0 || breakPoint > len(runes) {
@@ -1362,23 +1382,40 @@ func (t *Table) prepareWithMerges(content [][]string, config tw.CellConfig, posi
// No parameters are required. // No parameters are required.
// Returns an error if rendering fails in any section. // Returns an error if rendering fails in any section.
func (t *Table) render() error { func (t *Table) render() error {
t.ensureInitialized() 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 { if t.config.Stream.Enable {
t.logger.Warn("Render() called in streaming mode. Use Start/Append/Close methods instead.") 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") return errors.New("render called in streaming mode; use Start/Append/Close")
} }
// Calculate and cache numCols for THIS batch render pass // Calculate and cache the column count for this specific batch render pass.
t.batchRenderNumCols = t.maxColumns() // Calculate ONCE t.batchRenderNumCols = t.maxColumns()
t.isBatchRenderNumColsSet = true // Mark the cache as active for this render pass t.isBatchRenderNumColsSet = true
t.logger.Debugf("Render(): Set batchRenderNumCols to %d and isBatchRenderNumColsSet to true.", t.batchRenderNumCols)
defer func() { defer func() {
t.isBatchRenderNumColsSet = false 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) 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) (t.caption.Spot >= tw.SpotTopLeft && t.caption.Spot <= tw.SpotBottomRight)
var tableStringBuffer *strings.Builder var tableStringBuffer *strings.Builder
targetWriter := t.writer targetWriter := t.writer // Can be the original writer or the MultiWriter.
originalWriter := t.writer // Save original writer for restoration if needed
// 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 { if isTopOrBottomCaption {
tableStringBuffer = &strings.Builder{} tableStringBuffer = &strings.Builder{}
targetWriter = tableStringBuffer targetWriter = tableStringBuffer
@@ -1398,17 +1436,15 @@ func (t *Table) render() error {
t.logger.Debugf("No caption detected. Rendering table core directly to writer.") 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 t.writer = targetWriter
ctx, mctx, err := t.prepareContexts() ctx, mctx, err := t.prepareContexts()
if err != nil { if err != nil {
t.writer = originalWriter
t.logger.Errorf("prepareContexts failed: %v", err) t.logger.Errorf("prepareContexts failed: %v", err)
return errors.Newf("failed to prepare table contexts").Wrap(err) return errors.Newf("failed to prepare table contexts").Wrap(err)
} }
if err := ctx.renderer.Start(t.writer); err != nil { if err := ctx.renderer.Start(t.writer); err != nil {
t.writer = originalWriter
t.logger.Errorf("Renderer Start() error: %v", err) t.logger.Errorf("Renderer Start() error: %v", err)
return errors.Newf("renderer start failed").Wrap(err) return errors.Newf("renderer start failed").Wrap(err)
} }
@@ -1440,18 +1476,21 @@ func (t *Table) render() error {
renderError = true 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 { if renderError {
return firstRenderErr // Return error from core rendering if any return firstRenderErr
} }
//Caption Handling & Final Output --- // Caption Handling & Final Output
if isTopOrBottomCaption { if isTopOrBottomCaption {
renderedTableContent := tableStringBuffer.String() renderedTableContent := tableStringBuffer.String()
t.logger.Debugf("[Render] Table core buffer length: %d", len(renderedTableContent)) 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()) shouldHaveBorders := t.renderer != nil && (t.renderer.Config().Borders.Top.Enabled() || t.renderer.Config().Borders.Bottom.Enabled())
if len(renderedTableContent) == 0 && shouldHaveBorders { if len(renderedTableContent) == 0 && shouldHaveBorders {
var sb strings.Builder var sb strings.Builder
@@ -1503,7 +1542,7 @@ func (t *Table) render() error {
t.hasPrinted = true t.hasPrinted = true
t.logger.Info("Render() completed.") t.logger.Info("Render() completed.")
return nil // Success return nil
} }
// renderFooter renders the table's footer section with borders and padding. // 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 { if hasTopPadding {
hctx.rowIdx = 0 hctx.rowIdx = 0
hctx.lineIdx = -1 hctx.lineIdx = -1
if !(hasContentAbove && cfg.Settings.Lines.ShowFooterLine.Enabled()) { if !hasContentAbove || !cfg.Settings.Lines.ShowFooterLine.Enabled() {
hctx.location = tw.LocationFirst hctx.location = tw.LocationFirst
} else { } else {
hctx.location = tw.LocationMiddle hctx.location = tw.LocationMiddle
@@ -1699,7 +1738,7 @@ func (t *Table) renderFooter(ctx *renderContext, mctx *mergeContext) error {
hctx.line = padLine(line, ctx.numCols) hctx.line = padLine(line, ctx.numCols)
isFirstContentLine := i == 0 isFirstContentLine := i == 0
isLastContentLine := i == len(ctx.footerLines)-1 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 hctx.location = tw.LocationFirst
} else if isLastContentLine && !hasBottomPaddingConfig { } else if isLastContentLine && !hasBottomPaddingConfig {
hctx.location = tw.LocationEnd hctx.location = tw.LocationEnd
@@ -1716,7 +1755,7 @@ func (t *Table) renderFooter(ctx *renderContext, mctx *mergeContext) error {
if hasBottomPaddingConfig { if hasBottomPaddingConfig {
paddingLineContentForContext = make([]string, ctx.numCols) paddingLineContentForContext = make([]string, ctx.numCols)
formattedPaddingCells := make([]string, ctx.numCols) formattedPaddingCells := make([]string, ctx.numCols)
var representativePadChar string = " " representativePadChar := " "
ctx.logger.Debugf("Constructing Footer Bottom Padding line content strings") ctx.logger.Debugf("Constructing Footer Bottom Padding line content strings")
for j := 0; j < ctx.numCols; j++ { for j := 0; j < ctx.numCols; j++ {
colWd := ctx.widths[tw.Footer].Get(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 == " " { if j == 0 || representativePadChar == " " {
representativePadChar = padChar representativePadChar = padChar
} }
padWidth := twwidth.Width(padChar) padWidth := max(twwidth.Width(padChar), 1)
if padWidth < 1 {
padWidth = 1
}
repeatCount := 0 repeatCount := 0
if colWd > 0 && padWidth > 0 { if colWd > 0 && padWidth > 0 {
repeatCount = colWd / padWidth repeatCount = colWd / padWidth
@@ -2139,19 +2175,21 @@ func (t *Table) renderRow(ctx *renderContext, mctx *mergeContext) error {
hctx.lineIdx = j hctx.lineIdx = j
hctx.line = padLine(visualLineData, ctx.numCols) hctx.line = padLine(visualLineData, ctx.numCols)
if j > 0 { if t.config.Behavior.TrimLine.Enabled() {
visualLineHasActualContent := false if j > 0 {
for kCellIdx, cellContentInVisualLine := range hctx.line { visualLineHasActualContent := false
if t.Trimmer(cellContentInVisualLine) != "" { for kCellIdx, cellContentInVisualLine := range hctx.line {
visualLineHasActualContent = true if t.Trimmer(cellContentInVisualLine) != "" {
ctx.logger.Debug("Visual line [%d][%d] has content in cell %d: '%s'. Not skipping.", i, j, kCellIdx, cellContentInVisualLine) visualLineHasActualContent = true
break ctx.logger.Debug("Visual line [%d][%d] has content in cell %d: '%s'. Not skipping.", i, j, kCellIdx, cellContentInVisualLine)
break
}
} }
}
if !visualLineHasActualContent { if !visualLineHasActualContent {
ctx.logger.Debug("Skipping visual line [%d][%d] as it's entirely blank after trimming. Line: %q", i, j, hctx.line) ctx.logger.Debug("Skipping visual line [%d][%d] as it's entirely blank after trimming. Line: %q", i, j, hctx.line)
continue continue
}
} }
} }
-1
View File
@@ -12,7 +12,6 @@ type CellFormatting struct {
// Deprecated: kept for compatibility // Deprecated: kept for compatibility
// will be removed soon // will be removed soon
Alignment Align // Text alignment within the cell (e.g., Left, Right, Center) Alignment Align // Text alignment within the cell (e.g., Left, Right, Center)
} }
// CellPadding defines padding settings for table cells. // CellPadding defines padding settings for table cells.
+12 -6
View File
@@ -3,12 +3,18 @@
package tw package tw
import ( import (
"math"
"strconv"
"strings"
"unicode"
"unicode/utf8"
"github.com/olekukonko/tablewriter/pkg/twwidth" "github.com/olekukonko/tablewriter/pkg/twwidth"
"math" // For mathematical operations like ceiling // For mathematical operations like ceiling
"strconv" // For string-to-number conversions // For string-to-number conversions
"strings" // For string manipulation utilities // For string manipulation utilities
"unicode" // For Unicode character classification // For Unicode character classification
"unicode/utf8" // For UTF-8 rune handling // For UTF-8 rune handling
) )
// Title normalizes and uppercases a label string for use in headers. // 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. // Pad aligns a string within a specified width using a padding character.
// It truncates if the string is wider than the target width. // 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) sDisplayWidth := twwidth.Width(s)
if sDisplayWidth > totalWidth { if sDisplayWidth > totalWidth {
return twwidth.Truncate(s, totalWidth) // Only truncate if necessary return twwidth.Truncate(s, totalWidth) // Only truncate if necessary
+9 -17
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. // Each iterates over each key-value pair in the map and calls the provided function.
// Does nothing if the map is nil. // Does nothing if the map is nil.
func (m Mapper[K, V]) Each(fn func(K, V)) { func (m Mapper[K, V]) Each(fn func(K, V)) {
if m != nil { for k, v := range m {
for k, v := range m { fn(k, v)
fn(k, v)
}
} }
} }
// Filter returns a new Mapper containing only the key-value pairs that satisfy the predicate. // 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] { func (m Mapper[K, V]) Filter(fn func(K, V) bool) Mapper[K, V] {
result := NewMapper[K, V]() result := NewMapper[K, V]()
if m != nil { for k, v := range m {
for k, v := range m { if fn(k, v) {
if fn(k, v) { result[k] = v
result[k] = v
}
} }
} }
return result 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. // 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] { func (m Mapper[K, V]) MapValues(fn func(V) V) Mapper[K, V] {
result := NewMapper[K, V]() result := NewMapper[K, V]()
if m != nil { for k, v := range m {
for k, v := range m { result[k] = fn(v)
result[k] = fn(v)
}
} }
return result 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. // Clone returns a shallow copy of the Mapper.
func (m Mapper[K, V]) Clone() Mapper[K, V] { func (m Mapper[K, V]) Clone() Mapper[K, V] {
result := NewMapper[K, V]() result := NewMapper[K, V]()
if m != nil { for k, v := range m {
for k, v := range m { result[k] = v
result[k] = v
}
} }
return result return result
} }
+3 -6
View File
@@ -10,9 +10,6 @@ var (
SeparatorsNone = Separators{ShowHeader: Off, ShowFooter: Off, BetweenRows: Off, BetweenColumns: Off} 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 represents standard single-space padding on left/right var PaddingDefault = Padding{Left: " ", Right: " ", Overwrite: true}
// Equivalent to Padding{Left: " ", Right: " ", Overwrite: true}
PaddingDefault = Padding{Left: " ", Right: " ", Overwrite: true}
)
+2 -1
View File
@@ -1,8 +1,9 @@
package tw package tw
import ( import (
"github.com/olekukonko/ll"
"io" "io"
"github.com/olekukonko/ll"
) )
// Renderer defines the interface for rendering tables to an io.Writer. // Renderer defines the interface for rendering tables to an io.Writer.
+13 -26
View File
@@ -1,5 +1,7 @@
package tw package tw
import "slices"
// Slicer is a generic slice type that provides additional methods for slice manipulation. // Slicer is a generic slice type that provides additional methods for slice manipulation.
type Slicer[T any] []T 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. // Each iterates over each element in the slice and calls the provided function.
// Does nothing if the slice is nil. // Does nothing if the slice is nil.
func (s Slicer[T]) Each(fn func(T)) { func (s Slicer[T]) Each(fn func(T)) {
if s != nil { for _, v := range s {
for _, v := range s { fn(v)
fn(v)
}
} }
} }
// Filter returns a new Slicer containing only elements that satisfy the predicate. // Filter returns a new Slicer containing only elements that satisfy the predicate.
func (s Slicer[T]) Filter(fn func(T) bool) Slicer[T] { func (s Slicer[T]) Filter(fn func(T) bool) Slicer[T] {
result := NewSlicer[T]() result := NewSlicer[T]()
if s != nil { for _, v := range s {
for _, v := range s { if fn(v) {
if fn(v) { result = result.Append(v)
result = result.Append(v)
}
} }
} }
return result 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. // Map returns a new Slicer with each element transformed by the provided function.
func (s Slicer[T]) Map(fn func(T) T) Slicer[T] { func (s Slicer[T]) Map(fn func(T) T) Slicer[T] {
result := NewSlicer[T]() result := NewSlicer[T]()
if s != nil { for _, v := range s {
for _, v := range s { result = result.Append(fn(v))
result = result.Append(fn(v))
}
} }
return result return result
} }
// Contains returns true if the slice contains an element that satisfies the predicate. // Contains returns true if the slice contains an element that satisfies the predicate.
func (s Slicer[T]) Contains(fn func(T) bool) bool { func (s Slicer[T]) Contains(fn func(T) bool) bool {
if s != nil { return slices.ContainsFunc(s, fn)
for _, v := range s {
if fn(v) {
return true
}
}
}
return false
} }
// Find returns the first element that satisfies the predicate, along with a boolean indicating if it was found. // 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) { func (s Slicer[T]) Find(fn func(T) bool) (T, bool) {
if s != nil { for _, v := range s {
for _, v := range s { if fn(v) {
if fn(v) { return v, true
return v, true
}
} }
} }
var zero T var zero T
+32 -3
View File
@@ -3,9 +3,12 @@
package tw package tw
import ( import (
"fmt" "bytes"
"github.com/olekukonko/errors" "io"
"strconv"
"strings" "strings"
"github.com/olekukonko/errors"
) // Custom error handling library ) // Custom error handling library
// Position defines where formatting applies in the table (e.g., header, footer, or rows). // 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 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. // Align specifies the text alignment within a table cell.
type Align string type Align string
@@ -52,7 +62,7 @@ func (a Alignment) String() string {
if i > 0 { if i > 0 {
str.WriteString("; ") str.WriteString("; ")
} }
str.WriteString(fmt.Sprint(i)) str.WriteString(strconv.Itoa(i))
str.WriteString("=") str.WriteString("=")
str.WriteString(string(a)) str.WriteString(string(a))
} }
@@ -157,6 +167,7 @@ type Struct struct {
type Behavior struct { type Behavior struct {
AutoHide State // AutoHide determines whether empty columns are hidden. Ignored in streaming mode. 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. 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. Header Control // Header specifies control settings for the table header.
Footer Control // Footer specifies control settings for the table footer. Footer Control // Footer specifies control settings for the table footer.
@@ -212,3 +223,21 @@ func (p Padding) Empty() bool {
func (p Padding) Paddable() bool { func (p Padding) Paddable() bool {
return !p.Empty() || p.Overwrite 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
}
+30 -40
View File
@@ -3,14 +3,15 @@ package tablewriter
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"github.com/olekukonko/errors"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/tablewriter/tw"
"io" "io"
"math" "math"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"github.com/olekukonko/errors"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/tablewriter/tw"
) )
// applyHierarchicalMerges applies hierarchical merges to row content. // 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. // 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 { func (t *Table) buildPaddingLineContents(padChar string, widths tw.Mapper[int, int], numCols int, merges map[int]tw.MergeState) []string {
line := make([]string, numCols) line := make([]string, numCols)
padWidth := twwidth.Width(padChar) padWidth := max(twwidth.Width(padChar), 1)
if padWidth < 1 {
padWidth = 1
}
for j := 0; j < numCols; j++ { for j := 0; j < numCols; j++ {
mergeState := tw.MergeState{} mergeState := tw.MergeState{}
if merges != nil { if merges != nil {
@@ -582,9 +580,9 @@ func (t *Table) calculateAndNormalizeWidths(ctx *renderContext) error {
ctx.numCols, t.config.Behavior.Compact.Merge.Enabled()) ctx.numCols, t.config.Behavior.Compact.Merge.Enabled())
// Initialize width maps // Initialize width maps
//t.headerWidths = tw.NewMapper[int, int]() // t.headerWidths = tw.NewMapper[int, int]()
//t.rowWidths = tw.NewMapper[int, int]() // t.rowWidths = tw.NewMapper[int, int]()
//t.footerWidths = tw.NewMapper[int, int]() // t.footerWidths = tw.NewMapper[int, int]()
// Compute content-based widths for each section // Compute content-based widths for each section
for _, lines := range ctx.headerLines { for _, lines := range ctx.headerLines {
@@ -720,7 +718,7 @@ func (t *Table) calculateAndNormalizeWidths(ctx *renderContext) error {
twwidth.Width(headerCellPadding.Left) + twwidth.Width(headerCellPadding.Left) +
twwidth.Width(headerCellPadding.Right) twwidth.Width(headerCellPadding.Right)
currentSumOfColumnWidths := 0 currentSumOfColumnWidths := 0
workingWidths.Each(func(_ int, w int) { currentSumOfColumnWidths += w }) workingWidths.Each(func(_, w int) { currentSumOfColumnWidths += w })
numSeparatorsInFullSpan := 0 numSeparatorsInFullSpan := 0
if ctx.numCols > 1 { if ctx.numCols > 1 {
if t.renderer != nil && t.renderer.Config().Settings.Separators.BetweenColumns.Enabled() { 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) mergedContentString, actualMergedHeaderContentPhysicalWidth, totalCurrentSpanPhysicalWidth)
shortfall := actualMergedHeaderContentPhysicalWidth - totalCurrentSpanPhysicalWidth shortfall := actualMergedHeaderContentPhysicalWidth - totalCurrentSpanPhysicalWidth
numNonZeroCols := 0 numNonZeroCols := 0
workingWidths.Each(func(_ int, w int) { workingWidths.Each(func(_, w int) {
if w > 0 { if w > 0 {
numNonZeroCols++ numNonZeroCols++
} }
@@ -744,7 +742,7 @@ func (t *Table) calculateAndNormalizeWidths(ctx *renderContext) error {
if numNonZeroCols > 0 && shortfall > 0 { if numNonZeroCols > 0 && shortfall > 0 {
extraPerColumn := int(math.Ceil(float64(shortfall) / float64(numNonZeroCols))) extraPerColumn := int(math.Ceil(float64(shortfall) / float64(numNonZeroCols)))
finalSumAfterExpansion := 0 finalSumAfterExpansion := 0
workingWidths.Each(func(colIdx int, currentW int) { workingWidths.Each(func(colIdx, currentW int) {
if currentW > 0 || (numNonZeroCols == ctx.numCols && ctx.numCols > 0) { if currentW > 0 || (numNonZeroCols == ctx.numCols && ctx.numCols > 0) {
newWidth := currentW + extraPerColumn newWidth := currentW + extraPerColumn
workingWidths.Set(colIdx, newWidth) workingWidths.Set(colIdx, newWidth)
@@ -782,7 +780,7 @@ func (t *Table) calculateAndNormalizeWidths(ctx *renderContext) error {
if t.config.Widths.Global > 0 { if t.config.Widths.Global > 0 {
ctx.logger.Debugf("Applying global width constraint: %d", t.config.Widths.Global) ctx.logger.Debugf("Applying global width constraint: %d", t.config.Widths.Global)
currentSumOfFinalColWidths := 0 currentSumOfFinalColWidths := 0
finalWidths.Each(func(_ int, w int) { currentSumOfFinalColWidths += w }) finalWidths.Each(func(_, w int) { currentSumOfFinalColWidths += w })
numSeparators := 0 numSeparators := 0
if ctx.numCols > 1 && t.renderer != nil && t.renderer.Config().Settings.Separators.BetweenColumns.Enabled() { 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()) 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 totalCurrentTablePhysicalWidth := currentSumOfFinalColWidths + numSeparators
if totalCurrentTablePhysicalWidth > t.config.Widths.Global { if totalCurrentTablePhysicalWidth > t.config.Widths.Global {
ctx.logger.Debugf("Table width %d exceeds global limit %d. Shrinking.", 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 targetTotalColumnContentWidth := max(t.config.Widths.Global-numSeparators, 0)
if targetTotalColumnContentWidth < 0 {
targetTotalColumnContentWidth = 0
}
if ctx.numCols > 0 && targetTotalColumnContentWidth < ctx.numCols { if ctx.numCols > 0 && targetTotalColumnContentWidth < ctx.numCols {
targetTotalColumnContentWidth = ctx.numCols targetTotalColumnContentWidth = ctx.numCols
} }
hardMinimums := tw.NewMapper[int, int]() hardMinimums := tw.NewMapper[int, int]()
sumOfHardMinimums := 0 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++ { for i := 0; i < ctx.numCols; i++ {
minW := 1 minW := 1
if isHeaderContentHardToWrap && len(ctx.headerLines) > 0 { if isHeaderContentHardToWrap && len(ctx.headerLines) > 0 {
@@ -820,7 +815,7 @@ func (t *Table) calculateAndNormalizeWidths(ctx *renderContext) error {
} }
tempSum := 0 tempSum := 0
scaledHardMinimums := tw.NewMapper[int, int]() 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)) scaledMinW := int(math.Round(float64(currentMinW) * scaleFactorMin))
if scaledMinW < 1 && targetTotalColumnContentWidth > 0 { if scaledMinW < 1 && targetTotalColumnContentWidth > 0 {
scaledMinW = 1 scaledMinW = 1
@@ -894,31 +889,29 @@ func (t *Table) calculateAndNormalizeWidths(ctx *renderContext) error {
if errorInDist < 0 { if errorInDist < 0 {
adj = -1 adj = -1
} }
if !(adj < 0 && w+adj < hardMinimums.Get(colToAdjust)) { if adj >= 0 || w+adj >= hardMinimums.Get(colToAdjust) {
finalWidths.Set(colToAdjust, w+adj) finalWidths.Set(colToAdjust, w+adj)
} else if adj > 0 { } else if adj > 0 {
finalWidths.Set(colToAdjust, w+adj) finalWidths.Set(colToAdjust, w+adj)
} }
} }
} }
} else { } else if ctx.numCols > 0 {
if ctx.numCols > 0 { extraPerCol := remainingWidthToDistribute / ctx.numCols
extraPerCol := remainingWidthToDistribute / ctx.numCols rem := remainingWidthToDistribute % ctx.numCols
rem := remainingWidthToDistribute % ctx.numCols for i := 0; i < ctx.numCols; i++ {
for i := 0; i < ctx.numCols; i++ { currentW := finalWidths.Get(i)
currentW := finalWidths.Get(i) add := extraPerCol
add := extraPerCol if i < rem {
if i < rem { add++
add++
}
finalWidths.Set(i, currentW+add)
} }
finalWidths.Set(i, currentW+add)
} }
} }
} }
} }
finalSumCheck := 0 finalSumCheck := 0
finalWidths.Each(func(idx int, w int) { finalWidths.Each(func(idx, w int) {
if w < 1 && targetTotalColumnContentWidth > 0 { if w < 1 && targetTotalColumnContentWidth > 0 {
finalWidths.Set(idx, 1) finalWidths.Set(idx, 1)
} else if w < 0 { } else if w < 0 {
@@ -945,10 +938,7 @@ func (t *Table) calculateContentMaxWidth(colIdx int, config tw.CellConfig, padLe
if isStreaming { if isStreaming {
// Existing streaming logic remains unchanged // Existing streaming logic remains unchanged
totalColumnWidthFromStream := t.streamWidths.Get(colIdx) totalColumnWidthFromStream := max(t.streamWidths.Get(colIdx), 0)
if totalColumnWidthFromStream < 0 {
totalColumnWidthFromStream = 0
}
effectiveContentMaxWidth = totalColumnWidthFromStream - padLeftWidth - padRightWidth effectiveContentMaxWidth = totalColumnWidthFromStream - padLeftWidth - padRightWidth
if effectiveContentMaxWidth < 1 && totalColumnWidthFromStream > (padLeftWidth+padRightWidth) { if effectiveContentMaxWidth < 1 && totalColumnWidthFromStream > (padLeftWidth+padRightWidth) {
effectiveContentMaxWidth = 1 effectiveContentMaxWidth = 1
@@ -1252,7 +1242,7 @@ func (t *Table) convertCellsToStrings(rowInput interface{}, cellCfg tw.CellConfi
var err error var err error
switch v := rowInput.(type) { switch v := rowInput.(type) {
//Directly supported slice types // Directly supported slice types
case []string: case []string:
cells = v cells = v
case []interface{}: // Catches variadic simple types grouped by Append 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() 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. // These are now dispatched to convertItemToCells by the default case.
// Keeping direct tw.Formatter and fmt.Stringer here could be a micro-optimization // Keeping direct tw.Formatter and fmt.Stringer here could be a micro-optimization
// if `rowInput` is *exactly* that type (not a struct implementing it), // 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 // 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) cells, err := t.convertCellsToStrings(row, config)
if err != nil { if err != nil {
return nil, err return nil, err
+1 -1
View File
@@ -1165,7 +1165,7 @@ github.com/olekukonko/errors
github.com/olekukonko/ll github.com/olekukonko/ll
github.com/olekukonko/ll/lh github.com/olekukonko/ll/lh
github.com/olekukonko/ll/lx github.com/olekukonko/ll/lx
# github.com/olekukonko/tablewriter v1.0.9 # github.com/olekukonko/tablewriter v1.1.0
## explicit; go 1.21 ## explicit; go 1.21
github.com/olekukonko/tablewriter github.com/olekukonko/tablewriter
github.com/olekukonko/tablewriter/pkg/twwarp github.com/olekukonko/tablewriter/pkg/twwarp