sq/cli/output/tablew/internal/texttable.go
Neil O'Toole 7c56377b40
Struct alignment (#369)
* Field alignment
2024-01-27 00:11:24 -07:00

590 lines
12 KiB
Go

// Copyright 2014 Oleku Konko All rights reserved.
// Use of this source code is governed by a MIT
// license that can be found in the LICENSE file.
// This module is a Table writer API for the Go Programming Language.
// The protocols were written in pure Go and works on windows and unix systems
// Package tablewriter creates & generates text based table
package internal
import (
"bytes"
"context"
"fmt"
"io"
"regexp"
"strings"
"unicode"
)
// MaxRowWidth defines maximum row width
const (
MaxRowWidth = 30
)
const (
// BorderCenterChar defines char at center of border
BorderCenterChar = "+"
// BorderRowChar defines char of row border
BorderRowChar = "-"
// BorderColumnChar defines char of column border
BorderColumnChar = "|"
// Empty defines no padding space or border
Empty = ""
// Space defines space char for padding and borders
Space = " "
)
// AlignDefault and other alignment constants
const (
AlignDefault = iota
AlignLeft
AlignCenter
AlignRight
)
var (
decimal = regexp.MustCompile(`^-*\d*\.?\d*$`)
percent = regexp.MustCompile(`^-*\d*\.?\d*$%$`)
)
// Border struct
type Border struct {
Left bool
Right bool
Top bool
Bottom bool
}
// Table struct
type Table struct {
out io.Writer
cs map[int]int
rs map[int]int
colTrans map[int]textTransFunc
cellTrans map[string]textTransFunc
headerTrans textTransFunc
pCenter string
pRow string
pColumn string
rows [][]string
lines [][][]string
headers []string
footers []string
mW int
tColumn int
tRow int
hAlign int
fAlign int
align int
colSize int
borders Border
autoFmt bool
autoWrap bool
rowLine bool
hdrLine bool
hdrDisable bool
}
// NewTable returns a new table that writes to writer.
func NewTable(writer io.Writer) *Table {
t := &Table{
out: writer,
rows: [][]string{},
lines: [][][]string{},
cs: make(map[int]int),
rs: make(map[int]int),
headers: []string{},
footers: []string{},
autoFmt: true,
autoWrap: true,
mW: MaxRowWidth,
pCenter: BorderCenterChar,
pRow: BorderRowChar,
pColumn: BorderColumnChar,
tColumn: -1,
tRow: -1,
hAlign: AlignDefault,
fAlign: AlignDefault,
align: AlignDefault,
rowLine: false,
hdrLine: false,
hdrDisable: false,
borders: Border{Left: true, Right: true, Bottom: true, Top: true},
colSize: -1,
colTrans: make(map[int]textTransFunc),
cellTrans: make(map[string]textTransFunc),
headerTrans: fmt.Sprint,
}
return t
}
// SetColTrans sets the column transformer.
func (t *Table) SetColTrans(col int, trans textTransFunc) {
t.colTrans[col] = trans
}
// SetCellTrans sets the cell transformer.
//
// REVISIT: does this even work? How does it interact with SetColTrans?
func (t *Table) SetCellTrans(row, col int, trans textTransFunc) {
t.cellTrans[fmt.Sprintf("[%v][%v]", row, col)] = trans
}
// SetHeaderTrans sets the transformer for the header row.
func (t *Table) SetHeaderTrans(trans textTransFunc) {
t.headerTrans = trans
}
func (t *Table) getCellTrans(row, col int) textTransFunc {
colTrans := t.getColTrans(col)
key := fmt.Sprintf("[%v][%v]", row, col)
cellTrans := t.cellTrans[key]
if cellTrans == nil {
cellTrans = func(val ...any) string {
return fmt.Sprint(val...)
}
}
return func(val ...any) string {
return cellTrans(colTrans(val...))
}
}
func (t *Table) getColTrans(col int) textTransFunc {
trans := t.colTrans[col]
if trans != nil {
return trans
}
return func(val ...any) string {
return fmt.Sprint(val...)
}
}
// RenderAll table output
func (t *Table) RenderAll(ctx context.Context) error {
if t.borders.Top {
t.printLine(true)
}
t.printHeading()
if err := t.printRows(ctx); err != nil {
return err
}
if !t.rowLine && t.borders.Bottom {
t.printLine(true)
}
t.printFooter()
return nil
}
// SetHeader sets table header
func (t *Table) SetHeader(keys []string) {
t.colSize = len(keys)
for i, v := range keys {
t.parseDimension(v, i, -1)
t.headers = append(t.headers, v)
}
}
// SetFooter sets table Footer
func (t *Table) SetFooter(keys []string) {
// t.colSize = len(keys)
for i, v := range keys {
t.parseDimension(v, i, -1)
t.footers = append(t.footers, v)
}
}
// SetAutoFormatHeaders turns header autoformatting on/off. Default is on (true).
func (t *Table) SetAutoFormatHeaders(auto bool) {
t.autoFmt = auto
}
// SetAutoWrapText turns automatic multiline text adjustment on/off. Default is on (true).
func (t *Table) SetAutoWrapText(auto bool) {
t.autoWrap = auto
}
// SetColWidth sets the default column width
func (t *Table) SetColWidth(width int) {
t.mW = width
}
// SetColumnSeparator sets the Column Separator
func (t *Table) SetColumnSeparator(sep string) {
t.pColumn = sep
}
// SetRowSeparator sets the Row Separator
func (t *Table) SetRowSeparator(sep string) {
t.pRow = sep
}
// SetCenterSeparator sets the center Separator
func (t *Table) SetCenterSeparator(sep string) {
t.pCenter = sep
}
// SetHeaderAlignment sets Header Alignment
func (t *Table) SetHeaderAlignment(hAlign int) {
t.hAlign = hAlign
}
// SetFooterAlignment sets Footer Alignment
func (t *Table) SetFooterAlignment(fAlign int) {
t.fAlign = fAlign
}
// SetAlignment sets Table Alignment
func (t *Table) SetAlignment(align int) {
t.align = align
}
// SetHeaderLine enable / disable a line after the header
func (t *Table) SetHeaderLine(line bool) {
t.hdrLine = line
}
// SetDisableHeader enable / disable printing of headers
func (t *Table) SetHeaderDisable(disable bool) {
t.hdrDisable = disable
}
// SetRowLine enable / disable a line on each row of the table
func (t *Table) SetRowLine(line bool) {
t.rowLine = line
}
// SetBorder enable / disable line around the table
func (t *Table) SetBorder(border bool) {
t.SetBorders(Border{border, border, border, border})
}
// SetBorders sets borders
func (t *Table) SetBorders(border Border) {
t.borders = border
}
// Append row to table
func (t *Table) Append(row []string) {
rowSize := len(t.headers)
if rowSize > t.colSize {
t.colSize = rowSize
}
n := len(t.lines)
line := [][]string{}
for i, v := range row {
// Detect string width
// Detect String height
// Break strings into words
out := t.parseDimension(v, i, n)
// Append broken words
line = append(line, out)
}
t.lines = append(t.lines, line)
}
// AppendBulk allows support for Bulk Append
// Eliminates repeated for loops
func (t *Table) AppendBulk(rows [][]string) {
for _, row := range rows {
t.Append(row)
}
}
// Print line based on row width
func (t *Table) printLine(nl bool) {
fmt.Fprint(t.out, t.pCenter)
for i := 0; i < len(t.cs); i++ {
v := t.cs[i]
fmt.Fprintf(t.out, "%s%s%s%s",
t.pRow,
strings.Repeat(string(t.pRow), v),
t.pRow,
t.pCenter)
}
if nl {
fmt.Fprintln(t.out)
}
}
// Return the PadRight function if align is left, PadLeft if align is right,
// and Pad by default
func pad(align int) func(string, string, int) string {
padFunc := Pad
switch align {
case AlignLeft:
padFunc = PadRight
case AlignRight:
padFunc = PadLeft
}
return padFunc
}
// Print heading information
func (t *Table) printHeading() {
// Check if headers is available
if len(t.headers) < 1 || t.hdrDisable {
return
}
buf := &bytes.Buffer{}
// Check if border is set
// Replace with space if not set
fmt.Fprint(buf, ConditionString(t.borders.Left, t.pColumn, Empty))
fmt.Fprint(t.out, strings.TrimRightFunc(buf.String(), unicode.IsSpace))
// Identify last column
end := len(t.cs) - 1
// Get pad function
padFunc := pad(t.hAlign)
// Print Heading column
for i := 0; i <= end; i++ {
v := t.cs[i]
h := t.headers[i]
if t.autoFmt {
h = Title(h)
}
pad := ConditionString(i == end && !t.borders.Left, Space, t.pColumn)
var head string
if i == end {
// Trim the padding from the final column
// head = strings.TrimRightFunc(head, unicode.IsPunct)
head = t.headerTrans(h)
} else {
head = t.headerTrans(fmt.Sprintf("%s %s ", padFunc(h, Space, v), pad))
}
fmt.Fprint(t.out, head)
}
// Next line
fmt.Fprintln(t.out)
if t.hdrLine {
t.printLine(true)
}
}
// Print heading information
func (t *Table) printFooter() {
// Check if headers is available
if len(t.footers) < 1 {
return
}
// Only print line if border is not set
if !t.borders.Bottom {
t.printLine(true)
}
// Check if border is set
// Replace with space if not set
fmt.Fprint(t.out, ConditionString(t.borders.Bottom, t.pColumn, Space))
// Identify last column
end := len(t.cs) - 1
// Get pad function
padFunc := pad(t.fAlign)
// Print Heading column
for i := 0; i <= end; i++ {
v := t.cs[i]
f := t.footers[i]
if t.autoFmt {
f = Title(f)
}
pad := ConditionString(i == end && !t.borders.Top, Space, t.pColumn)
if len(t.footers[i]) == 0 {
pad = Space
}
fmt.Fprintf(t.out, " %s %s",
padFunc(f, Space, v),
pad)
}
// Next line
fmt.Fprintln(t.out)
hasPrinted := false
for i := 0; i <= end; i++ {
v := t.cs[i]
pad := t.pRow
center := t.pCenter
length := len(t.footers[i])
if length > 0 {
hasPrinted = true
}
// Set center to be space if length is 0
if length == 0 && !t.borders.Right {
center = Space
}
// Print first junction
if i == 0 {
fmt.Fprint(t.out, center)
}
// Pad With space of length is 0
if length == 0 {
pad = Space
}
// Ignore left space of it has printed before
if hasPrinted || t.borders.Left {
pad = t.pRow
center = t.pCenter
}
// Change Center start position
if center == Space {
if i < end && len(t.footers[i+1]) != 0 {
center = t.pCenter
}
}
// Print the footer
fmt.Fprintf(t.out, "%s%s%s%s",
pad,
strings.Repeat(string(pad), v),
pad,
center)
}
fmt.Fprintln(t.out)
}
func (t *Table) printRows(ctx context.Context) error {
for i, lines := range t.lines {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
t.printRow(lines, i)
}
return nil
}
// Print Row Information
func (t *Table) printRow(columns [][]string, colKey int) {
// Get Maximum Height
max := t.rs[colKey]
total := len(columns)
// Pad Each Height
for i, line := range columns {
length := len(line)
pad := max - length
for n := 0; n < pad; n++ {
columns[i] = append(columns[i], " ")
}
}
for x := 0; x < max; x++ {
for y := 0; y < total; y++ {
// Check if border is set
fmt.Fprint(t.out, ConditionString(!t.borders.Left && y == 0, Empty, t.pColumn))
text := columns[y][x]
tran := t.getCellTrans(colKey, y)
// This would print alignment
// Default alignment would use multiple configuration
switch t.align {
case AlignCenter: //
fmt.Fprintf(t.out, "%s", Pad(text, Space, t.cs[y]))
case AlignRight:
fmt.Fprintf(t.out, "%s", PadLeft(text, Space, t.cs[y]))
case AlignLeft:
cellContent := text
if y != total-1 {
cellContent = PadRight(text, Space, t.cs[y])
fmt.Fprintf(t.out, tran("%s "), cellContent)
} else {
fmt.Fprintf(t.out, tran(strings.TrimRightFunc(cellContent, unicode.IsSpace)))
}
default:
if decimal.MatchString(strings.TrimSpace(text)) || percent.MatchString(strings.TrimSpace(text)) {
fmt.Fprintf(t.out, "%s", PadLeft(text, Space, t.cs[y]))
} else {
fmt.Fprintf(t.out, "%s", PadRight(text, Space, t.cs[y]))
}
}
}
// Check if border is set
// Replace with space if not set
// fmt.Fprint(t.out, ConditionString(t.borders.Left, t.pColumn, Space))
fmt.Fprintln(t.out)
}
if t.rowLine {
t.printLine(true)
}
}
func (t *Table) parseDimension(str string, colKey, rowKey int) []string {
var (
raw []string
max int
)
w := DisplayWidth(str)
// Calculate Width
// Check if with is grater than maximum width
if w > t.mW {
w = t.mW
}
// Check if width exists
v, ok := t.cs[colKey]
if !ok || v < w || v == 0 {
t.cs[colKey] = w
}
if rowKey == -1 {
return raw
}
// Calculate Height
if t.autoWrap {
raw, _ = WrapString(str, t.cs[colKey])
} else {
raw = getLines(str)
}
for _, line := range raw {
if w := DisplayWidth(line); w > max {
max = w
}
}
// Make sure the with is the same length as maximum word
// Important for cases where the width is smaller than maxu word
if max > t.cs[colKey] {
t.cs[colKey] = max
}
h := len(raw)
v, ok = t.rs[rowKey]
if !ok || v < h || v == 0 {
t.rs[rowKey] = h
}
return raw
}
// textTransFunc is a function that can transform text, typically
// to add color.
type textTransFunc func(a ...any) string