feat: table 的输出
This commit is contained in:
parent
d4cfa79dfc
commit
30c596f8ef
101
helper/table/config.go
Normal file
101
helper/table/config.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package table
|
||||||
|
|
||||||
|
import "git.hexq.cn/tiglog/golib/helper/text"
|
||||||
|
|
||||||
|
// ColumnConfig contains configurations that determine and modify the way the
|
||||||
|
// contents of the column get rendered.
|
||||||
|
type ColumnConfig struct {
|
||||||
|
// Name is the name of the Column as it appears in the first Header row.
|
||||||
|
// If a Header is not provided, or the name is not found in the header, this
|
||||||
|
// will not work.
|
||||||
|
Name string
|
||||||
|
// Number is the Column # from left. When specified, it overrides the Name
|
||||||
|
// property. If you know the exact Column number, use this instead of Name.
|
||||||
|
Number int
|
||||||
|
|
||||||
|
// Align defines the horizontal alignment
|
||||||
|
Align text.Align
|
||||||
|
// AlignFooter defines the horizontal alignment of Footer rows
|
||||||
|
AlignFooter text.Align
|
||||||
|
// AlignHeader defines the horizontal alignment of Header rows
|
||||||
|
AlignHeader text.Align
|
||||||
|
|
||||||
|
// AutoMerge merges cells with similar values and prevents separators from
|
||||||
|
// being drawn. Caveats:
|
||||||
|
// * VAlign is applied on the individual cell and not on the merged cell
|
||||||
|
// * Does not work in CSV/HTML/Markdown render modes
|
||||||
|
// * Does not work well with horizontal auto-merge (RowConfig.AutoMerge)
|
||||||
|
//
|
||||||
|
// Works best when:
|
||||||
|
// * Style().Options.SeparateRows == true
|
||||||
|
// * Style().Color.Row == Style().Color.RowAlternate (or not set)
|
||||||
|
AutoMerge bool
|
||||||
|
|
||||||
|
// Colors defines the colors to be used on the column
|
||||||
|
Colors text.Colors
|
||||||
|
// ColorsFooter defines the colors to be used on the column in Footer rows
|
||||||
|
ColorsFooter text.Colors
|
||||||
|
// ColorsHeader defines the colors to be used on the column in Header rows
|
||||||
|
ColorsHeader text.Colors
|
||||||
|
|
||||||
|
// Hidden when set to true will prevent the column from being rendered.
|
||||||
|
// This is useful in cases like needing a column for sorting, but not for
|
||||||
|
// display.
|
||||||
|
Hidden bool
|
||||||
|
|
||||||
|
// Transformer is a custom-function that changes the way the value gets
|
||||||
|
// rendered to the console. Refer to text/transformer.go for ready-to-use
|
||||||
|
// Transformer functions.
|
||||||
|
Transformer text.Transformer
|
||||||
|
// TransformerFooter is like Transformer but for Footer rows
|
||||||
|
TransformerFooter text.Transformer
|
||||||
|
// TransformerHeader is like Transformer but for Header rows
|
||||||
|
TransformerHeader text.Transformer
|
||||||
|
|
||||||
|
// VAlign defines the vertical alignment
|
||||||
|
VAlign text.VAlign
|
||||||
|
// VAlignFooter defines the vertical alignment in Footer rows
|
||||||
|
VAlignFooter text.VAlign
|
||||||
|
// VAlignHeader defines the vertical alignment in Header rows
|
||||||
|
VAlignHeader text.VAlign
|
||||||
|
|
||||||
|
// WidthMax defines the maximum character length of the column
|
||||||
|
WidthMax int
|
||||||
|
// WidthEnforcer enforces the WidthMax value on the column contents;
|
||||||
|
// default: text.WrapText
|
||||||
|
WidthMaxEnforcer WidthEnforcer
|
||||||
|
// WidthMin defines the minimum character length of the column
|
||||||
|
WidthMin int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ColumnConfig) getWidthMaxEnforcer() WidthEnforcer {
|
||||||
|
if c.WidthMax == 0 {
|
||||||
|
return widthEnforcerNone
|
||||||
|
}
|
||||||
|
if c.WidthMaxEnforcer != nil {
|
||||||
|
return c.WidthMaxEnforcer
|
||||||
|
}
|
||||||
|
return text.WrapText
|
||||||
|
}
|
||||||
|
|
||||||
|
// RowConfig contains configurations that determine and modify the way the
|
||||||
|
// contents of a row get rendered.
|
||||||
|
type RowConfig struct {
|
||||||
|
// AutoMerge merges cells with similar values and prevents separators from
|
||||||
|
// being drawn. Caveats:
|
||||||
|
// * Align is overridden to text.AlignCenter on the merged cell (unless set
|
||||||
|
// by AutoMergeAlign value below)
|
||||||
|
// * Does not work in CSV/HTML/Markdown render modes
|
||||||
|
// * Does not work well with vertical auto-merge (ColumnConfig.AutoMerge)
|
||||||
|
AutoMerge bool
|
||||||
|
|
||||||
|
// Alignment to use on a merge (defaults to text.AlignCenter)
|
||||||
|
AutoMergeAlign text.Align
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc RowConfig) getAutoMergeAlign() text.Align {
|
||||||
|
if rc.AutoMergeAlign == text.AlignDefault {
|
||||||
|
return text.AlignCenter
|
||||||
|
}
|
||||||
|
return rc.AutoMergeAlign
|
||||||
|
}
|
75
helper/table/config_test.go
Normal file
75
helper/table/config_test.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
//
|
||||||
|
// config_test.go
|
||||||
|
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
||||||
|
//
|
||||||
|
// Distributed under terms of the MIT license.
|
||||||
|
//
|
||||||
|
|
||||||
|
package table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.hexq.cn/tiglog/golib/helper/text"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestColumnConfig_getWidthMaxEnforcer(t *testing.T) {
|
||||||
|
t.Run("no width enforcer", func(t *testing.T) {
|
||||||
|
cc := ColumnConfig{}
|
||||||
|
|
||||||
|
widthEnforcer := cc.getWidthMaxEnforcer()
|
||||||
|
assert.Equal(t, "1234567890", widthEnforcer("1234567890", 0))
|
||||||
|
assert.Equal(t, "1234567890", widthEnforcer("1234567890", 1))
|
||||||
|
assert.Equal(t, "1234567890", widthEnforcer("1234567890", 5))
|
||||||
|
assert.Equal(t, "1234567890", widthEnforcer("1234567890", 10))
|
||||||
|
assert.Equal(t, "1234567890", widthEnforcer("1234567890", 100))
|
||||||
|
assert.Equal(t, "1234567890", widthEnforcer("1234567890", 1000))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("default width enforcer", func(t *testing.T) {
|
||||||
|
cc := ColumnConfig{
|
||||||
|
WidthMax: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
widthEnforcer := cc.getWidthMaxEnforcer()
|
||||||
|
assert.Equal(t, "", widthEnforcer("1234567890", 0))
|
||||||
|
assert.Equal(t, "1\n2\n3\n4\n5\n6\n7\n8\n9\n0", widthEnforcer("1234567890", 1))
|
||||||
|
assert.Equal(t, "12345\n67890", widthEnforcer("1234567890", 5))
|
||||||
|
assert.Equal(t, "1234567890", widthEnforcer("1234567890", 10))
|
||||||
|
assert.Equal(t, "1234567890", widthEnforcer("1234567890", 100))
|
||||||
|
assert.Equal(t, "1234567890", widthEnforcer("1234567890", 1000))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("custom width enforcer (1)", func(t *testing.T) {
|
||||||
|
cc := ColumnConfig{
|
||||||
|
WidthMax: 10,
|
||||||
|
WidthMaxEnforcer: text.Trim,
|
||||||
|
}
|
||||||
|
|
||||||
|
widthEnforcer := cc.getWidthMaxEnforcer()
|
||||||
|
assert.Equal(t, text.Trim("1234567890", 0), widthEnforcer("1234567890", 0))
|
||||||
|
assert.Equal(t, text.Trim("1234567890", 1), widthEnforcer("1234567890", 1))
|
||||||
|
assert.Equal(t, text.Trim("1234567890", 5), widthEnforcer("1234567890", 5))
|
||||||
|
assert.Equal(t, text.Trim("1234567890", 10), widthEnforcer("1234567890", 10))
|
||||||
|
assert.Equal(t, text.Trim("1234567890", 100), widthEnforcer("1234567890", 100))
|
||||||
|
assert.Equal(t, text.Trim("1234567890", 1000), widthEnforcer("1234567890", 1000))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("custom width enforcer (2)", func(t *testing.T) {
|
||||||
|
cc := ColumnConfig{
|
||||||
|
WidthMax: 10,
|
||||||
|
WidthMaxEnforcer: func(col string, maxLen int) string {
|
||||||
|
return "foo"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
widthEnforcer := cc.getWidthMaxEnforcer()
|
||||||
|
assert.Equal(t, "foo", widthEnforcer("1234567890", 0))
|
||||||
|
assert.Equal(t, "foo", widthEnforcer("1234567890", 1))
|
||||||
|
assert.Equal(t, "foo", widthEnforcer("1234567890", 5))
|
||||||
|
assert.Equal(t, "foo", widthEnforcer("1234567890", 10))
|
||||||
|
assert.Equal(t, "foo", widthEnforcer("1234567890", 100))
|
||||||
|
assert.Equal(t, "foo", widthEnforcer("1234567890", 1000))
|
||||||
|
})
|
||||||
|
}
|
408
helper/table/render.go
Normal file
408
helper/table/render.go
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
package table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"git.hexq.cn/tiglog/golib/helper/text"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Render renders the Table in a human-readable "pretty" format. Example:
|
||||||
|
//
|
||||||
|
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
|
||||||
|
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||||
|
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||||
|
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||||
|
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||||
|
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||||
|
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||||
|
// │ │ │ TOTAL │ 10000 │ │
|
||||||
|
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
|
||||||
|
func (t *Table) Render() string {
|
||||||
|
t.initForRender()
|
||||||
|
|
||||||
|
var out strings.Builder
|
||||||
|
if t.numColumns > 0 {
|
||||||
|
t.renderTitle(&out)
|
||||||
|
|
||||||
|
// top-most border
|
||||||
|
t.renderRowsBorderTop(&out)
|
||||||
|
|
||||||
|
// header rows
|
||||||
|
t.renderRowsHeader(&out)
|
||||||
|
|
||||||
|
// (data) rows
|
||||||
|
t.renderRows(&out, t.rows, renderHint{})
|
||||||
|
|
||||||
|
// footer rows
|
||||||
|
t.renderRowsFooter(&out)
|
||||||
|
|
||||||
|
// bottom-most border
|
||||||
|
t.renderRowsBorderBottom(&out)
|
||||||
|
|
||||||
|
// caption
|
||||||
|
if t.caption != "" {
|
||||||
|
out.WriteRune('\n')
|
||||||
|
out.WriteString(t.caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t.render(&out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) renderColumn(out *strings.Builder, row rowStr, colIdx int, maxColumnLength int, hint renderHint) int {
|
||||||
|
numColumnsRendered := 1
|
||||||
|
|
||||||
|
// when working on the first column, and autoIndex is true, insert a new
|
||||||
|
// column with the row number on it.
|
||||||
|
if colIdx == 0 && t.autoIndex {
|
||||||
|
hintAutoIndex := hint
|
||||||
|
hintAutoIndex.isAutoIndexColumn = true
|
||||||
|
t.renderColumnAutoIndex(out, hintAutoIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// when working on column number 2 or more, render the column separator
|
||||||
|
if colIdx > 0 {
|
||||||
|
t.renderColumnSeparator(out, row, colIdx, hint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract the text, convert-case if not-empty and align horizontally
|
||||||
|
mergeVertically := t.shouldMergeCellsVertically(colIdx, hint)
|
||||||
|
var colStr string
|
||||||
|
if mergeVertically {
|
||||||
|
// leave colStr empty; align will expand the column as necessary
|
||||||
|
} else if colIdx < len(row) {
|
||||||
|
colStr = t.getFormat(hint).Apply(row[colIdx])
|
||||||
|
}
|
||||||
|
align := t.getAlign(colIdx, hint)
|
||||||
|
|
||||||
|
// if horizontal cell merges are enabled, look ahead and see how many cells
|
||||||
|
// have the same content and merge them all until a cell with a different
|
||||||
|
// content is found; override alignment to Center in this case
|
||||||
|
rowConfig := t.getRowConfig(hint)
|
||||||
|
if rowConfig.AutoMerge && !hint.isSeparatorRow {
|
||||||
|
// get the real row to consider all lines in each column instead of just
|
||||||
|
// looking at the current "line"
|
||||||
|
rowUnwrapped := t.getRow(hint.rowNumber-1, hint)
|
||||||
|
for idx := colIdx + 1; idx < len(rowUnwrapped); idx++ {
|
||||||
|
if rowUnwrapped[colIdx] != rowUnwrapped[idx] {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
align = rowConfig.getAutoMergeAlign()
|
||||||
|
maxColumnLength += t.getMaxColumnLengthForMerging(idx)
|
||||||
|
numColumnsRendered++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
colStr = align.Apply(colStr, maxColumnLength)
|
||||||
|
|
||||||
|
// pad both sides of the column
|
||||||
|
if !hint.isSeparatorRow || (hint.isSeparatorRow && mergeVertically) {
|
||||||
|
colStr = t.style.Box.PaddingLeft + colStr + t.style.Box.PaddingRight
|
||||||
|
}
|
||||||
|
|
||||||
|
t.renderColumnColorized(out, colIdx, colStr, hint)
|
||||||
|
|
||||||
|
return colIdx + numColumnsRendered
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) renderColumnAutoIndex(out *strings.Builder, hint renderHint) {
|
||||||
|
var outAutoIndex strings.Builder
|
||||||
|
outAutoIndex.Grow(t.maxColumnLengths[0])
|
||||||
|
|
||||||
|
if hint.isSeparatorRow {
|
||||||
|
numChars := t.autoIndexVIndexMaxLength + utf8.RuneCountInString(t.style.Box.PaddingLeft) +
|
||||||
|
utf8.RuneCountInString(t.style.Box.PaddingRight)
|
||||||
|
chars := t.style.Box.MiddleHorizontal
|
||||||
|
if hint.isAutoIndexColumn && hint.isHeaderOrFooterSeparator() {
|
||||||
|
chars = text.RepeatAndTrim(" ", len(t.style.Box.MiddleHorizontal))
|
||||||
|
}
|
||||||
|
outAutoIndex.WriteString(text.RepeatAndTrim(chars, numChars))
|
||||||
|
} else {
|
||||||
|
outAutoIndex.WriteString(t.style.Box.PaddingLeft)
|
||||||
|
rowNumStr := fmt.Sprint(hint.rowNumber)
|
||||||
|
if hint.isHeaderRow || hint.isFooterRow || hint.rowLineNumber > 1 {
|
||||||
|
rowNumStr = strings.Repeat(" ", t.autoIndexVIndexMaxLength)
|
||||||
|
}
|
||||||
|
outAutoIndex.WriteString(text.AlignRight.Apply(rowNumStr, t.autoIndexVIndexMaxLength))
|
||||||
|
outAutoIndex.WriteString(t.style.Box.PaddingRight)
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.style.Color.IndexColumn != nil {
|
||||||
|
colors := t.style.Color.IndexColumn
|
||||||
|
if hint.isFooterRow {
|
||||||
|
colors = t.style.Color.Footer
|
||||||
|
}
|
||||||
|
out.WriteString(colors.Sprint(outAutoIndex.String()))
|
||||||
|
} else {
|
||||||
|
out.WriteString(outAutoIndex.String())
|
||||||
|
}
|
||||||
|
hint.isAutoIndexColumn = true
|
||||||
|
t.renderColumnSeparator(out, rowStr{}, 0, hint)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) renderColumnColorized(out *strings.Builder, colIdx int, colStr string, hint renderHint) {
|
||||||
|
colors := t.getColumnColors(colIdx, hint)
|
||||||
|
if colors != nil {
|
||||||
|
out.WriteString(colors.Sprint(colStr))
|
||||||
|
} else if hint.isHeaderRow && t.style.Color.Header != nil {
|
||||||
|
out.WriteString(t.style.Color.Header.Sprint(colStr))
|
||||||
|
} else if hint.isFooterRow && t.style.Color.Footer != nil {
|
||||||
|
out.WriteString(t.style.Color.Footer.Sprint(colStr))
|
||||||
|
} else if hint.isRegularRow() {
|
||||||
|
if colIdx == t.indexColumn-1 && t.style.Color.IndexColumn != nil {
|
||||||
|
out.WriteString(t.style.Color.IndexColumn.Sprint(colStr))
|
||||||
|
} else if hint.rowNumber%2 == 0 && t.style.Color.RowAlternate != nil {
|
||||||
|
out.WriteString(t.style.Color.RowAlternate.Sprint(colStr))
|
||||||
|
} else if t.style.Color.Row != nil {
|
||||||
|
out.WriteString(t.style.Color.Row.Sprint(colStr))
|
||||||
|
} else {
|
||||||
|
out.WriteString(colStr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.WriteString(colStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) renderColumnSeparator(out *strings.Builder, row rowStr, colIdx int, hint renderHint) {
|
||||||
|
if t.style.Options.SeparateColumns {
|
||||||
|
separator := t.getColumnSeparator(row, colIdx, hint)
|
||||||
|
|
||||||
|
colors := t.getSeparatorColors(hint)
|
||||||
|
if colors.EscapeSeq() != "" {
|
||||||
|
out.WriteString(colors.Sprint(separator))
|
||||||
|
} else {
|
||||||
|
out.WriteString(separator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) renderLine(out *strings.Builder, row rowStr, hint renderHint) {
|
||||||
|
// if the output has content, it means that this call is working on line
|
||||||
|
// number 2 or more; separate them with a newline
|
||||||
|
if out.Len() > 0 {
|
||||||
|
out.WriteRune('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
// use a brand-new strings.Builder if a row length limit has been set
|
||||||
|
var outLine *strings.Builder
|
||||||
|
if t.allowedRowLength > 0 {
|
||||||
|
outLine = &strings.Builder{}
|
||||||
|
} else {
|
||||||
|
outLine = out
|
||||||
|
}
|
||||||
|
// grow the strings.Builder to the maximum possible row length
|
||||||
|
outLine.Grow(t.maxRowLength)
|
||||||
|
|
||||||
|
nextColIdx := 0
|
||||||
|
t.renderMarginLeft(outLine, hint)
|
||||||
|
for colIdx, maxColumnLength := range t.maxColumnLengths {
|
||||||
|
if colIdx != nextColIdx {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nextColIdx = t.renderColumn(outLine, row, colIdx, maxColumnLength, hint)
|
||||||
|
}
|
||||||
|
t.renderMarginRight(outLine, hint)
|
||||||
|
|
||||||
|
// merge the strings.Builder objects if a new one was created earlier
|
||||||
|
if outLine != out {
|
||||||
|
t.renderLineMergeOutputs(out, outLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if a page size has been set, and said number of lines has already
|
||||||
|
// been rendered, and the header is not being rendered right now, render
|
||||||
|
// the header all over again with a spacing line
|
||||||
|
if hint.isRegularRow() {
|
||||||
|
t.numLinesRendered++
|
||||||
|
if t.pageSize > 0 && t.numLinesRendered%t.pageSize == 0 && !hint.isLastLineOfLastRow() {
|
||||||
|
t.renderRowsFooter(out)
|
||||||
|
t.renderRowsBorderBottom(out)
|
||||||
|
out.WriteString(t.style.Box.PageSeparator)
|
||||||
|
t.renderRowsBorderTop(out)
|
||||||
|
t.renderRowsHeader(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) renderLineMergeOutputs(out *strings.Builder, outLine *strings.Builder) {
|
||||||
|
outLineStr := outLine.String()
|
||||||
|
if text.RuneWidthWithoutEscSequences(outLineStr) > t.allowedRowLength {
|
||||||
|
trimLength := t.allowedRowLength - utf8.RuneCountInString(t.style.Box.UnfinishedRow)
|
||||||
|
if trimLength > 0 {
|
||||||
|
out.WriteString(text.Trim(outLineStr, trimLength))
|
||||||
|
out.WriteString(t.style.Box.UnfinishedRow)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.WriteString(outLineStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) renderMarginLeft(out *strings.Builder, hint renderHint) {
|
||||||
|
out.WriteString(t.style.Format.Direction.Modifier())
|
||||||
|
if t.style.Options.DrawBorder {
|
||||||
|
border := t.getBorderLeft(hint)
|
||||||
|
colors := t.getBorderColors(hint)
|
||||||
|
if colors.EscapeSeq() != "" {
|
||||||
|
out.WriteString(colors.Sprint(border))
|
||||||
|
} else {
|
||||||
|
out.WriteString(border)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) renderMarginRight(out *strings.Builder, hint renderHint) {
|
||||||
|
if t.style.Options.DrawBorder {
|
||||||
|
border := t.getBorderRight(hint)
|
||||||
|
colors := t.getBorderColors(hint)
|
||||||
|
if colors.EscapeSeq() != "" {
|
||||||
|
out.WriteString(colors.Sprint(border))
|
||||||
|
} else {
|
||||||
|
out.WriteString(border)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) renderRow(out *strings.Builder, row rowStr, hint renderHint) {
|
||||||
|
if len(row) > 0 {
|
||||||
|
// fit every column into the allowedColumnLength/maxColumnLength limit
|
||||||
|
// and in the process find the max. number of lines in any column in
|
||||||
|
// this row
|
||||||
|
colMaxLines, rowWrapped := t.wrapRow(row)
|
||||||
|
|
||||||
|
// if there is just 1 line in all columns, add the row as such; else
|
||||||
|
// split each column into individual lines and render them one-by-one
|
||||||
|
if colMaxLines == 1 {
|
||||||
|
hint.isLastLineOfRow = true
|
||||||
|
t.renderLine(out, rowWrapped, hint)
|
||||||
|
} else {
|
||||||
|
// convert one row into N # of rows based on colMaxLines
|
||||||
|
rowLines := make([]rowStr, len(row))
|
||||||
|
for colIdx, colStr := range rowWrapped {
|
||||||
|
rowLines[colIdx] = t.getVAlign(colIdx, hint).ApplyStr(colStr, colMaxLines)
|
||||||
|
}
|
||||||
|
for colLineIdx := 0; colLineIdx < colMaxLines; colLineIdx++ {
|
||||||
|
rowLine := make(rowStr, len(rowLines))
|
||||||
|
for colIdx, colLines := range rowLines {
|
||||||
|
rowLine[colIdx] = colLines[colLineIdx]
|
||||||
|
}
|
||||||
|
hint.isLastLineOfRow = colLineIdx == colMaxLines-1
|
||||||
|
hint.rowLineNumber = colLineIdx + 1
|
||||||
|
t.renderLine(out, rowLine, hint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) renderRowSeparator(out *strings.Builder, hint renderHint) {
|
||||||
|
if hint.isBorderTop || hint.isBorderBottom {
|
||||||
|
if !t.style.Options.DrawBorder {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if hint.isHeaderRow && !t.style.Options.SeparateHeader {
|
||||||
|
return
|
||||||
|
} else if hint.isFooterRow && !t.style.Options.SeparateFooter {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hint.isSeparatorRow = true
|
||||||
|
t.renderLine(out, t.rowSeparator, hint)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) renderRows(out *strings.Builder, rows []rowStr, hint renderHint) {
|
||||||
|
for rowIdx, row := range rows {
|
||||||
|
hint.isFirstRow = rowIdx == 0
|
||||||
|
hint.isLastRow = rowIdx == len(rows)-1
|
||||||
|
hint.rowNumber = rowIdx + 1
|
||||||
|
t.renderRow(out, row, hint)
|
||||||
|
|
||||||
|
if (t.style.Options.SeparateRows && rowIdx < len(rows)-1) || // last row before footer
|
||||||
|
(t.separators[rowIdx] && rowIdx != len(rows)-1) { // manually added separator not after last row
|
||||||
|
hint.isFirstRow = false
|
||||||
|
t.renderRowSeparator(out, hint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) renderRowsBorderBottom(out *strings.Builder) {
|
||||||
|
if len(t.rowsFooter) > 0 {
|
||||||
|
t.renderRowSeparator(out, renderHint{isBorderBottom: true, isFooterRow: true, rowNumber: len(t.rowsFooter)})
|
||||||
|
} else {
|
||||||
|
t.renderRowSeparator(out, renderHint{isBorderBottom: true, isFooterRow: false, rowNumber: len(t.rows)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) renderRowsBorderTop(out *strings.Builder) {
|
||||||
|
if len(t.rowsHeader) > 0 || t.autoIndex {
|
||||||
|
t.renderRowSeparator(out, renderHint{isBorderTop: true, isHeaderRow: true, rowNumber: 0})
|
||||||
|
} else {
|
||||||
|
t.renderRowSeparator(out, renderHint{isBorderTop: true, isHeaderRow: false, rowNumber: 0})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) renderRowsFooter(out *strings.Builder) {
|
||||||
|
if len(t.rowsFooter) > 0 {
|
||||||
|
t.renderRowSeparator(out, renderHint{
|
||||||
|
isFooterRow: true,
|
||||||
|
isFirstRow: true,
|
||||||
|
isSeparatorRow: true,
|
||||||
|
})
|
||||||
|
t.renderRows(out, t.rowsFooter, renderHint{isFooterRow: true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) renderRowsHeader(out *strings.Builder) {
|
||||||
|
if len(t.rowsHeader) > 0 || t.autoIndex {
|
||||||
|
hintSeparator := renderHint{isHeaderRow: true, isLastRow: true, isSeparatorRow: true}
|
||||||
|
|
||||||
|
if len(t.rowsHeader) > 0 {
|
||||||
|
t.renderRows(out, t.rowsHeader, renderHint{isHeaderRow: true})
|
||||||
|
hintSeparator.rowNumber = len(t.rowsHeader)
|
||||||
|
} else if t.autoIndex {
|
||||||
|
t.renderRow(out, t.getAutoIndexColumnIDs(), renderHint{isAutoIndexRow: true, isHeaderRow: true})
|
||||||
|
hintSeparator.rowNumber = 1
|
||||||
|
}
|
||||||
|
t.renderRowSeparator(out, hintSeparator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) renderTitle(out *strings.Builder) {
|
||||||
|
if t.title != "" {
|
||||||
|
colors := t.style.Title.Colors
|
||||||
|
colorsBorder := t.getBorderColors(renderHint{isTitleRow: true})
|
||||||
|
rowLength := t.maxRowLength
|
||||||
|
if t.allowedRowLength != 0 && t.allowedRowLength < rowLength {
|
||||||
|
rowLength = t.allowedRowLength
|
||||||
|
}
|
||||||
|
if t.style.Options.DrawBorder {
|
||||||
|
lenBorder := rowLength - text.RuneWidthWithoutEscSequences(t.style.Box.TopLeft+t.style.Box.TopRight)
|
||||||
|
out.WriteString(colorsBorder.Sprint(t.style.Box.TopLeft))
|
||||||
|
out.WriteString(colorsBorder.Sprint(text.RepeatAndTrim(t.style.Box.MiddleHorizontal, lenBorder)))
|
||||||
|
out.WriteString(colorsBorder.Sprint(t.style.Box.TopRight))
|
||||||
|
}
|
||||||
|
|
||||||
|
lenText := rowLength - text.RuneWidthWithoutEscSequences(t.style.Box.PaddingLeft+t.style.Box.PaddingRight)
|
||||||
|
if t.style.Options.DrawBorder {
|
||||||
|
lenText -= text.RuneWidthWithoutEscSequences(t.style.Box.Left + t.style.Box.Right)
|
||||||
|
}
|
||||||
|
titleText := text.WrapText(t.title, lenText)
|
||||||
|
for _, titleLine := range strings.Split(titleText, "\n") {
|
||||||
|
t.renderTitleLine(out, lenText, titleLine, colors, colorsBorder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) renderTitleLine(out *strings.Builder, lenText int, titleLine string, colors text.Colors, colorsBorder text.Colors) {
|
||||||
|
titleLine = strings.TrimSpace(titleLine)
|
||||||
|
titleLine = t.style.Title.Format.Apply(titleLine)
|
||||||
|
titleLine = t.style.Title.Align.Apply(titleLine, lenText)
|
||||||
|
titleLine = t.style.Box.PaddingLeft + titleLine + t.style.Box.PaddingRight
|
||||||
|
|
||||||
|
if out.Len() > 0 {
|
||||||
|
out.WriteRune('\n')
|
||||||
|
}
|
||||||
|
if t.style.Options.DrawBorder {
|
||||||
|
out.WriteString(colorsBorder.Sprint(t.style.Box.Left))
|
||||||
|
}
|
||||||
|
out.WriteString(colors.Sprint(titleLine))
|
||||||
|
if t.style.Options.DrawBorder {
|
||||||
|
out.WriteString(colorsBorder.Sprint(t.style.Box.Right))
|
||||||
|
}
|
||||||
|
}
|
53
helper/table/render_bidi_test.go
Normal file
53
helper/table/render_bidi_test.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/jedib0t/go-pretty/v6/text"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTable_Render_BiDiText(t *testing.T) {
|
||||||
|
table := Table{}
|
||||||
|
table.AppendHeader(Row{"תאריך", "סכום", "מחלקה", "תגים"})
|
||||||
|
table.AppendRow(Row{"2020-01-01", 5.0, "מחלקה1", []string{"תג1", "תג2"}})
|
||||||
|
table.AppendRow(Row{"2021-02-01", 5.0, "מחלקה1", []string{"תג1"}})
|
||||||
|
table.AppendRow(Row{"2022-03-01", 5.0, "מחלקה2", []string{"תג1"}})
|
||||||
|
table.AppendFooter(Row{"סהכ", 30})
|
||||||
|
table.SetAutoIndex(true)
|
||||||
|
|
||||||
|
//table.Style().Format.Direction = text.Default
|
||||||
|
compareOutput(t, table.Render(), `
|
||||||
|
+---+------------+------+--------+-----------+
|
||||||
|
| | תאריך | סכום | מחלקה | תגים |
|
||||||
|
+---+------------+------+--------+-----------+
|
||||||
|
| 1 | 2020-01-01 | 5 | מחלקה1 | [תג1 תג2] |
|
||||||
|
| 2 | 2021-02-01 | 5 | מחלקה1 | [תג1] |
|
||||||
|
| 3 | 2022-03-01 | 5 | מחלקה2 | [תג1] |
|
||||||
|
+---+------------+------+--------+-----------+
|
||||||
|
| | סהכ | 30 | | |
|
||||||
|
+---+------------+------+--------+-----------+`)
|
||||||
|
|
||||||
|
table.Style().Format.Direction = text.LeftToRight
|
||||||
|
compareOutput(t, table.Render(), `
|
||||||
|
+---+------------+------+--------+-----------+
|
||||||
|
| | תאריך | סכום | מחלקה | תגים |
|
||||||
|
+---+------------+------+--------+-----------+
|
||||||
|
| 1 | 2020-01-01 | 5 | מחלקה1 | [תג1 תג2] |
|
||||||
|
| 2 | 2021-02-01 | 5 | מחלקה1 | [תג1] |
|
||||||
|
| 3 | 2022-03-01 | 5 | מחלקה2 | [תג1] |
|
||||||
|
+---+------------+------+--------+-----------+
|
||||||
|
| | סהכ | 30 | | |
|
||||||
|
+---+------------+------+--------+-----------+`)
|
||||||
|
|
||||||
|
table.Style().Format.Direction = text.RightToLeft
|
||||||
|
compareOutput(t, table.Render(), `
|
||||||
|
+---+------------+------+--------+-----------+
|
||||||
|
| | תאריך | סכום | מחלקה | תגים |
|
||||||
|
+---+------------+------+--------+-----------+
|
||||||
|
| 1 | 2020-01-01 | 5 | מחלקה1 | [תג1 תג2] |
|
||||||
|
| 2 | 2021-02-01 | 5 | מחלקה1 | [תג1] |
|
||||||
|
| 3 | 2022-03-01 | 5 | מחלקה2 | [תג1] |
|
||||||
|
+---+------------+------+--------+-----------+
|
||||||
|
| | סהכ | 30 | | |
|
||||||
|
+---+------------+------+--------+-----------+`)
|
||||||
|
}
|
81
helper/table/render_csv.go
Normal file
81
helper/table/render_csv.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RenderCSV renders the Table in CSV format. Example:
|
||||||
|
// #,First Name,Last Name,Salary,
|
||||||
|
// 1,Arya,Stark,3000,
|
||||||
|
// 20,Jon,Snow,2000,"You know nothing\, Jon Snow!"
|
||||||
|
// 300,Tyrion,Lannister,5000,
|
||||||
|
// ,,Total,10000,
|
||||||
|
func (t *Table) RenderCSV() string {
|
||||||
|
t.initForRender()
|
||||||
|
|
||||||
|
var out strings.Builder
|
||||||
|
if t.numColumns > 0 {
|
||||||
|
if t.title != "" {
|
||||||
|
out.WriteString(t.title)
|
||||||
|
}
|
||||||
|
if t.autoIndex && len(t.rowsHeader) == 0 {
|
||||||
|
t.csvRenderRow(&out, t.getAutoIndexColumnIDs(), renderHint{isAutoIndexRow: true, isHeaderRow: true})
|
||||||
|
}
|
||||||
|
t.csvRenderRows(&out, t.rowsHeader, renderHint{isHeaderRow: true})
|
||||||
|
t.csvRenderRows(&out, t.rows, renderHint{})
|
||||||
|
t.csvRenderRows(&out, t.rowsFooter, renderHint{isFooterRow: true})
|
||||||
|
if t.caption != "" {
|
||||||
|
out.WriteRune('\n')
|
||||||
|
out.WriteString(t.caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t.render(&out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) csvFixCommas(str string) string {
|
||||||
|
return strings.Replace(str, ",", "\\,", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) csvFixDoubleQuotes(str string) string {
|
||||||
|
return strings.Replace(str, "\"", "\\\"", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) csvRenderRow(out *strings.Builder, row rowStr, hint renderHint) {
|
||||||
|
// when working on line number 2 or more, insert a newline first
|
||||||
|
if out.Len() > 0 {
|
||||||
|
out.WriteRune('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate the columns to render in CSV format and append to "out"
|
||||||
|
for colIdx, colStr := range row {
|
||||||
|
// auto-index column
|
||||||
|
if colIdx == 0 && t.autoIndex {
|
||||||
|
if hint.isRegularRow() {
|
||||||
|
out.WriteString(fmt.Sprint(hint.rowNumber))
|
||||||
|
}
|
||||||
|
out.WriteRune(',')
|
||||||
|
}
|
||||||
|
if colIdx > 0 {
|
||||||
|
out.WriteRune(',')
|
||||||
|
}
|
||||||
|
if strings.ContainsAny(colStr, "\",\n") {
|
||||||
|
out.WriteRune('"')
|
||||||
|
out.WriteString(t.csvFixCommas(t.csvFixDoubleQuotes(colStr)))
|
||||||
|
out.WriteRune('"')
|
||||||
|
} else if utf8.RuneCountInString(colStr) > 0 {
|
||||||
|
out.WriteString(colStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for colIdx := len(row); colIdx < t.numColumns; colIdx++ {
|
||||||
|
out.WriteRune(',')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) csvRenderRows(out *strings.Builder, rows []rowStr, hint renderHint) {
|
||||||
|
for rowIdx, row := range rows {
|
||||||
|
hint.rowNumber = rowIdx + 1
|
||||||
|
t.csvRenderRow(out, row, hint)
|
||||||
|
}
|
||||||
|
}
|
139
helper/table/render_csv_test.go
Normal file
139
helper/table/render_csv_test.go
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
package table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTable_RenderCSV(t *testing.T) {
|
||||||
|
tw := NewWriter()
|
||||||
|
tw.AppendHeader(testHeader)
|
||||||
|
tw.AppendRows(testRows)
|
||||||
|
tw.AppendRow(testRowMultiLine)
|
||||||
|
tw.AppendRow(testRowTabs)
|
||||||
|
tw.AppendFooter(testFooter)
|
||||||
|
tw.SetCaption(testCaption)
|
||||||
|
tw.SetTitle(testTitle1)
|
||||||
|
|
||||||
|
compareOutput(t, tw.RenderCSV(), `
|
||||||
|
Game of Thrones
|
||||||
|
#,First Name,Last Name,Salary,
|
||||||
|
1,Arya,Stark,3000,
|
||||||
|
20,Jon,Snow,2000,"You know nothing\, Jon Snow!"
|
||||||
|
300,Tyrion,Lannister,5000,
|
||||||
|
0,Winter,Is,0,"Coming.
|
||||||
|
The North Remembers!
|
||||||
|
This is known."
|
||||||
|
0,Valar,Morghulis,0,Faceless Men
|
||||||
|
,,Total,10000,
|
||||||
|
A Song of Ice and Fire`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_RenderCSV_AutoIndex(t *testing.T) {
|
||||||
|
tw := NewWriter()
|
||||||
|
for rowIdx := 0; rowIdx < 10; rowIdx++ {
|
||||||
|
row := make(Row, 10)
|
||||||
|
for colIdx := 0; colIdx < 10; colIdx++ {
|
||||||
|
row[colIdx] = fmt.Sprintf("%s%d", AutoIndexColumnID(colIdx), rowIdx+1)
|
||||||
|
}
|
||||||
|
tw.AppendRow(row)
|
||||||
|
}
|
||||||
|
for rowIdx := 0; rowIdx < 1; rowIdx++ {
|
||||||
|
row := make(Row, 10)
|
||||||
|
for colIdx := 0; colIdx < 10; colIdx++ {
|
||||||
|
row[colIdx] = AutoIndexColumnID(colIdx) + "F"
|
||||||
|
}
|
||||||
|
tw.AppendFooter(row)
|
||||||
|
}
|
||||||
|
tw.SetAutoIndex(true)
|
||||||
|
tw.SetStyle(StyleLight)
|
||||||
|
|
||||||
|
compareOutput(t, tw.RenderCSV(), `
|
||||||
|
,A,B,C,D,E,F,G,H,I,J
|
||||||
|
1,A1,B1,C1,D1,E1,F1,G1,H1,I1,J1
|
||||||
|
2,A2,B2,C2,D2,E2,F2,G2,H2,I2,J2
|
||||||
|
3,A3,B3,C3,D3,E3,F3,G3,H3,I3,J3
|
||||||
|
4,A4,B4,C4,D4,E4,F4,G4,H4,I4,J4
|
||||||
|
5,A5,B5,C5,D5,E5,F5,G5,H5,I5,J5
|
||||||
|
6,A6,B6,C6,D6,E6,F6,G6,H6,I6,J6
|
||||||
|
7,A7,B7,C7,D7,E7,F7,G7,H7,I7,J7
|
||||||
|
8,A8,B8,C8,D8,E8,F8,G8,H8,I8,J8
|
||||||
|
9,A9,B9,C9,D9,E9,F9,G9,H9,I9,J9
|
||||||
|
10,A10,B10,C10,D10,E10,F10,G10,H10,I10,J10
|
||||||
|
,AF,BF,CF,DF,EF,FF,GF,HF,IF,JF`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_RenderCSV_Empty(t *testing.T) {
|
||||||
|
tw := NewWriter()
|
||||||
|
assert.Empty(t, tw.RenderCSV())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_RenderCSV_HiddenColumns(t *testing.T) {
|
||||||
|
tw := NewWriter()
|
||||||
|
tw.AppendHeader(testHeader)
|
||||||
|
tw.AppendRows(testRows)
|
||||||
|
tw.AppendFooter(testFooter)
|
||||||
|
|
||||||
|
// ensure sorting is done before hiding the columns
|
||||||
|
tw.SortBy([]SortBy{
|
||||||
|
{Name: "Salary", Mode: DscNumeric},
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("every column hidden", func(t *testing.T) {
|
||||||
|
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0, 1, 2, 3, 4}))
|
||||||
|
|
||||||
|
compareOutput(t, tw.RenderCSV(), "")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("first column hidden", func(t *testing.T) {
|
||||||
|
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0}))
|
||||||
|
|
||||||
|
compareOutput(t, tw.RenderCSV(), `
|
||||||
|
First Name,Last Name,Salary,
|
||||||
|
>>Tyrion,Lannister<<,5013,
|
||||||
|
>>Arya,Stark<<,3013,
|
||||||
|
>>Jon,Snow<<,2013,"~You know nothing\, Jon Snow!~"
|
||||||
|
,Total,10000,`)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("column hidden in the middle", func(t *testing.T) {
|
||||||
|
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{1}))
|
||||||
|
|
||||||
|
compareOutput(t, tw.RenderCSV(), `
|
||||||
|
#,Last Name,Salary,
|
||||||
|
307,Lannister<<,5013,
|
||||||
|
8,Stark<<,3013,
|
||||||
|
27,Snow<<,2013,"~You know nothing\, Jon Snow!~"
|
||||||
|
,Total,10000,`)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("last column hidden", func(t *testing.T) {
|
||||||
|
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{4}))
|
||||||
|
|
||||||
|
compareOutput(t, tw.RenderCSV(), `
|
||||||
|
#,First Name,Last Name,Salary
|
||||||
|
307,>>Tyrion,Lannister<<,5013
|
||||||
|
8,>>Arya,Stark<<,3013
|
||||||
|
27,>>Jon,Snow<<,2013
|
||||||
|
,,Total,10000`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_RenderCSV_Sorted(t *testing.T) {
|
||||||
|
tw := NewWriter()
|
||||||
|
tw.AppendHeader(testHeader)
|
||||||
|
tw.AppendRows(testRows)
|
||||||
|
tw.AppendRow(Row{11, "Sansa", "Stark", 6000})
|
||||||
|
tw.AppendFooter(testFooter)
|
||||||
|
tw.SortBy([]SortBy{{Name: "Last Name", Mode: Asc}, {Name: "First Name", Mode: Asc}})
|
||||||
|
|
||||||
|
compareOutput(t, tw.RenderCSV(), `
|
||||||
|
#,First Name,Last Name,Salary,
|
||||||
|
300,Tyrion,Lannister,5000,
|
||||||
|
20,Jon,Snow,2000,"You know nothing\, Jon Snow!"
|
||||||
|
1,Arya,Stark,3000,
|
||||||
|
11,Sansa,Stark,6000,
|
||||||
|
,,Total,10000,`)
|
||||||
|
}
|
39
helper/table/render_hint.go
Normal file
39
helper/table/render_hint.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package table
|
||||||
|
|
||||||
|
// renderHint has hints for the Render*() logic
|
||||||
|
type renderHint struct {
|
||||||
|
isAutoIndexColumn bool // auto-index column?
|
||||||
|
isAutoIndexRow bool // auto-index row?
|
||||||
|
isBorderBottom bool // bottom-border?
|
||||||
|
isBorderTop bool // top-border?
|
||||||
|
isFirstRow bool // first-row of header/footer/regular-rows?
|
||||||
|
isFooterRow bool // footer row?
|
||||||
|
isHeaderRow bool // header row?
|
||||||
|
isLastLineOfRow bool // last-line of the current row?
|
||||||
|
isLastRow bool // last-row of header/footer/regular-rows?
|
||||||
|
isSeparatorRow bool // separator row?
|
||||||
|
isTitleRow bool // title row?
|
||||||
|
rowLineNumber int // the line number for a multi-line row
|
||||||
|
rowNumber int // the row number/index
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *renderHint) isBorderOrSeparator() bool {
|
||||||
|
return h.isBorderTop || h.isSeparatorRow || h.isBorderBottom
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *renderHint) isRegularRow() bool {
|
||||||
|
return !h.isHeaderRow && !h.isFooterRow
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *renderHint) isRegularNonSeparatorRow() bool {
|
||||||
|
return !h.isHeaderRow && !h.isFooterRow && !h.isSeparatorRow
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *renderHint) isHeaderOrFooterSeparator() bool {
|
||||||
|
return h.isSeparatorRow && !h.isBorderBottom && !h.isBorderTop &&
|
||||||
|
((h.isHeaderRow && !h.isLastRow) || (h.isFooterRow && (!h.isFirstRow || h.rowNumber > 0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *renderHint) isLastLineOfLastRow() bool {
|
||||||
|
return h.isLastLineOfRow && h.isLastRow
|
||||||
|
}
|
244
helper/table/render_html.go
Normal file
244
helper/table/render_html.go
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
package table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultHTMLCSSClass stores the css-class to use when none-provided via
|
||||||
|
// SetHTMLCSSClass(cssClass string).
|
||||||
|
DefaultHTMLCSSClass = "go-pretty-table"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RenderHTML renders the Table in HTML format. Example:
|
||||||
|
// <table class="go-pretty-table">
|
||||||
|
// <thead>
|
||||||
|
// <tr>
|
||||||
|
// <th align="right">#</th>
|
||||||
|
// <th>First Name</th>
|
||||||
|
// <th>Last Name</th>
|
||||||
|
// <th align="right">Salary</th>
|
||||||
|
// <th> </th>
|
||||||
|
// </tr>
|
||||||
|
// </thead>
|
||||||
|
// <tbody>
|
||||||
|
// <tr>
|
||||||
|
// <td align="right">1</td>
|
||||||
|
// <td>Arya</td>
|
||||||
|
// <td>Stark</td>
|
||||||
|
// <td align="right">3000</td>
|
||||||
|
// <td> </td>
|
||||||
|
// </tr>
|
||||||
|
// <tr>
|
||||||
|
// <td align="right">20</td>
|
||||||
|
// <td>Jon</td>
|
||||||
|
// <td>Snow</td>
|
||||||
|
// <td align="right">2000</td>
|
||||||
|
// <td>You know nothing, Jon Snow!</td>
|
||||||
|
// </tr>
|
||||||
|
// <tr>
|
||||||
|
// <td align="right">300</td>
|
||||||
|
// <td>Tyrion</td>
|
||||||
|
// <td>Lannister</td>
|
||||||
|
// <td align="right">5000</td>
|
||||||
|
// <td> </td>
|
||||||
|
// </tr>
|
||||||
|
// </tbody>
|
||||||
|
// <tfoot>
|
||||||
|
// <tr>
|
||||||
|
// <td align="right"> </td>
|
||||||
|
// <td> </td>
|
||||||
|
// <td>Total</td>
|
||||||
|
// <td align="right">10000</td>
|
||||||
|
// <td> </td>
|
||||||
|
// </tr>
|
||||||
|
// </tfoot>
|
||||||
|
// </table>
|
||||||
|
func (t *Table) RenderHTML() string {
|
||||||
|
t.initForRender()
|
||||||
|
|
||||||
|
var out strings.Builder
|
||||||
|
if t.numColumns > 0 {
|
||||||
|
out.WriteString("<table class=\"")
|
||||||
|
if t.htmlCSSClass != "" {
|
||||||
|
out.WriteString(t.htmlCSSClass)
|
||||||
|
} else {
|
||||||
|
out.WriteString(t.style.HTML.CSSClass)
|
||||||
|
}
|
||||||
|
out.WriteString("\">\n")
|
||||||
|
t.htmlRenderTitle(&out)
|
||||||
|
t.htmlRenderRowsHeader(&out)
|
||||||
|
t.htmlRenderRows(&out, t.rows, renderHint{})
|
||||||
|
t.htmlRenderRowsFooter(&out)
|
||||||
|
t.htmlRenderCaption(&out)
|
||||||
|
out.WriteString("</table>")
|
||||||
|
}
|
||||||
|
return t.render(&out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) htmlGetColStrAndTag(row rowStr, colIdx int, hint renderHint) (string, string) {
|
||||||
|
// get the column contents
|
||||||
|
var colStr string
|
||||||
|
if colIdx < len(row) {
|
||||||
|
colStr = row[colIdx]
|
||||||
|
}
|
||||||
|
|
||||||
|
// header uses "th" instead of "td"
|
||||||
|
colTagName := "td"
|
||||||
|
if hint.isHeaderRow {
|
||||||
|
colTagName = "th"
|
||||||
|
}
|
||||||
|
|
||||||
|
return colStr, colTagName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) htmlRenderCaption(out *strings.Builder) {
|
||||||
|
if t.caption != "" {
|
||||||
|
out.WriteString(" <caption class=\"caption\" style=\"caption-side: bottom;\">")
|
||||||
|
out.WriteString(t.caption)
|
||||||
|
out.WriteString("</caption>\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) htmlRenderColumn(out *strings.Builder, colStr string) {
|
||||||
|
if t.style.HTML.EscapeText {
|
||||||
|
colStr = html.EscapeString(colStr)
|
||||||
|
}
|
||||||
|
if t.style.HTML.Newline != "\n" {
|
||||||
|
colStr = strings.Replace(colStr, "\n", t.style.HTML.Newline, -1)
|
||||||
|
}
|
||||||
|
out.WriteString(colStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) htmlRenderColumnAttributes(out *strings.Builder, row rowStr, colIdx int, hint renderHint) {
|
||||||
|
// determine the HTML "align"/"valign" property values
|
||||||
|
align := t.getAlign(colIdx, hint).HTMLProperty()
|
||||||
|
vAlign := t.getVAlign(colIdx, hint).HTMLProperty()
|
||||||
|
// determine the HTML "class" property values for the colors
|
||||||
|
class := t.getColumnColors(colIdx, hint).HTMLProperty()
|
||||||
|
|
||||||
|
if align != "" {
|
||||||
|
out.WriteRune(' ')
|
||||||
|
out.WriteString(align)
|
||||||
|
}
|
||||||
|
if class != "" {
|
||||||
|
out.WriteRune(' ')
|
||||||
|
out.WriteString(class)
|
||||||
|
}
|
||||||
|
if vAlign != "" {
|
||||||
|
out.WriteRune(' ')
|
||||||
|
out.WriteString(vAlign)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) htmlRenderColumnAutoIndex(out *strings.Builder, hint renderHint) {
|
||||||
|
if hint.isHeaderRow {
|
||||||
|
out.WriteString(" <th>")
|
||||||
|
out.WriteString(t.style.HTML.EmptyColumn)
|
||||||
|
out.WriteString("</th>\n")
|
||||||
|
} else if hint.isFooterRow {
|
||||||
|
out.WriteString(" <td>")
|
||||||
|
out.WriteString(t.style.HTML.EmptyColumn)
|
||||||
|
out.WriteString("</td>\n")
|
||||||
|
} else {
|
||||||
|
out.WriteString(" <td align=\"right\">")
|
||||||
|
out.WriteString(fmt.Sprint(hint.rowNumber))
|
||||||
|
out.WriteString("</td>\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) htmlRenderRow(out *strings.Builder, row rowStr, hint renderHint) {
|
||||||
|
out.WriteString(" <tr>\n")
|
||||||
|
for colIdx := 0; colIdx < t.numColumns; colIdx++ {
|
||||||
|
// auto-index column
|
||||||
|
if colIdx == 0 && t.autoIndex {
|
||||||
|
t.htmlRenderColumnAutoIndex(out, hint)
|
||||||
|
}
|
||||||
|
|
||||||
|
colStr, colTagName := t.htmlGetColStrAndTag(row, colIdx, hint)
|
||||||
|
// write the row
|
||||||
|
out.WriteString(" <")
|
||||||
|
out.WriteString(colTagName)
|
||||||
|
t.htmlRenderColumnAttributes(out, row, colIdx, hint)
|
||||||
|
out.WriteString(">")
|
||||||
|
if len(colStr) == 0 {
|
||||||
|
out.WriteString(t.style.HTML.EmptyColumn)
|
||||||
|
} else {
|
||||||
|
t.htmlRenderColumn(out, colStr)
|
||||||
|
}
|
||||||
|
out.WriteString("</")
|
||||||
|
out.WriteString(colTagName)
|
||||||
|
out.WriteString(">\n")
|
||||||
|
}
|
||||||
|
out.WriteString(" </tr>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) htmlRenderRows(out *strings.Builder, rows []rowStr, hint renderHint) {
|
||||||
|
if len(rows) > 0 {
|
||||||
|
// determine that tag to use based on the type of the row
|
||||||
|
rowsTag := "tbody"
|
||||||
|
if hint.isHeaderRow {
|
||||||
|
rowsTag = "thead"
|
||||||
|
} else if hint.isFooterRow {
|
||||||
|
rowsTag = "tfoot"
|
||||||
|
}
|
||||||
|
|
||||||
|
var renderedTagOpen, shouldRenderTagClose bool
|
||||||
|
for idx, row := range rows {
|
||||||
|
hint.rowNumber = idx + 1
|
||||||
|
if len(row) > 0 {
|
||||||
|
if !renderedTagOpen {
|
||||||
|
out.WriteString(" <")
|
||||||
|
out.WriteString(rowsTag)
|
||||||
|
out.WriteString(">\n")
|
||||||
|
renderedTagOpen = true
|
||||||
|
}
|
||||||
|
t.htmlRenderRow(out, row, hint)
|
||||||
|
shouldRenderTagClose = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if shouldRenderTagClose {
|
||||||
|
out.WriteString(" </")
|
||||||
|
out.WriteString(rowsTag)
|
||||||
|
out.WriteString(">\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) htmlRenderRowsFooter(out *strings.Builder) {
|
||||||
|
if len(t.rowsFooter) > 0 {
|
||||||
|
t.htmlRenderRows(out, t.rowsFooter, renderHint{isFooterRow: true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) htmlRenderRowsHeader(out *strings.Builder) {
|
||||||
|
if len(t.rowsHeader) > 0 {
|
||||||
|
t.htmlRenderRows(out, t.rowsHeader, renderHint{isHeaderRow: true})
|
||||||
|
} else if t.autoIndex {
|
||||||
|
hint := renderHint{isAutoIndexRow: true, isHeaderRow: true}
|
||||||
|
t.htmlRenderRows(out, []rowStr{t.getAutoIndexColumnIDs()}, hint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) htmlRenderTitle(out *strings.Builder) {
|
||||||
|
if t.title != "" {
|
||||||
|
align := t.style.Title.Align.HTMLProperty()
|
||||||
|
colors := t.style.Title.Colors.HTMLProperty()
|
||||||
|
title := t.style.Title.Format.Apply(t.title)
|
||||||
|
|
||||||
|
out.WriteString(" <caption class=\"title\"")
|
||||||
|
if align != "" {
|
||||||
|
out.WriteRune(' ')
|
||||||
|
out.WriteString(align)
|
||||||
|
}
|
||||||
|
if colors != "" {
|
||||||
|
out.WriteRune(' ')
|
||||||
|
out.WriteString(colors)
|
||||||
|
}
|
||||||
|
out.WriteRune('>')
|
||||||
|
out.WriteString(title)
|
||||||
|
out.WriteString("</caption>\n")
|
||||||
|
}
|
||||||
|
}
|
519
helper/table/render_html_test.go
Normal file
519
helper/table/render_html_test.go
Normal file
@ -0,0 +1,519 @@
|
|||||||
|
package table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/jedib0t/go-pretty/v6/text"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTable_RenderHTML(t *testing.T) {
|
||||||
|
tw := NewWriter()
|
||||||
|
tw.AppendHeader(testHeader)
|
||||||
|
tw.AppendRows(testRows)
|
||||||
|
tw.AppendRow(testRowMultiLine)
|
||||||
|
tw.AppendFooter(testFooter)
|
||||||
|
tw.SetColumnConfigs([]ColumnConfig{
|
||||||
|
{Name: "Salary", VAlign: text.VAlignBottom},
|
||||||
|
{Number: 5, VAlign: text.VAlignBottom},
|
||||||
|
})
|
||||||
|
tw.SetTitle(testTitle1)
|
||||||
|
tw.SetCaption(testCaption)
|
||||||
|
tw.Style().Title = TitleOptions{
|
||||||
|
Align: text.AlignLeft,
|
||||||
|
Colors: text.Colors{text.BgBlack, text.Bold, text.FgHiBlue},
|
||||||
|
Format: text.FormatTitle,
|
||||||
|
}
|
||||||
|
|
||||||
|
compareOutput(t, tw.RenderHTML(), `
|
||||||
|
<table class="go-pretty-table">
|
||||||
|
<caption class="title" align="left" class="bg-black bold fg-hi-blue">Game Of Thrones</caption>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="right">#</th>
|
||||||
|
<th>First Name</th>
|
||||||
|
<th>Last Name</th>
|
||||||
|
<th align="right">Salary</th>
|
||||||
|
<th> </th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="right">1</td>
|
||||||
|
<td>Arya</td>
|
||||||
|
<td>Stark</td>
|
||||||
|
<td align="right" valign="bottom">3000</td>
|
||||||
|
<td valign="bottom"> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="right">20</td>
|
||||||
|
<td>Jon</td>
|
||||||
|
<td>Snow</td>
|
||||||
|
<td align="right" valign="bottom">2000</td>
|
||||||
|
<td valign="bottom">You know nothing, Jon Snow!</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="right">300</td>
|
||||||
|
<td>Tyrion</td>
|
||||||
|
<td>Lannister</td>
|
||||||
|
<td align="right" valign="bottom">5000</td>
|
||||||
|
<td valign="bottom"> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="right">0</td>
|
||||||
|
<td>Winter</td>
|
||||||
|
<td>Is</td>
|
||||||
|
<td align="right" valign="bottom">0</td>
|
||||||
|
<td valign="bottom">Coming.<br/>The North Remembers!<br/>This is known.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td align="right"> </td>
|
||||||
|
<td> </td>
|
||||||
|
<td>Total</td>
|
||||||
|
<td align="right">10000</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
<caption class="caption" style="caption-side: bottom;">A Song of Ice and Fire</caption>
|
||||||
|
</table>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_RenderHTML_AutoIndex(t *testing.T) {
|
||||||
|
tw := NewWriter()
|
||||||
|
for rowIdx := 0; rowIdx < 3; rowIdx++ {
|
||||||
|
row := make(Row, 3)
|
||||||
|
for colIdx := 0; colIdx < 3; colIdx++ {
|
||||||
|
row[colIdx] = fmt.Sprintf("%s%d", AutoIndexColumnID(colIdx), rowIdx+1)
|
||||||
|
}
|
||||||
|
tw.AppendRow(row)
|
||||||
|
}
|
||||||
|
for rowIdx := 0; rowIdx < 1; rowIdx++ {
|
||||||
|
row := make(Row, 3)
|
||||||
|
for colIdx := 0; colIdx < 3; colIdx++ {
|
||||||
|
row[colIdx] = AutoIndexColumnID(colIdx) + "F"
|
||||||
|
}
|
||||||
|
tw.AppendFooter(row)
|
||||||
|
}
|
||||||
|
tw.SetOutputMirror(nil)
|
||||||
|
tw.SetAutoIndex(true)
|
||||||
|
tw.SetStyle(StyleLight)
|
||||||
|
|
||||||
|
compareOutput(t, tw.RenderHTML(), `
|
||||||
|
<table class="go-pretty-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th> </th>
|
||||||
|
<th align="center">A</th>
|
||||||
|
<th align="center">B</th>
|
||||||
|
<th align="center">C</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="right">1</td>
|
||||||
|
<td>A1</td>
|
||||||
|
<td>B1</td>
|
||||||
|
<td>C1</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="right">2</td>
|
||||||
|
<td>A2</td>
|
||||||
|
<td>B2</td>
|
||||||
|
<td>C2</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="right">3</td>
|
||||||
|
<td>A3</td>
|
||||||
|
<td>B3</td>
|
||||||
|
<td>C3</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td> </td>
|
||||||
|
<td>AF</td>
|
||||||
|
<td>BF</td>
|
||||||
|
<td>CF</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_RenderHTML_Colored(t *testing.T) {
|
||||||
|
tw := NewWriter()
|
||||||
|
tw.AppendHeader(testHeader)
|
||||||
|
tw.AppendRows(testRows)
|
||||||
|
tw.AppendRow(testRowMultiLine)
|
||||||
|
tw.AppendFooter(testFooter)
|
||||||
|
tw.SetCaption(testCaption)
|
||||||
|
tw.SetTitle(testTitle1)
|
||||||
|
tw.Style().HTML.CSSClass = "go-pretty-table-colored"
|
||||||
|
colorsBlackOnWhite := text.Colors{text.BgWhite, text.FgBlack}
|
||||||
|
tw.SetColumnConfigs([]ColumnConfig{
|
||||||
|
{
|
||||||
|
Name: "#",
|
||||||
|
Colors: text.Colors{text.Bold},
|
||||||
|
ColorsHeader: colorsBlackOnWhite,
|
||||||
|
}, {
|
||||||
|
Name: "First Name",
|
||||||
|
Colors: text.Colors{text.FgCyan},
|
||||||
|
ColorsHeader: colorsBlackOnWhite,
|
||||||
|
}, {
|
||||||
|
Name: "Last Name",
|
||||||
|
Colors: text.Colors{text.FgMagenta},
|
||||||
|
ColorsHeader: colorsBlackOnWhite,
|
||||||
|
ColorsFooter: colorsBlackOnWhite,
|
||||||
|
}, {
|
||||||
|
Name: "Salary",
|
||||||
|
Colors: text.Colors{text.FgYellow},
|
||||||
|
ColorsHeader: colorsBlackOnWhite,
|
||||||
|
ColorsFooter: colorsBlackOnWhite,
|
||||||
|
VAlign: text.VAlignBottom,
|
||||||
|
}, {
|
||||||
|
Number: 5,
|
||||||
|
Colors: text.Colors{text.FgBlack},
|
||||||
|
ColorsHeader: colorsBlackOnWhite,
|
||||||
|
VAlign: text.VAlignBottom,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
compareOutput(t, tw.RenderHTML(), `
|
||||||
|
<table class="go-pretty-table-colored">
|
||||||
|
<caption class="title">Game of Thrones</caption>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="right" class="bg-white fg-black">#</th>
|
||||||
|
<th class="bg-white fg-black">First Name</th>
|
||||||
|
<th class="bg-white fg-black">Last Name</th>
|
||||||
|
<th align="right" class="bg-white fg-black">Salary</th>
|
||||||
|
<th class="bg-white fg-black"> </th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="right" class="bold">1</td>
|
||||||
|
<td class="fg-cyan">Arya</td>
|
||||||
|
<td class="fg-magenta">Stark</td>
|
||||||
|
<td align="right" class="fg-yellow" valign="bottom">3000</td>
|
||||||
|
<td class="fg-black" valign="bottom"> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="right" class="bold">20</td>
|
||||||
|
<td class="fg-cyan">Jon</td>
|
||||||
|
<td class="fg-magenta">Snow</td>
|
||||||
|
<td align="right" class="fg-yellow" valign="bottom">2000</td>
|
||||||
|
<td class="fg-black" valign="bottom">You know nothing, Jon Snow!</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="right" class="bold">300</td>
|
||||||
|
<td class="fg-cyan">Tyrion</td>
|
||||||
|
<td class="fg-magenta">Lannister</td>
|
||||||
|
<td align="right" class="fg-yellow" valign="bottom">5000</td>
|
||||||
|
<td class="fg-black" valign="bottom"> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="right" class="bold">0</td>
|
||||||
|
<td class="fg-cyan">Winter</td>
|
||||||
|
<td class="fg-magenta">Is</td>
|
||||||
|
<td align="right" class="fg-yellow" valign="bottom">0</td>
|
||||||
|
<td class="fg-black" valign="bottom">Coming.<br/>The North Remembers!<br/>This is known.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td align="right"> </td>
|
||||||
|
<td> </td>
|
||||||
|
<td class="bg-white fg-black">Total</td>
|
||||||
|
<td align="right" class="bg-white fg-black">10000</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
<caption class="caption" style="caption-side: bottom;">A Song of Ice and Fire</caption>
|
||||||
|
</table>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_RenderHTML_CustomStyle(t *testing.T) {
|
||||||
|
tw := NewWriter()
|
||||||
|
tw.AppendHeader(testHeader)
|
||||||
|
tw.AppendRow(Row{1, "Arya", "Stark", 3000, "<a href=\"https://duckduckgo.com/?q=arya+stark+not+today\">Not today.</a>"})
|
||||||
|
tw.AppendRow(Row{1, "Jon", "Snow", 2000, "You know\nnothing,\nJon Snow!"})
|
||||||
|
tw.AppendRow(Row{300, "Tyrion", "Lannister", 5000})
|
||||||
|
tw.AppendFooter(testFooter)
|
||||||
|
tw.SetAutoIndex(true)
|
||||||
|
tw.Style().HTML = HTMLOptions{
|
||||||
|
CSSClass: "game-of-thrones",
|
||||||
|
EmptyColumn: "<!-- test --> ",
|
||||||
|
EscapeText: false,
|
||||||
|
Newline: "<!-- newline -->",
|
||||||
|
}
|
||||||
|
tw.SetOutputMirror(nil)
|
||||||
|
|
||||||
|
compareOutput(t, tw.RenderHTML(), `
|
||||||
|
<table class="game-of-thrones">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><!-- test --> </th>
|
||||||
|
<th align="right">#</th>
|
||||||
|
<th>First Name</th>
|
||||||
|
<th>Last Name</th>
|
||||||
|
<th align="right">Salary</th>
|
||||||
|
<th><!-- test --> </th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="right">1</td>
|
||||||
|
<td align="right">1</td>
|
||||||
|
<td>Arya</td>
|
||||||
|
<td>Stark</td>
|
||||||
|
<td align="right">3000</td>
|
||||||
|
<td><a href="https://duckduckgo.com/?q=arya+stark+not+today">Not today.</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="right">2</td>
|
||||||
|
<td align="right">1</td>
|
||||||
|
<td>Jon</td>
|
||||||
|
<td>Snow</td>
|
||||||
|
<td align="right">2000</td>
|
||||||
|
<td>You know<!-- newline -->nothing,<!-- newline -->Jon Snow!</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="right">3</td>
|
||||||
|
<td align="right">300</td>
|
||||||
|
<td>Tyrion</td>
|
||||||
|
<td>Lannister</td>
|
||||||
|
<td align="right">5000</td>
|
||||||
|
<td><!-- test --> </td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td><!-- test --> </td>
|
||||||
|
<td align="right"><!-- test --> </td>
|
||||||
|
<td><!-- test --> </td>
|
||||||
|
<td>Total</td>
|
||||||
|
<td align="right">10000</td>
|
||||||
|
<td><!-- test --> </td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_RenderHTML_Empty(t *testing.T) {
|
||||||
|
tw := NewWriter()
|
||||||
|
assert.Empty(t, tw.RenderHTML())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_RenderHTML_HiddenColumns(t *testing.T) {
|
||||||
|
tw := NewWriter()
|
||||||
|
tw.AppendHeader(testHeader)
|
||||||
|
tw.AppendRows(testRows)
|
||||||
|
tw.AppendFooter(testFooter)
|
||||||
|
|
||||||
|
// ensure sorting is done before hiding the columns
|
||||||
|
tw.SortBy([]SortBy{
|
||||||
|
{Name: "Salary", Mode: DscNumeric},
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("every column hidden", func(t *testing.T) {
|
||||||
|
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0, 1, 2, 3, 4}))
|
||||||
|
|
||||||
|
compareOutput(t, tw.RenderHTML(), "")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("first column hidden", func(t *testing.T) {
|
||||||
|
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0}))
|
||||||
|
|
||||||
|
compareOutput(t, tw.RenderHTML(), `
|
||||||
|
<table class="go-pretty-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>First Name</th>
|
||||||
|
<th>Last Name</th>
|
||||||
|
<th align="right">Salary</th>
|
||||||
|
<th> </th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>>>Tyrion</td>
|
||||||
|
<td>Lannister<<</td>
|
||||||
|
<td align="right">5013</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>>>Arya</td>
|
||||||
|
<td>Stark<<</td>
|
||||||
|
<td align="right">3013</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>>>Jon</td>
|
||||||
|
<td>Snow<<</td>
|
||||||
|
<td align="right">2013</td>
|
||||||
|
<td>~You know nothing, Jon Snow!~</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td> </td>
|
||||||
|
<td>Total</td>
|
||||||
|
<td align="right">10000</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>`)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("column hidden in the middle", func(t *testing.T) {
|
||||||
|
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{1}))
|
||||||
|
|
||||||
|
compareOutput(t, tw.RenderHTML(), `
|
||||||
|
<table class="go-pretty-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="right">#</th>
|
||||||
|
<th>Last Name</th>
|
||||||
|
<th align="right">Salary</th>
|
||||||
|
<th> </th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="right">307</td>
|
||||||
|
<td>Lannister<<</td>
|
||||||
|
<td align="right">5013</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="right">8</td>
|
||||||
|
<td>Stark<<</td>
|
||||||
|
<td align="right">3013</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="right">27</td>
|
||||||
|
<td>Snow<<</td>
|
||||||
|
<td align="right">2013</td>
|
||||||
|
<td>~You know nothing, Jon Snow!~</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td align="right"> </td>
|
||||||
|
<td>Total</td>
|
||||||
|
<td align="right">10000</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>`)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("last column hidden", func(t *testing.T) {
|
||||||
|
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{4}))
|
||||||
|
|
||||||
|
compareOutput(t, tw.RenderHTML(), `
|
||||||
|
<table class="go-pretty-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="right">#</th>
|
||||||
|
<th>First Name</th>
|
||||||
|
<th>Last Name</th>
|
||||||
|
<th align="right">Salary</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="right">307</td>
|
||||||
|
<td>>>Tyrion</td>
|
||||||
|
<td>Lannister<<</td>
|
||||||
|
<td align="right">5013</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="right">8</td>
|
||||||
|
<td>>>Arya</td>
|
||||||
|
<td>Stark<<</td>
|
||||||
|
<td align="right">3013</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="right">27</td>
|
||||||
|
<td>>>Jon</td>
|
||||||
|
<td>Snow<<</td>
|
||||||
|
<td align="right">2013</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td align="right"> </td>
|
||||||
|
<td> </td>
|
||||||
|
<td>Total</td>
|
||||||
|
<td align="right">10000</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_RenderHTML_Sorted(t *testing.T) {
|
||||||
|
tw := NewWriter()
|
||||||
|
tw.AppendHeader(testHeader)
|
||||||
|
tw.AppendRows(testRows)
|
||||||
|
tw.AppendRow(Row{11, "Sansa", "Stark", 6000})
|
||||||
|
tw.AppendFooter(testFooter)
|
||||||
|
tw.SortBy([]SortBy{{Name: "Last Name", Mode: Asc}, {Name: "First Name", Mode: Asc}})
|
||||||
|
|
||||||
|
compareOutput(t, tw.RenderHTML(), `
|
||||||
|
<table class="go-pretty-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="right">#</th>
|
||||||
|
<th>First Name</th>
|
||||||
|
<th>Last Name</th>
|
||||||
|
<th align="right">Salary</th>
|
||||||
|
<th> </th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="right">300</td>
|
||||||
|
<td>Tyrion</td>
|
||||||
|
<td>Lannister</td>
|
||||||
|
<td align="right">5000</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="right">20</td>
|
||||||
|
<td>Jon</td>
|
||||||
|
<td>Snow</td>
|
||||||
|
<td align="right">2000</td>
|
||||||
|
<td>You know nothing, Jon Snow!</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="right">1</td>
|
||||||
|
<td>Arya</td>
|
||||||
|
<td>Stark</td>
|
||||||
|
<td align="right">3000</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="right">11</td>
|
||||||
|
<td>Sansa</td>
|
||||||
|
<td>Stark</td>
|
||||||
|
<td align="right">6000</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td align="right"> </td>
|
||||||
|
<td> </td>
|
||||||
|
<td>Total</td>
|
||||||
|
<td align="right">10000</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>`)
|
||||||
|
}
|
294
helper/table/render_init.go
Normal file
294
helper/table/render_init.go
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
package table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.hexq.cn/tiglog/golib/helper/text"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *Table) analyzeAndStringify(row Row, hint renderHint) rowStr {
|
||||||
|
// update t.numColumns if this row is the longest seen till now
|
||||||
|
if len(row) > t.numColumns {
|
||||||
|
// init the slice for the first time; and pad it the rest of the time
|
||||||
|
if t.numColumns == 0 {
|
||||||
|
t.columnIsNonNumeric = make([]bool, len(row))
|
||||||
|
} else {
|
||||||
|
t.columnIsNonNumeric = append(t.columnIsNonNumeric, make([]bool, len(row)-t.numColumns)...)
|
||||||
|
}
|
||||||
|
// update t.numColumns
|
||||||
|
t.numColumns = len(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert each column to string and figure out if it has non-numeric data
|
||||||
|
rowOut := make(rowStr, len(row))
|
||||||
|
for colIdx, col := range row {
|
||||||
|
// if the column is not a number, keep track of it
|
||||||
|
if !hint.isHeaderRow && !hint.isFooterRow && !t.columnIsNonNumeric[colIdx] && !isNumber(col) {
|
||||||
|
t.columnIsNonNumeric[colIdx] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
rowOut[colIdx] = t.analyzeAndStringifyColumn(colIdx, col, hint)
|
||||||
|
}
|
||||||
|
return rowOut
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) analyzeAndStringifyColumn(colIdx int, col interface{}, hint renderHint) string {
|
||||||
|
// convert to a string and store it in the row
|
||||||
|
var colStr string
|
||||||
|
if transformer := t.getColumnTransformer(colIdx, hint); transformer != nil {
|
||||||
|
colStr = transformer(col)
|
||||||
|
} else if colStrVal, ok := col.(string); ok {
|
||||||
|
colStr = colStrVal
|
||||||
|
} else {
|
||||||
|
colStr = fmt.Sprint(col)
|
||||||
|
}
|
||||||
|
if strings.Contains(colStr, "\t") {
|
||||||
|
colStr = strings.Replace(colStr, "\t", " ", -1)
|
||||||
|
}
|
||||||
|
if strings.Contains(colStr, "\r") {
|
||||||
|
colStr = strings.Replace(colStr, "\r", "", -1)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s%s", t.style.Format.Direction.Modifier(), colStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) extractMaxColumnLengths(rows []rowStr, hint renderHint) {
|
||||||
|
for rowIdx, row := range rows {
|
||||||
|
hint.rowNumber = rowIdx + 1
|
||||||
|
t.extractMaxColumnLengthsFromRow(row, t.getMergedColumnIndices(row, hint))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) extractMaxColumnLengthsFromRow(row rowStr, mci mergedColumnIndices) {
|
||||||
|
for colIdx, colStr := range row {
|
||||||
|
longestLineLen := text.LongestLineLen(colStr)
|
||||||
|
maxColWidth := t.getColumnWidthMax(colIdx)
|
||||||
|
if maxColWidth > 0 && maxColWidth < longestLineLen {
|
||||||
|
longestLineLen = maxColWidth
|
||||||
|
}
|
||||||
|
mergedColumnsLength := mci.mergedLength(colIdx, t.maxColumnLengths)
|
||||||
|
if longestLineLen > mergedColumnsLength {
|
||||||
|
if mergedColumnsLength > 0 {
|
||||||
|
t.extractMaxColumnLengthsFromRowForMergedColumns(colIdx, longestLineLen, mci)
|
||||||
|
} else {
|
||||||
|
t.maxColumnLengths[colIdx] = longestLineLen
|
||||||
|
}
|
||||||
|
} else if maxColWidth == 0 && longestLineLen > t.maxColumnLengths[colIdx] {
|
||||||
|
t.maxColumnLengths[colIdx] = longestLineLen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) extractMaxColumnLengthsFromRowForMergedColumns(colIdx int, mergedColumnLength int, mci mergedColumnIndices) {
|
||||||
|
numMergedColumns := mci.len(colIdx)
|
||||||
|
mergedColumnLength -= (numMergedColumns - 1) * text.RuneWidthWithoutEscSequences(t.style.Box.MiddleSeparator)
|
||||||
|
maxLengthSplitAcrossColumns := mergedColumnLength / numMergedColumns
|
||||||
|
if maxLengthSplitAcrossColumns > t.maxColumnLengths[colIdx] {
|
||||||
|
t.maxColumnLengths[colIdx] = maxLengthSplitAcrossColumns
|
||||||
|
}
|
||||||
|
for otherColIdx := range mci[colIdx] {
|
||||||
|
if maxLengthSplitAcrossColumns > t.maxColumnLengths[otherColIdx] {
|
||||||
|
t.maxColumnLengths[otherColIdx] = maxLengthSplitAcrossColumns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) initForRender() {
|
||||||
|
// pick a default style if none was set until now
|
||||||
|
t.Style()
|
||||||
|
|
||||||
|
// reset rendering state
|
||||||
|
t.reset()
|
||||||
|
|
||||||
|
// initialize the column configs and normalize them
|
||||||
|
t.initForRenderColumnConfigs()
|
||||||
|
|
||||||
|
// initialize and stringify all the raw rows
|
||||||
|
t.initForRenderRows()
|
||||||
|
|
||||||
|
// find the longest continuous line in each column
|
||||||
|
t.initForRenderColumnLengths()
|
||||||
|
|
||||||
|
// generate a separator row and calculate maximum row length
|
||||||
|
t.initForRenderRowSeparator()
|
||||||
|
|
||||||
|
// reset the counter for the number of lines rendered
|
||||||
|
t.numLinesRendered = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) initForRenderColumnConfigs() {
|
||||||
|
t.columnConfigMap = map[int]ColumnConfig{}
|
||||||
|
for _, colCfg := range t.columnConfigs {
|
||||||
|
// find the column number if none provided; this logic can work only if
|
||||||
|
// a header row is present and has a column with the given name
|
||||||
|
if colCfg.Number == 0 {
|
||||||
|
for _, row := range t.rowsHeaderRaw {
|
||||||
|
colCfg.Number = row.findColumnNumber(colCfg.Name)
|
||||||
|
if colCfg.Number > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if colCfg.Number > 0 {
|
||||||
|
t.columnConfigMap[colCfg.Number-1] = colCfg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) initForRenderColumnLengths() {
|
||||||
|
t.maxColumnLengths = make([]int, t.numColumns)
|
||||||
|
t.extractMaxColumnLengths(t.rowsHeader, renderHint{isHeaderRow: true})
|
||||||
|
t.extractMaxColumnLengths(t.rows, renderHint{})
|
||||||
|
t.extractMaxColumnLengths(t.rowsFooter, renderHint{isFooterRow: true})
|
||||||
|
|
||||||
|
// increase the column lengths if any are under the limits
|
||||||
|
for colIdx := range t.maxColumnLengths {
|
||||||
|
minWidth := t.getColumnWidthMin(colIdx)
|
||||||
|
if minWidth > 0 && t.maxColumnLengths[colIdx] < minWidth {
|
||||||
|
t.maxColumnLengths[colIdx] = minWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) initForRenderHideColumns() {
|
||||||
|
if !t.hasHiddenColumns() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
colIdxMap := t.hideColumns()
|
||||||
|
|
||||||
|
// re-create columnIsNonNumeric with new column indices
|
||||||
|
columnIsNonNumeric := make([]bool, t.numColumns)
|
||||||
|
for oldColIdx, nonNumeric := range t.columnIsNonNumeric {
|
||||||
|
if newColIdx, ok := colIdxMap[oldColIdx]; ok {
|
||||||
|
columnIsNonNumeric[newColIdx] = nonNumeric
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.columnIsNonNumeric = columnIsNonNumeric
|
||||||
|
|
||||||
|
// re-create columnConfigMap with new column indices
|
||||||
|
columnConfigMap := make(map[int]ColumnConfig)
|
||||||
|
for oldColIdx, cc := range t.columnConfigMap {
|
||||||
|
if newColIdx, ok := colIdxMap[oldColIdx]; ok {
|
||||||
|
columnConfigMap[newColIdx] = cc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.columnConfigMap = columnConfigMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) initForRenderRows() {
|
||||||
|
// auto-index: calc the index column's max length
|
||||||
|
t.autoIndexVIndexMaxLength = len(fmt.Sprint(len(t.rowsRaw)))
|
||||||
|
|
||||||
|
// stringify all the rows to make it easy to render
|
||||||
|
if t.rowPainter != nil {
|
||||||
|
t.rowsColors = make([]text.Colors, len(t.rowsRaw))
|
||||||
|
}
|
||||||
|
t.rows = t.initForRenderRowsStringify(t.rowsRaw, renderHint{})
|
||||||
|
t.rowsFooter = t.initForRenderRowsStringify(t.rowsFooterRaw, renderHint{isFooterRow: true})
|
||||||
|
t.rowsHeader = t.initForRenderRowsStringify(t.rowsHeaderRaw, renderHint{isHeaderRow: true})
|
||||||
|
|
||||||
|
// sort the rows as requested
|
||||||
|
t.initForRenderSortRows()
|
||||||
|
|
||||||
|
// suppress columns without any content
|
||||||
|
t.initForRenderSuppressColumns()
|
||||||
|
|
||||||
|
// strip out hidden columns
|
||||||
|
t.initForRenderHideColumns()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) initForRenderRowsStringify(rows []Row, hint renderHint) []rowStr {
|
||||||
|
rowsStr := make([]rowStr, len(rows))
|
||||||
|
for idx, row := range rows {
|
||||||
|
if t.rowPainter != nil && hint.isRegularRow() {
|
||||||
|
t.rowsColors[idx] = t.rowPainter(row)
|
||||||
|
}
|
||||||
|
rowsStr[idx] = t.analyzeAndStringify(row, hint)
|
||||||
|
}
|
||||||
|
return rowsStr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) initForRenderRowSeparator() {
|
||||||
|
t.maxRowLength = 0
|
||||||
|
if t.autoIndex {
|
||||||
|
t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.PaddingLeft)
|
||||||
|
t.maxRowLength += len(fmt.Sprint(len(t.rows)))
|
||||||
|
t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.PaddingRight)
|
||||||
|
if t.style.Options.SeparateColumns {
|
||||||
|
t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.MiddleSeparator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t.style.Options.SeparateColumns {
|
||||||
|
t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.MiddleSeparator) * (t.numColumns - 1)
|
||||||
|
}
|
||||||
|
t.rowSeparator = make(rowStr, t.numColumns)
|
||||||
|
for colIdx, maxColumnLength := range t.maxColumnLengths {
|
||||||
|
maxColumnLength += text.RuneWidthWithoutEscSequences(t.style.Box.PaddingLeft + t.style.Box.PaddingRight)
|
||||||
|
t.maxRowLength += maxColumnLength
|
||||||
|
t.rowSeparator[colIdx] = text.RepeatAndTrim(t.style.Box.MiddleHorizontal, maxColumnLength)
|
||||||
|
}
|
||||||
|
if t.style.Options.DrawBorder {
|
||||||
|
t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.Left + t.style.Box.Right)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) initForRenderSortRows() {
|
||||||
|
if len(t.sortBy) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort the rows
|
||||||
|
sortedRowIndices := t.getSortedRowIndices()
|
||||||
|
sortedRows := make([]rowStr, len(t.rows))
|
||||||
|
for idx := range t.rows {
|
||||||
|
sortedRows[idx] = t.rows[sortedRowIndices[idx]]
|
||||||
|
}
|
||||||
|
t.rows = sortedRows
|
||||||
|
|
||||||
|
// sort the rowsColors
|
||||||
|
if len(t.rowsColors) > 0 {
|
||||||
|
sortedRowsColors := make([]text.Colors, len(t.rows))
|
||||||
|
for idx := range t.rows {
|
||||||
|
sortedRowsColors[idx] = t.rowsColors[sortedRowIndices[idx]]
|
||||||
|
}
|
||||||
|
t.rowsColors = sortedRowsColors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) initForRenderSuppressColumns() {
|
||||||
|
shouldSuppressColumn := func(colIdx int) bool {
|
||||||
|
for _, row := range t.rows {
|
||||||
|
if colIdx < len(row) && row[colIdx] != "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.suppressEmptyColumns {
|
||||||
|
for colIdx := 0; colIdx < t.numColumns; colIdx++ {
|
||||||
|
if shouldSuppressColumn(colIdx) {
|
||||||
|
cc := t.columnConfigMap[colIdx]
|
||||||
|
cc.Hidden = true
|
||||||
|
t.columnConfigMap[colIdx] = cc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset initializes all the variables used to maintain rendering information
|
||||||
|
// that are written to in this file
|
||||||
|
func (t *Table) reset() {
|
||||||
|
t.autoIndexVIndexMaxLength = 0
|
||||||
|
t.columnConfigMap = nil
|
||||||
|
t.columnIsNonNumeric = nil
|
||||||
|
t.maxColumnLengths = nil
|
||||||
|
t.maxRowLength = 0
|
||||||
|
t.numColumns = 0
|
||||||
|
t.numLinesRendered = 0
|
||||||
|
t.rowSeparator = nil
|
||||||
|
t.rows = nil
|
||||||
|
t.rowsColors = nil
|
||||||
|
t.rowsFooter = nil
|
||||||
|
t.rowsHeader = nil
|
||||||
|
}
|
113
helper/table/render_markdown.go
Normal file
113
helper/table/render_markdown.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
package table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RenderMarkdown renders the Table in Markdown format. Example:
|
||||||
|
// | # | First Name | Last Name | Salary | |
|
||||||
|
// | ---:| --- | --- | ---:| --- |
|
||||||
|
// | 1 | Arya | Stark | 3000 | |
|
||||||
|
// | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
|
||||||
|
// | 300 | Tyrion | Lannister | 5000 | |
|
||||||
|
// | | | Total | 10000 | |
|
||||||
|
func (t *Table) RenderMarkdown() string {
|
||||||
|
t.initForRender()
|
||||||
|
|
||||||
|
var out strings.Builder
|
||||||
|
if t.numColumns > 0 {
|
||||||
|
t.markdownRenderTitle(&out)
|
||||||
|
t.markdownRenderRowsHeader(&out)
|
||||||
|
t.markdownRenderRows(&out, t.rows, renderHint{})
|
||||||
|
t.markdownRenderRowsFooter(&out)
|
||||||
|
t.markdownRenderCaption(&out)
|
||||||
|
}
|
||||||
|
return t.render(&out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) markdownRenderCaption(out *strings.Builder) {
|
||||||
|
if t.caption != "" {
|
||||||
|
out.WriteRune('\n')
|
||||||
|
out.WriteRune('_')
|
||||||
|
out.WriteString(t.caption)
|
||||||
|
out.WriteRune('_')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) markdownRenderRow(out *strings.Builder, row rowStr, hint renderHint) {
|
||||||
|
// when working on line number 2 or more, insert a newline first
|
||||||
|
if out.Len() > 0 {
|
||||||
|
out.WriteRune('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
// render each column up to the max. columns seen in all the rows
|
||||||
|
out.WriteRune('|')
|
||||||
|
for colIdx := 0; colIdx < t.numColumns; colIdx++ {
|
||||||
|
t.markdownRenderRowAutoIndex(out, colIdx, hint)
|
||||||
|
|
||||||
|
if hint.isSeparatorRow {
|
||||||
|
out.WriteString(t.getAlign(colIdx, hint).MarkdownProperty())
|
||||||
|
} else {
|
||||||
|
var colStr string
|
||||||
|
if colIdx < len(row) {
|
||||||
|
colStr = row[colIdx]
|
||||||
|
}
|
||||||
|
out.WriteRune(' ')
|
||||||
|
if strings.Contains(colStr, "|") {
|
||||||
|
colStr = strings.Replace(colStr, "|", "\\|", -1)
|
||||||
|
}
|
||||||
|
if strings.Contains(colStr, "\n") {
|
||||||
|
colStr = strings.Replace(colStr, "\n", "<br/>", -1)
|
||||||
|
}
|
||||||
|
out.WriteString(colStr)
|
||||||
|
out.WriteRune(' ')
|
||||||
|
}
|
||||||
|
out.WriteRune('|')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) markdownRenderRowAutoIndex(out *strings.Builder, colIdx int, hint renderHint) {
|
||||||
|
if colIdx == 0 && t.autoIndex {
|
||||||
|
out.WriteRune(' ')
|
||||||
|
if hint.isSeparatorRow {
|
||||||
|
out.WriteString("---:")
|
||||||
|
} else if hint.isRegularRow() {
|
||||||
|
out.WriteString(fmt.Sprintf("%d ", hint.rowNumber))
|
||||||
|
}
|
||||||
|
out.WriteRune('|')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) markdownRenderRows(out *strings.Builder, rows []rowStr, hint renderHint) {
|
||||||
|
if len(rows) > 0 {
|
||||||
|
for idx, row := range rows {
|
||||||
|
hint.rowNumber = idx + 1
|
||||||
|
t.markdownRenderRow(out, row, hint)
|
||||||
|
|
||||||
|
if idx == len(rows)-1 && hint.isHeaderRow {
|
||||||
|
t.markdownRenderRow(out, t.rowSeparator, renderHint{isSeparatorRow: true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) markdownRenderRowsFooter(out *strings.Builder) {
|
||||||
|
t.markdownRenderRows(out, t.rowsFooter, renderHint{isFooterRow: true})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) markdownRenderRowsHeader(out *strings.Builder) {
|
||||||
|
if len(t.rowsHeader) > 0 {
|
||||||
|
t.markdownRenderRows(out, t.rowsHeader, renderHint{isHeaderRow: true})
|
||||||
|
} else if t.autoIndex {
|
||||||
|
t.markdownRenderRows(out, []rowStr{t.getAutoIndexColumnIDs()}, renderHint{isAutoIndexRow: true, isHeaderRow: true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) markdownRenderTitle(out *strings.Builder) {
|
||||||
|
if t.title != "" {
|
||||||
|
out.WriteString("# ")
|
||||||
|
out.WriteString(t.title)
|
||||||
|
}
|
||||||
|
}
|
143
helper/table/render_markdown_test.go
Normal file
143
helper/table/render_markdown_test.go
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
package table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTable_RenderMarkdown(t *testing.T) {
|
||||||
|
tw := NewWriter()
|
||||||
|
tw.AppendHeader(testHeader)
|
||||||
|
tw.AppendRows(testRows)
|
||||||
|
tw.AppendRow(testRowNewLines)
|
||||||
|
tw.AppendRow(testRowPipes)
|
||||||
|
tw.AppendFooter(testFooter)
|
||||||
|
tw.SetCaption(testCaption)
|
||||||
|
tw.SetTitle(testTitle1)
|
||||||
|
|
||||||
|
compareOutput(t, tw.RenderMarkdown(), `
|
||||||
|
# Game of Thrones
|
||||||
|
| # | First Name | Last Name | Salary | |
|
||||||
|
| ---:| --- | --- | ---:| --- |
|
||||||
|
| 1 | Arya | Stark | 3000 | |
|
||||||
|
| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
|
||||||
|
| 300 | Tyrion | Lannister | 5000 | |
|
||||||
|
| 0 | Valar | Morghulis | 0 | Faceless<br/>Men |
|
||||||
|
| 0 | Valar | Morghulis | 0 | Faceless\|Men |
|
||||||
|
| | | Total | 10000 | |
|
||||||
|
_A Song of Ice and Fire_`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_RenderMarkdown_AutoIndex(t *testing.T) {
|
||||||
|
tw := NewWriter()
|
||||||
|
for rowIdx := 0; rowIdx < 10; rowIdx++ {
|
||||||
|
row := make(Row, 10)
|
||||||
|
for colIdx := 0; colIdx < 10; colIdx++ {
|
||||||
|
row[colIdx] = fmt.Sprintf("%s%d", AutoIndexColumnID(colIdx), rowIdx+1)
|
||||||
|
}
|
||||||
|
tw.AppendRow(row)
|
||||||
|
}
|
||||||
|
for rowIdx := 0; rowIdx < 1; rowIdx++ {
|
||||||
|
row := make(Row, 10)
|
||||||
|
for colIdx := 0; colIdx < 10; colIdx++ {
|
||||||
|
row[colIdx] = AutoIndexColumnID(colIdx) + "F"
|
||||||
|
}
|
||||||
|
tw.AppendFooter(row)
|
||||||
|
}
|
||||||
|
tw.SetAutoIndex(true)
|
||||||
|
tw.SetStyle(StyleLight)
|
||||||
|
|
||||||
|
compareOutput(t, tw.RenderMarkdown(), `
|
||||||
|
| | A | B | C | D | E | F | G | H | I | J |
|
||||||
|
| ---:| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| 1 | A1 | B1 | C1 | D1 | E1 | F1 | G1 | H1 | I1 | J1 |
|
||||||
|
| 2 | A2 | B2 | C2 | D2 | E2 | F2 | G2 | H2 | I2 | J2 |
|
||||||
|
| 3 | A3 | B3 | C3 | D3 | E3 | F3 | G3 | H3 | I3 | J3 |
|
||||||
|
| 4 | A4 | B4 | C4 | D4 | E4 | F4 | G4 | H4 | I4 | J4 |
|
||||||
|
| 5 | A5 | B5 | C5 | D5 | E5 | F5 | G5 | H5 | I5 | J5 |
|
||||||
|
| 6 | A6 | B6 | C6 | D6 | E6 | F6 | G6 | H6 | I6 | J6 |
|
||||||
|
| 7 | A7 | B7 | C7 | D7 | E7 | F7 | G7 | H7 | I7 | J7 |
|
||||||
|
| 8 | A8 | B8 | C8 | D8 | E8 | F8 | G8 | H8 | I8 | J8 |
|
||||||
|
| 9 | A9 | B9 | C9 | D9 | E9 | F9 | G9 | H9 | I9 | J9 |
|
||||||
|
| 10 | A10 | B10 | C10 | D10 | E10 | F10 | G10 | H10 | I10 | J10 |
|
||||||
|
| | AF | BF | CF | DF | EF | FF | GF | HF | IF | JF |`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_RenderMarkdown_Empty(t *testing.T) {
|
||||||
|
tw := NewWriter()
|
||||||
|
assert.Empty(t, tw.RenderMarkdown())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_RenderMarkdown_HiddenColumns(t *testing.T) {
|
||||||
|
tw := NewWriter()
|
||||||
|
tw.AppendHeader(testHeader)
|
||||||
|
tw.AppendRows(testRows)
|
||||||
|
tw.AppendFooter(testFooter)
|
||||||
|
|
||||||
|
// ensure sorting is done before hiding the columns
|
||||||
|
tw.SortBy([]SortBy{
|
||||||
|
{Name: "Salary", Mode: DscNumeric},
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("every column hidden", func(t *testing.T) {
|
||||||
|
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0, 1, 2, 3, 4}))
|
||||||
|
|
||||||
|
compareOutput(t, tw.RenderMarkdown(), "")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("first column hidden", func(t *testing.T) {
|
||||||
|
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0}))
|
||||||
|
|
||||||
|
compareOutput(t, tw.RenderMarkdown(), `
|
||||||
|
| First Name | Last Name | Salary | |
|
||||||
|
| --- | --- | ---:| --- |
|
||||||
|
| >>Tyrion | Lannister<< | 5013 | |
|
||||||
|
| >>Arya | Stark<< | 3013 | |
|
||||||
|
| >>Jon | Snow<< | 2013 | ~You know nothing, Jon Snow!~ |
|
||||||
|
| | Total | 10000 | |`)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("column hidden in the middle", func(t *testing.T) {
|
||||||
|
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{1}))
|
||||||
|
|
||||||
|
compareOutput(t, tw.RenderMarkdown(), `
|
||||||
|
| # | Last Name | Salary | |
|
||||||
|
| ---:| --- | ---:| --- |
|
||||||
|
| 307 | Lannister<< | 5013 | |
|
||||||
|
| 8 | Stark<< | 3013 | |
|
||||||
|
| 27 | Snow<< | 2013 | ~You know nothing, Jon Snow!~ |
|
||||||
|
| | Total | 10000 | |`)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("last column hidden", func(t *testing.T) {
|
||||||
|
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{4}))
|
||||||
|
|
||||||
|
compareOutput(t, tw.RenderMarkdown(), `
|
||||||
|
| # | First Name | Last Name | Salary |
|
||||||
|
| ---:| --- | --- | ---:|
|
||||||
|
| 307 | >>Tyrion | Lannister<< | 5013 |
|
||||||
|
| 8 | >>Arya | Stark<< | 3013 |
|
||||||
|
| 27 | >>Jon | Snow<< | 2013 |
|
||||||
|
| | | Total | 10000 |`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_RendeMarkdown_Sorted(t *testing.T) {
|
||||||
|
tw := NewWriter()
|
||||||
|
tw.AppendHeader(testHeader)
|
||||||
|
tw.AppendRows(testRows)
|
||||||
|
tw.AppendRow(Row{11, "Sansa", "Stark", 6000})
|
||||||
|
tw.AppendFooter(testFooter)
|
||||||
|
tw.SortBy([]SortBy{{Name: "Last Name", Mode: Asc}, {Name: "First Name", Mode: Asc}})
|
||||||
|
|
||||||
|
compareOutput(t, tw.RenderMarkdown(), `
|
||||||
|
| # | First Name | Last Name | Salary | |
|
||||||
|
| ---:| --- | --- | ---:| --- |
|
||||||
|
| 300 | Tyrion | Lannister | 5000 | |
|
||||||
|
| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
|
||||||
|
| 1 | Arya | Stark | 3000 | |
|
||||||
|
| 11 | Sansa | Stark | 6000 | |
|
||||||
|
| | | Total | 10000 | |`)
|
||||||
|
}
|
1863
helper/table/render_test.go
Normal file
1863
helper/table/render_test.go
Normal file
File diff suppressed because it is too large
Load Diff
127
helper/table/sort.go
Normal file
127
helper/table/sort.go
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
package table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SortBy defines What to sort (Column Name or Number), and How to sort (Mode).
|
||||||
|
type SortBy struct {
|
||||||
|
// Name is the name of the Column as it appears in the first Header row.
|
||||||
|
// If a Header is not provided, or the name is not found in the header, this
|
||||||
|
// will not work.
|
||||||
|
Name string
|
||||||
|
// Number is the Column # from left. When specified, it overrides the Name
|
||||||
|
// property. If you know the exact Column number, use this instead of Name.
|
||||||
|
Number int
|
||||||
|
|
||||||
|
// Mode tells the Writer how to Sort. Asc/Dsc/etc.
|
||||||
|
Mode SortMode
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortMode defines How to sort.
|
||||||
|
type SortMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Asc sorts the column in Ascending order alphabetically.
|
||||||
|
Asc SortMode = iota
|
||||||
|
// AscNumeric sorts the column in Ascending order numerically.
|
||||||
|
AscNumeric
|
||||||
|
// Dsc sorts the column in Descending order alphabetically.
|
||||||
|
Dsc
|
||||||
|
// DscNumeric sorts the column in Descending order numerically.
|
||||||
|
DscNumeric
|
||||||
|
)
|
||||||
|
|
||||||
|
type rowsSorter struct {
|
||||||
|
rows []rowStr
|
||||||
|
sortBy []SortBy
|
||||||
|
sortedIndices []int
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSortedRowIndices sorts and returns the row indices in Sorted order as
|
||||||
|
// directed by Table.sortBy which can be set using Table.SortBy(...)
|
||||||
|
func (t *Table) getSortedRowIndices() []int {
|
||||||
|
sortedIndices := make([]int, len(t.rows))
|
||||||
|
for idx := range t.rows {
|
||||||
|
sortedIndices[idx] = idx
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.sortBy != nil && len(t.sortBy) > 0 {
|
||||||
|
sort.Sort(rowsSorter{
|
||||||
|
rows: t.rows,
|
||||||
|
sortBy: t.parseSortBy(t.sortBy),
|
||||||
|
sortedIndices: sortedIndices,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortedIndices
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) parseSortBy(sortBy []SortBy) []SortBy {
|
||||||
|
var resSortBy []SortBy
|
||||||
|
for _, col := range sortBy {
|
||||||
|
colNum := 0
|
||||||
|
if col.Number > 0 && col.Number <= t.numColumns {
|
||||||
|
colNum = col.Number
|
||||||
|
} else if col.Name != "" && len(t.rowsHeader) > 0 {
|
||||||
|
for idx, colName := range t.rowsHeader[0] {
|
||||||
|
if col.Name == colName {
|
||||||
|
colNum = idx + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if colNum > 0 {
|
||||||
|
resSortBy = append(resSortBy, SortBy{
|
||||||
|
Name: col.Name,
|
||||||
|
Number: colNum,
|
||||||
|
Mode: col.Mode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resSortBy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs rowsSorter) Len() int {
|
||||||
|
return len(rs.rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs rowsSorter) Swap(i, j int) {
|
||||||
|
rs.sortedIndices[i], rs.sortedIndices[j] = rs.sortedIndices[j], rs.sortedIndices[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs rowsSorter) Less(i, j int) bool {
|
||||||
|
realI, realJ := rs.sortedIndices[i], rs.sortedIndices[j]
|
||||||
|
for _, col := range rs.sortBy {
|
||||||
|
rowI, rowJ, colIdx := rs.rows[realI], rs.rows[realJ], col.Number-1
|
||||||
|
if colIdx < len(rowI) && colIdx < len(rowJ) {
|
||||||
|
shouldContinue, returnValue := rs.lessColumns(rowI, rowJ, colIdx, col)
|
||||||
|
if !shouldContinue {
|
||||||
|
return returnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs rowsSorter) lessColumns(rowI rowStr, rowJ rowStr, colIdx int, col SortBy) (bool, bool) {
|
||||||
|
if rowI[colIdx] == rowJ[colIdx] {
|
||||||
|
return true, false
|
||||||
|
} else if col.Mode == Asc {
|
||||||
|
return false, rowI[colIdx] < rowJ[colIdx]
|
||||||
|
} else if col.Mode == Dsc {
|
||||||
|
return false, rowI[colIdx] > rowJ[colIdx]
|
||||||
|
}
|
||||||
|
|
||||||
|
iVal, iErr := strconv.ParseFloat(rowI[colIdx], 64)
|
||||||
|
jVal, jErr := strconv.ParseFloat(rowJ[colIdx], 64)
|
||||||
|
if iErr == nil && jErr == nil {
|
||||||
|
if col.Mode == AscNumeric {
|
||||||
|
return false, iVal < jVal
|
||||||
|
} else if col.Mode == DscNumeric {
|
||||||
|
return false, jVal < iVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, false
|
||||||
|
}
|
148
helper/table/sort_test.go
Normal file
148
helper/table/sort_test.go
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
package table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTable_sortRows_WithName(t *testing.T) {
|
||||||
|
table := Table{}
|
||||||
|
table.AppendHeader(Row{"#", "First Name", "Last Name", "Salary"})
|
||||||
|
table.AppendRows([]Row{
|
||||||
|
{1, "Arya", "Stark", 3000},
|
||||||
|
{11, "Sansa", "Stark", 3000},
|
||||||
|
{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"},
|
||||||
|
{300, "Tyrion", "Lannister", 5000},
|
||||||
|
})
|
||||||
|
table.SetStyle(StyleDefault)
|
||||||
|
table.initForRenderRows()
|
||||||
|
|
||||||
|
// sort by nothing
|
||||||
|
assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
// sort by "#"
|
||||||
|
table.SortBy([]SortBy{{Name: "#", Mode: AscNumeric}})
|
||||||
|
assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
table.SortBy([]SortBy{{Name: "#", Mode: DscNumeric}})
|
||||||
|
assert.Equal(t, []int{3, 2, 1, 0}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
// sort by First Name, Last Name
|
||||||
|
table.SortBy([]SortBy{{Name: "First Name", Mode: Asc}, {Name: "Last Name", Mode: Asc}})
|
||||||
|
assert.Equal(t, []int{0, 2, 1, 3}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
table.SortBy([]SortBy{{Name: "First Name", Mode: Asc}, {Name: "Last Name", Mode: Dsc}})
|
||||||
|
assert.Equal(t, []int{0, 2, 1, 3}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
table.SortBy([]SortBy{{Name: "First Name", Mode: Dsc}, {Name: "Last Name", Mode: Asc}})
|
||||||
|
assert.Equal(t, []int{3, 1, 2, 0}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
table.SortBy([]SortBy{{Name: "First Name", Mode: Dsc}, {Name: "Last Name", Mode: Dsc}})
|
||||||
|
assert.Equal(t, []int{3, 1, 2, 0}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
// sort by Last Name, First Name
|
||||||
|
table.SortBy([]SortBy{{Name: "Last Name", Mode: Asc}, {Name: "First Name", Mode: Asc}})
|
||||||
|
assert.Equal(t, []int{3, 2, 0, 1}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
table.SortBy([]SortBy{{Name: "Last Name", Mode: Asc}, {Name: "First Name", Mode: Dsc}})
|
||||||
|
assert.Equal(t, []int{3, 2, 1, 0}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
table.SortBy([]SortBy{{Name: "Last Name", Mode: Dsc}, {Name: "First Name", Mode: Asc}})
|
||||||
|
assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
table.SortBy([]SortBy{{Name: "Last Name", Mode: Dsc}, {Name: "First Name", Mode: Dsc}})
|
||||||
|
assert.Equal(t, []int{1, 0, 2, 3}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
// sort by Unknown Column
|
||||||
|
table.SortBy([]SortBy{{Name: "Last Name", Mode: Dsc}, {Name: "Foo Bar", Mode: Dsc}})
|
||||||
|
assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
// sort by Salary
|
||||||
|
table.SortBy([]SortBy{{Name: "Salary", Mode: AscNumeric}})
|
||||||
|
assert.Equal(t, []int{2, 0, 1, 3}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
table.SortBy([]SortBy{{Name: "Salary", Mode: DscNumeric}})
|
||||||
|
assert.Equal(t, []int{3, 0, 1, 2}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
table.SortBy(nil)
|
||||||
|
assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_sortRows_WithoutName(t *testing.T) {
|
||||||
|
table := Table{}
|
||||||
|
table.AppendRows([]Row{
|
||||||
|
{1, "Arya", "Stark", 3000},
|
||||||
|
{11, "Sansa", "Stark", 3000},
|
||||||
|
{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"},
|
||||||
|
{300, "Tyrion", "Lannister", 5000},
|
||||||
|
})
|
||||||
|
table.SetStyle(StyleDefault)
|
||||||
|
table.initForRenderRows()
|
||||||
|
|
||||||
|
// sort by nothing
|
||||||
|
assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
// sort by "#"
|
||||||
|
table.SortBy([]SortBy{{Number: 1, Mode: AscNumeric}})
|
||||||
|
assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
table.SortBy([]SortBy{{Number: 1, Mode: DscNumeric}})
|
||||||
|
assert.Equal(t, []int{3, 2, 1, 0}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
// sort by First Name, Last Name
|
||||||
|
table.SortBy([]SortBy{{Number: 2, Mode: Asc}, {Number: 3, Mode: Asc}})
|
||||||
|
assert.Equal(t, []int{0, 2, 1, 3}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
table.SortBy([]SortBy{{Number: 2, Mode: Asc}, {Number: 3, Mode: Dsc}})
|
||||||
|
assert.Equal(t, []int{0, 2, 1, 3}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
table.SortBy([]SortBy{{Number: 2, Mode: Dsc}, {Number: 3, Mode: Asc}})
|
||||||
|
assert.Equal(t, []int{3, 1, 2, 0}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
table.SortBy([]SortBy{{Number: 2, Mode: Dsc}, {Number: 3, Mode: Dsc}})
|
||||||
|
assert.Equal(t, []int{3, 1, 2, 0}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
// sort by Last Name, First Name
|
||||||
|
table.SortBy([]SortBy{{Number: 3, Mode: Asc}, {Number: 2, Mode: Asc}})
|
||||||
|
assert.Equal(t, []int{3, 2, 0, 1}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
table.SortBy([]SortBy{{Number: 3, Mode: Asc}, {Number: 2, Mode: Dsc}})
|
||||||
|
assert.Equal(t, []int{3, 2, 1, 0}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
table.SortBy([]SortBy{{Number: 3, Mode: Dsc}, {Number: 2, Mode: Asc}})
|
||||||
|
assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
table.SortBy([]SortBy{{Number: 3, Mode: Dsc}, {Number: 2, Mode: Dsc}})
|
||||||
|
assert.Equal(t, []int{1, 0, 2, 3}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
// sort by Unknown Column
|
||||||
|
table.SortBy([]SortBy{{Number: 3, Mode: Dsc}, {Number: 99, Mode: Dsc}})
|
||||||
|
assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
// sort by Salary
|
||||||
|
table.SortBy([]SortBy{{Number: 4, Mode: AscNumeric}})
|
||||||
|
assert.Equal(t, []int{2, 0, 1, 3}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
table.SortBy([]SortBy{{Number: 4, Mode: DscNumeric}})
|
||||||
|
assert.Equal(t, []int{3, 0, 1, 2}, table.getSortedRowIndices())
|
||||||
|
|
||||||
|
table.SortBy(nil)
|
||||||
|
assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_sortRows_InvalidMode(t *testing.T) {
|
||||||
|
table := Table{}
|
||||||
|
table.AppendRows([]Row{
|
||||||
|
{1, "Arya", "Stark", 3000},
|
||||||
|
{11, "Sansa", "Stark", 3000},
|
||||||
|
{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"},
|
||||||
|
{300, "Tyrion", "Lannister", 5000},
|
||||||
|
})
|
||||||
|
table.SetStyle(StyleDefault)
|
||||||
|
table.initForRenderRows()
|
||||||
|
|
||||||
|
// sort by "First Name"
|
||||||
|
table.SortBy([]SortBy{{Number: 2, Mode: AscNumeric}})
|
||||||
|
assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
|
||||||
|
}
|
879
helper/table/style.go
Normal file
879
helper/table/style.go
Normal file
@ -0,0 +1,879 @@
|
|||||||
|
package table
|
||||||
|
|
||||||
|
import "git.hexq.cn/tiglog/golib/helper/text"
|
||||||
|
|
||||||
|
// Style declares how to render the Table and provides very fine-grained control
|
||||||
|
// on how the Table gets rendered on the Console.
|
||||||
|
type Style struct {
|
||||||
|
Name string // name of the Style
|
||||||
|
Box BoxStyle // characters to use for the boxes
|
||||||
|
Color ColorOptions // colors to use for the rows and columns
|
||||||
|
Format FormatOptions // formatting options for the rows and columns
|
||||||
|
HTML HTMLOptions // rendering options for HTML mode
|
||||||
|
Options Options // misc. options for the table
|
||||||
|
Title TitleOptions // formation options for the title text
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// StyleDefault renders a Table like below:
|
||||||
|
// +-----+------------+-----------+--------+-----------------------------+
|
||||||
|
// | # | FIRST NAME | LAST NAME | SALARY | |
|
||||||
|
// +-----+------------+-----------+--------+-----------------------------+
|
||||||
|
// | 1 | Arya | Stark | 3000 | |
|
||||||
|
// | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
|
||||||
|
// | 300 | Tyrion | Lannister | 5000 | |
|
||||||
|
// +-----+------------+-----------+--------+-----------------------------+
|
||||||
|
// | | | TOTAL | 10000 | |
|
||||||
|
// +-----+------------+-----------+--------+-----------------------------+
|
||||||
|
StyleDefault = Style{
|
||||||
|
Name: "StyleDefault",
|
||||||
|
Box: StyleBoxDefault,
|
||||||
|
Color: ColorOptionsDefault,
|
||||||
|
Format: FormatOptionsDefault,
|
||||||
|
HTML: DefaultHTMLOptions,
|
||||||
|
Options: OptionsDefault,
|
||||||
|
Title: TitleOptionsDefault,
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleBold renders a Table like below:
|
||||||
|
// ┏━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
|
// ┃ # ┃ FIRST NAME ┃ LAST NAME ┃ SALARY ┃ ┃
|
||||||
|
// ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||||
|
// ┃ 1 ┃ Arya ┃ Stark ┃ 3000 ┃ ┃
|
||||||
|
// ┃ 20 ┃ Jon ┃ Snow ┃ 2000 ┃ You know nothing, Jon Snow! ┃
|
||||||
|
// ┃ 300 ┃ Tyrion ┃ Lannister ┃ 5000 ┃ ┃
|
||||||
|
// ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||||
|
// ┃ ┃ ┃ TOTAL ┃ 10000 ┃ ┃
|
||||||
|
// ┗━━━━━┻━━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||||
|
StyleBold = Style{
|
||||||
|
Name: "StyleBold",
|
||||||
|
Box: StyleBoxBold,
|
||||||
|
Color: ColorOptionsDefault,
|
||||||
|
Format: FormatOptionsDefault,
|
||||||
|
HTML: DefaultHTMLOptions,
|
||||||
|
Options: OptionsDefault,
|
||||||
|
Title: TitleOptionsDefault,
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleColoredBright renders a Table without any borders or separators,
|
||||||
|
// and with Black text on Cyan background for Header/Footer and
|
||||||
|
// White background for other rows.
|
||||||
|
StyleColoredBright = Style{
|
||||||
|
Name: "StyleColoredBright",
|
||||||
|
Box: StyleBoxDefault,
|
||||||
|
Color: ColorOptionsBright,
|
||||||
|
Format: FormatOptionsDefault,
|
||||||
|
HTML: DefaultHTMLOptions,
|
||||||
|
Options: OptionsNoBordersAndSeparators,
|
||||||
|
Title: TitleOptionsDark,
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleColoredDark renders a Table without any borders or separators, and
|
||||||
|
// with Header/Footer in Cyan text and other rows with White text, all on
|
||||||
|
// Black background.
|
||||||
|
StyleColoredDark = Style{
|
||||||
|
Name: "StyleColoredDark",
|
||||||
|
Box: StyleBoxDefault,
|
||||||
|
Color: ColorOptionsDark,
|
||||||
|
Format: FormatOptionsDefault,
|
||||||
|
HTML: DefaultHTMLOptions,
|
||||||
|
Options: OptionsNoBordersAndSeparators,
|
||||||
|
Title: TitleOptionsBright,
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleColoredBlackOnBlueWhite renders a Table without any borders or
|
||||||
|
// separators, and with Black text on Blue background for Header/Footer and
|
||||||
|
// White background for other rows.
|
||||||
|
StyleColoredBlackOnBlueWhite = Style{
|
||||||
|
Name: "StyleColoredBlackOnBlueWhite",
|
||||||
|
Box: StyleBoxDefault,
|
||||||
|
Color: ColorOptionsBlackOnBlueWhite,
|
||||||
|
Format: FormatOptionsDefault,
|
||||||
|
HTML: DefaultHTMLOptions,
|
||||||
|
Options: OptionsNoBordersAndSeparators,
|
||||||
|
Title: TitleOptionsBlueOnBlack,
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleColoredBlackOnCyanWhite renders a Table without any borders or
|
||||||
|
// separators, and with Black text on Cyan background for Header/Footer and
|
||||||
|
// White background for other rows.
|
||||||
|
StyleColoredBlackOnCyanWhite = Style{
|
||||||
|
Name: "StyleColoredBlackOnCyanWhite",
|
||||||
|
Box: StyleBoxDefault,
|
||||||
|
Color: ColorOptionsBlackOnCyanWhite,
|
||||||
|
Format: FormatOptionsDefault,
|
||||||
|
HTML: DefaultHTMLOptions,
|
||||||
|
Options: OptionsNoBordersAndSeparators,
|
||||||
|
Title: TitleOptionsCyanOnBlack,
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleColoredBlackOnGreenWhite renders a Table without any borders or
|
||||||
|
// separators, and with Black text on Green background for Header/Footer and
|
||||||
|
// White background for other rows.
|
||||||
|
StyleColoredBlackOnGreenWhite = Style{
|
||||||
|
Name: "StyleColoredBlackOnGreenWhite",
|
||||||
|
Box: StyleBoxDefault,
|
||||||
|
Color: ColorOptionsBlackOnGreenWhite,
|
||||||
|
Format: FormatOptionsDefault,
|
||||||
|
HTML: DefaultHTMLOptions,
|
||||||
|
Options: OptionsNoBordersAndSeparators,
|
||||||
|
Title: TitleOptionsGreenOnBlack,
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleColoredBlackOnMagentaWhite renders a Table without any borders or
|
||||||
|
// separators, and with Black text on Magenta background for Header/Footer and
|
||||||
|
// White background for other rows.
|
||||||
|
StyleColoredBlackOnMagentaWhite = Style{
|
||||||
|
Name: "StyleColoredBlackOnMagentaWhite",
|
||||||
|
Box: StyleBoxDefault,
|
||||||
|
Color: ColorOptionsBlackOnMagentaWhite,
|
||||||
|
Format: FormatOptionsDefault,
|
||||||
|
HTML: DefaultHTMLOptions,
|
||||||
|
Options: OptionsNoBordersAndSeparators,
|
||||||
|
Title: TitleOptionsMagentaOnBlack,
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleColoredBlackOnYellowWhite renders a Table without any borders or
|
||||||
|
// separators, and with Black text on Yellow background for Header/Footer and
|
||||||
|
// White background for other rows.
|
||||||
|
StyleColoredBlackOnYellowWhite = Style{
|
||||||
|
Name: "StyleColoredBlackOnYellowWhite",
|
||||||
|
Box: StyleBoxDefault,
|
||||||
|
Color: ColorOptionsBlackOnYellowWhite,
|
||||||
|
Format: FormatOptionsDefault,
|
||||||
|
HTML: DefaultHTMLOptions,
|
||||||
|
Options: OptionsNoBordersAndSeparators,
|
||||||
|
Title: TitleOptionsYellowOnBlack,
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleColoredBlackOnRedWhite renders a Table without any borders or
|
||||||
|
// separators, and with Black text on Red background for Header/Footer and
|
||||||
|
// White background for other rows.
|
||||||
|
StyleColoredBlackOnRedWhite = Style{
|
||||||
|
Name: "StyleColoredBlackOnRedWhite",
|
||||||
|
Box: StyleBoxDefault,
|
||||||
|
Color: ColorOptionsBlackOnRedWhite,
|
||||||
|
Format: FormatOptionsDefault,
|
||||||
|
HTML: DefaultHTMLOptions,
|
||||||
|
Options: OptionsNoBordersAndSeparators,
|
||||||
|
Title: TitleOptionsRedOnBlack,
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleColoredBlueWhiteOnBlack renders a Table without any borders or
|
||||||
|
// separators, and with Header/Footer in Blue text and other rows with
|
||||||
|
// White text, all on Black background.
|
||||||
|
StyleColoredBlueWhiteOnBlack = Style{
|
||||||
|
Name: "StyleColoredBlueWhiteOnBlack",
|
||||||
|
Box: StyleBoxDefault,
|
||||||
|
Color: ColorOptionsBlueWhiteOnBlack,
|
||||||
|
Format: FormatOptionsDefault,
|
||||||
|
HTML: DefaultHTMLOptions,
|
||||||
|
Options: OptionsNoBordersAndSeparators,
|
||||||
|
Title: TitleOptionsBlackOnBlue,
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleColoredCyanWhiteOnBlack renders a Table without any borders or
|
||||||
|
// separators, and with Header/Footer in Cyan text and other rows with
|
||||||
|
// White text, all on Black background.
|
||||||
|
StyleColoredCyanWhiteOnBlack = Style{
|
||||||
|
Name: "StyleColoredCyanWhiteOnBlack",
|
||||||
|
Box: StyleBoxDefault,
|
||||||
|
Color: ColorOptionsCyanWhiteOnBlack,
|
||||||
|
Format: FormatOptionsDefault,
|
||||||
|
HTML: DefaultHTMLOptions,
|
||||||
|
Options: OptionsNoBordersAndSeparators,
|
||||||
|
Title: TitleOptionsBlackOnCyan,
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleColoredGreenWhiteOnBlack renders a Table without any borders or
|
||||||
|
// separators, and with Header/Footer in Green text and other rows with
|
||||||
|
// White text, all on Black background.
|
||||||
|
StyleColoredGreenWhiteOnBlack = Style{
|
||||||
|
Name: "StyleColoredGreenWhiteOnBlack",
|
||||||
|
Box: StyleBoxDefault,
|
||||||
|
Color: ColorOptionsGreenWhiteOnBlack,
|
||||||
|
Format: FormatOptionsDefault,
|
||||||
|
HTML: DefaultHTMLOptions,
|
||||||
|
Options: OptionsNoBordersAndSeparators,
|
||||||
|
Title: TitleOptionsBlackOnGreen,
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleColoredMagentaWhiteOnBlack renders a Table without any borders or
|
||||||
|
// separators, and with Header/Footer in Magenta text and other rows with
|
||||||
|
// White text, all on Black background.
|
||||||
|
StyleColoredMagentaWhiteOnBlack = Style{
|
||||||
|
Name: "StyleColoredMagentaWhiteOnBlack",
|
||||||
|
Box: StyleBoxDefault,
|
||||||
|
Color: ColorOptionsMagentaWhiteOnBlack,
|
||||||
|
Format: FormatOptionsDefault,
|
||||||
|
HTML: DefaultHTMLOptions,
|
||||||
|
Options: OptionsNoBordersAndSeparators,
|
||||||
|
Title: TitleOptionsBlackOnMagenta,
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleColoredRedWhiteOnBlack renders a Table without any borders or
|
||||||
|
// separators, and with Header/Footer in Red text and other rows with
|
||||||
|
// White text, all on Black background.
|
||||||
|
StyleColoredRedWhiteOnBlack = Style{
|
||||||
|
Name: "StyleColoredRedWhiteOnBlack",
|
||||||
|
Box: StyleBoxDefault,
|
||||||
|
Color: ColorOptionsRedWhiteOnBlack,
|
||||||
|
Format: FormatOptionsDefault,
|
||||||
|
HTML: DefaultHTMLOptions,
|
||||||
|
Options: OptionsNoBordersAndSeparators,
|
||||||
|
Title: TitleOptionsBlackOnRed,
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleColoredYellowWhiteOnBlack renders a Table without any borders or
|
||||||
|
// separators, and with Header/Footer in Yellow text and other rows with
|
||||||
|
// White text, all on Black background.
|
||||||
|
StyleColoredYellowWhiteOnBlack = Style{
|
||||||
|
Name: "StyleColoredYellowWhiteOnBlack",
|
||||||
|
Box: StyleBoxDefault,
|
||||||
|
Color: ColorOptionsYellowWhiteOnBlack,
|
||||||
|
Format: FormatOptionsDefault,
|
||||||
|
HTML: DefaultHTMLOptions,
|
||||||
|
Options: OptionsNoBordersAndSeparators,
|
||||||
|
Title: TitleOptionsBlackOnYellow,
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleDouble renders a Table like below:
|
||||||
|
// ╔═════╦════════════╦═══════════╦════════╦═════════════════════════════╗
|
||||||
|
// ║ # ║ FIRST NAME ║ LAST NAME ║ SALARY ║ ║
|
||||||
|
// ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣
|
||||||
|
// ║ 1 ║ Arya ║ Stark ║ 3000 ║ ║
|
||||||
|
// ║ 20 ║ Jon ║ Snow ║ 2000 ║ You know nothing, Jon Snow! ║
|
||||||
|
// ║ 300 ║ Tyrion ║ Lannister ║ 5000 ║ ║
|
||||||
|
// ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣
|
||||||
|
// ║ ║ ║ TOTAL ║ 10000 ║ ║
|
||||||
|
// ╚═════╩════════════╩═══════════╩════════╩═════════════════════════════╝
|
||||||
|
StyleDouble = Style{
|
||||||
|
Name: "StyleDouble",
|
||||||
|
Box: StyleBoxDouble,
|
||||||
|
Color: ColorOptionsDefault,
|
||||||
|
Format: FormatOptionsDefault,
|
||||||
|
HTML: DefaultHTMLOptions,
|
||||||
|
Options: OptionsDefault,
|
||||||
|
Title: TitleOptionsDefault,
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleLight renders a Table like below:
|
||||||
|
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
|
||||||
|
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||||
|
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||||
|
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||||
|
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||||
|
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||||
|
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||||
|
// │ │ │ TOTAL │ 10000 │ │
|
||||||
|
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
|
||||||
|
StyleLight = Style{
|
||||||
|
Name: "StyleLight",
|
||||||
|
Box: StyleBoxLight,
|
||||||
|
Color: ColorOptionsDefault,
|
||||||
|
Format: FormatOptionsDefault,
|
||||||
|
HTML: DefaultHTMLOptions,
|
||||||
|
Options: OptionsDefault,
|
||||||
|
Title: TitleOptionsDefault,
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleRounded renders a Table like below:
|
||||||
|
// ╭─────┬────────────┬───────────┬────────┬─────────────────────────────╮
|
||||||
|
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||||
|
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||||
|
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||||
|
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||||
|
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||||
|
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||||
|
// │ │ │ TOTAL │ 10000 │ │
|
||||||
|
// ╰─────┴────────────┴───────────┴────────┴─────────────────────────────╯
|
||||||
|
StyleRounded = Style{
|
||||||
|
Name: "StyleRounded",
|
||||||
|
Box: StyleBoxRounded,
|
||||||
|
Color: ColorOptionsDefault,
|
||||||
|
Format: FormatOptionsDefault,
|
||||||
|
HTML: DefaultHTMLOptions,
|
||||||
|
Options: OptionsDefault,
|
||||||
|
Title: TitleOptionsDefault,
|
||||||
|
}
|
||||||
|
|
||||||
|
// styleTest renders a Table like below:
|
||||||
|
// (-----^------------^-----------^--------^-----------------------------)
|
||||||
|
// [< #>|<FIRST NAME>|<LAST NAME>|<SALARY>|< >]
|
||||||
|
// {-----+------------+-----------+--------+-----------------------------}
|
||||||
|
// [< 1>|<Arya >|<Stark >|< 3000>|< >]
|
||||||
|
// [< 20>|<Jon >|<Snow >|< 2000>|<You know nothing, Jon Snow!>]
|
||||||
|
// [<300>|<Tyrion >|<Lannister>|< 5000>|< >]
|
||||||
|
// {-----+------------+-----------+--------+-----------------------------}
|
||||||
|
// [< >|< >|<TOTAL >|< 10000>|< >]
|
||||||
|
// \-----v------------v-----------v--------v-----------------------------/
|
||||||
|
styleTest = Style{
|
||||||
|
Name: "styleTest",
|
||||||
|
Box: styleBoxTest,
|
||||||
|
Color: ColorOptionsDefault,
|
||||||
|
Format: FormatOptionsDefault,
|
||||||
|
HTML: DefaultHTMLOptions,
|
||||||
|
Options: OptionsDefault,
|
||||||
|
Title: TitleOptionsDefault,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// BoxStyle defines the characters/strings to use to render the borders and
|
||||||
|
// separators for the Table.
|
||||||
|
type BoxStyle struct {
|
||||||
|
BottomLeft string
|
||||||
|
BottomRight string
|
||||||
|
BottomSeparator string
|
||||||
|
EmptySeparator string
|
||||||
|
Left string
|
||||||
|
LeftSeparator string
|
||||||
|
MiddleHorizontal string
|
||||||
|
MiddleSeparator string
|
||||||
|
MiddleVertical string
|
||||||
|
PaddingLeft string
|
||||||
|
PaddingRight string
|
||||||
|
PageSeparator string
|
||||||
|
Right string
|
||||||
|
RightSeparator string
|
||||||
|
TopLeft string
|
||||||
|
TopRight string
|
||||||
|
TopSeparator string
|
||||||
|
UnfinishedRow string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// StyleBoxDefault defines a Boxed-Table like below:
|
||||||
|
// +-----+------------+-----------+--------+-----------------------------+
|
||||||
|
// | # | FIRST NAME | LAST NAME | SALARY | |
|
||||||
|
// +-----+------------+-----------+--------+-----------------------------+
|
||||||
|
// | 1 | Arya | Stark | 3000 | |
|
||||||
|
// | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
|
||||||
|
// | 300 | Tyrion | Lannister | 5000 | |
|
||||||
|
// +-----+------------+-----------+--------+-----------------------------+
|
||||||
|
// | | | TOTAL | 10000 | |
|
||||||
|
// +-----+------------+-----------+--------+-----------------------------+
|
||||||
|
StyleBoxDefault = BoxStyle{
|
||||||
|
BottomLeft: "+",
|
||||||
|
BottomRight: "+",
|
||||||
|
BottomSeparator: "+",
|
||||||
|
EmptySeparator: text.RepeatAndTrim(" ", text.RuneWidthWithoutEscSequences("+")),
|
||||||
|
Left: "|",
|
||||||
|
LeftSeparator: "+",
|
||||||
|
MiddleHorizontal: "-",
|
||||||
|
MiddleSeparator: "+",
|
||||||
|
MiddleVertical: "|",
|
||||||
|
PaddingLeft: " ",
|
||||||
|
PaddingRight: " ",
|
||||||
|
PageSeparator: "\n",
|
||||||
|
Right: "|",
|
||||||
|
RightSeparator: "+",
|
||||||
|
TopLeft: "+",
|
||||||
|
TopRight: "+",
|
||||||
|
TopSeparator: "+",
|
||||||
|
UnfinishedRow: " ~",
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleBoxBold defines a Boxed-Table like below:
|
||||||
|
// ┏━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
|
// ┃ # ┃ FIRST NAME ┃ LAST NAME ┃ SALARY ┃ ┃
|
||||||
|
// ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||||
|
// ┃ 1 ┃ Arya ┃ Stark ┃ 3000 ┃ ┃
|
||||||
|
// ┃ 20 ┃ Jon ┃ Snow ┃ 2000 ┃ You know nothing, Jon Snow! ┃
|
||||||
|
// ┃ 300 ┃ Tyrion ┃ Lannister ┃ 5000 ┃ ┃
|
||||||
|
// ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||||
|
// ┃ ┃ ┃ TOTAL ┃ 10000 ┃ ┃
|
||||||
|
// ┗━━━━━┻━━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||||
|
StyleBoxBold = BoxStyle{
|
||||||
|
BottomLeft: "┗",
|
||||||
|
BottomRight: "┛",
|
||||||
|
BottomSeparator: "┻",
|
||||||
|
EmptySeparator: text.RepeatAndTrim(" ", text.RuneWidthWithoutEscSequences("╋")),
|
||||||
|
Left: "┃",
|
||||||
|
LeftSeparator: "┣",
|
||||||
|
MiddleHorizontal: "━",
|
||||||
|
MiddleSeparator: "╋",
|
||||||
|
MiddleVertical: "┃",
|
||||||
|
PaddingLeft: " ",
|
||||||
|
PaddingRight: " ",
|
||||||
|
PageSeparator: "\n",
|
||||||
|
Right: "┃",
|
||||||
|
RightSeparator: "┫",
|
||||||
|
TopLeft: "┏",
|
||||||
|
TopRight: "┓",
|
||||||
|
TopSeparator: "┳",
|
||||||
|
UnfinishedRow: " ≈",
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleBoxDouble defines a Boxed-Table like below:
|
||||||
|
// ╔═════╦════════════╦═══════════╦════════╦═════════════════════════════╗
|
||||||
|
// ║ # ║ FIRST NAME ║ LAST NAME ║ SALARY ║ ║
|
||||||
|
// ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣
|
||||||
|
// ║ 1 ║ Arya ║ Stark ║ 3000 ║ ║
|
||||||
|
// ║ 20 ║ Jon ║ Snow ║ 2000 ║ You know nothing, Jon Snow! ║
|
||||||
|
// ║ 300 ║ Tyrion ║ Lannister ║ 5000 ║ ║
|
||||||
|
// ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣
|
||||||
|
// ║ ║ ║ TOTAL ║ 10000 ║ ║
|
||||||
|
// ╚═════╩════════════╩═══════════╩════════╩═════════════════════════════╝
|
||||||
|
StyleBoxDouble = BoxStyle{
|
||||||
|
BottomLeft: "╚",
|
||||||
|
BottomRight: "╝",
|
||||||
|
BottomSeparator: "╩",
|
||||||
|
EmptySeparator: text.RepeatAndTrim(" ", text.RuneWidthWithoutEscSequences("╬")),
|
||||||
|
Left: "║",
|
||||||
|
LeftSeparator: "╠",
|
||||||
|
MiddleHorizontal: "═",
|
||||||
|
MiddleSeparator: "╬",
|
||||||
|
MiddleVertical: "║",
|
||||||
|
PaddingLeft: " ",
|
||||||
|
PaddingRight: " ",
|
||||||
|
PageSeparator: "\n",
|
||||||
|
Right: "║",
|
||||||
|
RightSeparator: "╣",
|
||||||
|
TopLeft: "╔",
|
||||||
|
TopRight: "╗",
|
||||||
|
TopSeparator: "╦",
|
||||||
|
UnfinishedRow: " ≈",
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleBoxLight defines a Boxed-Table like below:
|
||||||
|
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
|
||||||
|
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||||
|
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||||
|
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||||
|
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||||
|
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||||
|
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||||
|
// │ │ │ TOTAL │ 10000 │ │
|
||||||
|
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
|
||||||
|
StyleBoxLight = BoxStyle{
|
||||||
|
BottomLeft: "└",
|
||||||
|
BottomRight: "┘",
|
||||||
|
BottomSeparator: "┴",
|
||||||
|
EmptySeparator: text.RepeatAndTrim(" ", text.RuneWidthWithoutEscSequences("┼")),
|
||||||
|
Left: "│",
|
||||||
|
LeftSeparator: "├",
|
||||||
|
MiddleHorizontal: "─",
|
||||||
|
MiddleSeparator: "┼",
|
||||||
|
MiddleVertical: "│",
|
||||||
|
PaddingLeft: " ",
|
||||||
|
PaddingRight: " ",
|
||||||
|
PageSeparator: "\n",
|
||||||
|
Right: "│",
|
||||||
|
RightSeparator: "┤",
|
||||||
|
TopLeft: "┌",
|
||||||
|
TopRight: "┐",
|
||||||
|
TopSeparator: "┬",
|
||||||
|
UnfinishedRow: " ≈",
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleBoxRounded defines a Boxed-Table like below:
|
||||||
|
// ╭─────┬────────────┬───────────┬────────┬─────────────────────────────╮
|
||||||
|
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||||
|
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||||
|
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||||
|
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||||
|
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||||
|
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||||
|
// │ │ │ TOTAL │ 10000 │ │
|
||||||
|
// ╰─────┴────────────┴───────────┴────────┴─────────────────────────────╯
|
||||||
|
StyleBoxRounded = BoxStyle{
|
||||||
|
BottomLeft: "╰",
|
||||||
|
BottomRight: "╯",
|
||||||
|
BottomSeparator: "┴",
|
||||||
|
EmptySeparator: text.RepeatAndTrim(" ", text.RuneWidthWithoutEscSequences("┼")),
|
||||||
|
Left: "│",
|
||||||
|
LeftSeparator: "├",
|
||||||
|
MiddleHorizontal: "─",
|
||||||
|
MiddleSeparator: "┼",
|
||||||
|
MiddleVertical: "│",
|
||||||
|
PaddingLeft: " ",
|
||||||
|
PaddingRight: " ",
|
||||||
|
PageSeparator: "\n",
|
||||||
|
Right: "│",
|
||||||
|
RightSeparator: "┤",
|
||||||
|
TopLeft: "╭",
|
||||||
|
TopRight: "╮",
|
||||||
|
TopSeparator: "┬",
|
||||||
|
UnfinishedRow: " ≈",
|
||||||
|
}
|
||||||
|
|
||||||
|
// styleBoxTest defines a Boxed-Table like below:
|
||||||
|
// (-----^------------^-----------^--------^-----------------------------)
|
||||||
|
// [< #>|<FIRST NAME>|<LAST NAME>|<SALARY>|< >]
|
||||||
|
// {-----+------------+-----------+--------+-----------------------------}
|
||||||
|
// [< 1>|<Arya >|<Stark >|< 3000>|< >]
|
||||||
|
// [< 20>|<Jon >|<Snow >|< 2000>|<You know nothing, Jon Snow!>]
|
||||||
|
// [<300>|<Tyrion >|<Lannister>|< 5000>|< >]
|
||||||
|
// {-----+------------+-----------+--------+-----------------------------}
|
||||||
|
// [< >|< >|<TOTAL >|< 10000>|< >]
|
||||||
|
// \-----v------------v-----------v--------v-----------------------------/
|
||||||
|
styleBoxTest = BoxStyle{
|
||||||
|
BottomLeft: "\\",
|
||||||
|
BottomRight: "/",
|
||||||
|
BottomSeparator: "v",
|
||||||
|
EmptySeparator: text.RepeatAndTrim(" ", text.RuneWidthWithoutEscSequences("+")),
|
||||||
|
Left: "[",
|
||||||
|
LeftSeparator: "{",
|
||||||
|
MiddleHorizontal: "--",
|
||||||
|
MiddleSeparator: "+",
|
||||||
|
MiddleVertical: "|",
|
||||||
|
PaddingLeft: "<",
|
||||||
|
PaddingRight: ">",
|
||||||
|
PageSeparator: "\n",
|
||||||
|
Right: "]",
|
||||||
|
RightSeparator: "}",
|
||||||
|
TopLeft: "(",
|
||||||
|
TopRight: ")",
|
||||||
|
TopSeparator: "^",
|
||||||
|
UnfinishedRow: " ~~~",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// ColorOptions defines the ANSI colors to use for parts of the Table.
|
||||||
|
type ColorOptions struct {
|
||||||
|
Border text.Colors // borders (if nil, uses one of the below)
|
||||||
|
Footer text.Colors // footer row(s) colors
|
||||||
|
Header text.Colors // header row(s) colors
|
||||||
|
IndexColumn text.Colors // index-column colors (row #, etc.)
|
||||||
|
Row text.Colors // regular row(s) colors
|
||||||
|
RowAlternate text.Colors // regular row(s) colors for the even-numbered rows
|
||||||
|
Separator text.Colors // separators (if nil, uses one of the above)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ColorOptionsDefault defines sensible ANSI color options - basically NONE.
|
||||||
|
ColorOptionsDefault = ColorOptions{}
|
||||||
|
|
||||||
|
// ColorOptionsBright renders dark text on bright background.
|
||||||
|
ColorOptionsBright = ColorOptionsBlackOnCyanWhite
|
||||||
|
|
||||||
|
// ColorOptionsDark renders bright text on dark background.
|
||||||
|
ColorOptionsDark = ColorOptionsCyanWhiteOnBlack
|
||||||
|
|
||||||
|
// ColorOptionsBlackOnBlueWhite renders Black text on Blue/White background.
|
||||||
|
ColorOptionsBlackOnBlueWhite = ColorOptions{
|
||||||
|
Footer: text.Colors{text.BgBlue, text.FgBlack},
|
||||||
|
Header: text.Colors{text.BgHiBlue, text.FgBlack},
|
||||||
|
IndexColumn: text.Colors{text.BgHiBlue, text.FgBlack},
|
||||||
|
Row: text.Colors{text.BgHiWhite, text.FgBlack},
|
||||||
|
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorOptionsBlackOnCyanWhite renders Black text on Cyan/White background.
|
||||||
|
ColorOptionsBlackOnCyanWhite = ColorOptions{
|
||||||
|
Footer: text.Colors{text.BgCyan, text.FgBlack},
|
||||||
|
Header: text.Colors{text.BgHiCyan, text.FgBlack},
|
||||||
|
IndexColumn: text.Colors{text.BgHiCyan, text.FgBlack},
|
||||||
|
Row: text.Colors{text.BgHiWhite, text.FgBlack},
|
||||||
|
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorOptionsBlackOnGreenWhite renders Black text on Green/White
|
||||||
|
// background.
|
||||||
|
ColorOptionsBlackOnGreenWhite = ColorOptions{
|
||||||
|
Footer: text.Colors{text.BgGreen, text.FgBlack},
|
||||||
|
Header: text.Colors{text.BgHiGreen, text.FgBlack},
|
||||||
|
IndexColumn: text.Colors{text.BgHiGreen, text.FgBlack},
|
||||||
|
Row: text.Colors{text.BgHiWhite, text.FgBlack},
|
||||||
|
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorOptionsBlackOnMagentaWhite renders Black text on Magenta/White
|
||||||
|
// background.
|
||||||
|
ColorOptionsBlackOnMagentaWhite = ColorOptions{
|
||||||
|
Footer: text.Colors{text.BgMagenta, text.FgBlack},
|
||||||
|
Header: text.Colors{text.BgHiMagenta, text.FgBlack},
|
||||||
|
IndexColumn: text.Colors{text.BgHiMagenta, text.FgBlack},
|
||||||
|
Row: text.Colors{text.BgHiWhite, text.FgBlack},
|
||||||
|
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorOptionsBlackOnRedWhite renders Black text on Red/White background.
|
||||||
|
ColorOptionsBlackOnRedWhite = ColorOptions{
|
||||||
|
Footer: text.Colors{text.BgRed, text.FgBlack},
|
||||||
|
Header: text.Colors{text.BgHiRed, text.FgBlack},
|
||||||
|
IndexColumn: text.Colors{text.BgHiRed, text.FgBlack},
|
||||||
|
Row: text.Colors{text.BgHiWhite, text.FgBlack},
|
||||||
|
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorOptionsBlackOnYellowWhite renders Black text on Yellow/White
|
||||||
|
// background.
|
||||||
|
ColorOptionsBlackOnYellowWhite = ColorOptions{
|
||||||
|
Footer: text.Colors{text.BgYellow, text.FgBlack},
|
||||||
|
Header: text.Colors{text.BgHiYellow, text.FgBlack},
|
||||||
|
IndexColumn: text.Colors{text.BgHiYellow, text.FgBlack},
|
||||||
|
Row: text.Colors{text.BgHiWhite, text.FgBlack},
|
||||||
|
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorOptionsBlueWhiteOnBlack renders Blue/White text on Black background.
|
||||||
|
ColorOptionsBlueWhiteOnBlack = ColorOptions{
|
||||||
|
Footer: text.Colors{text.FgBlue, text.BgHiBlack},
|
||||||
|
Header: text.Colors{text.FgHiBlue, text.BgHiBlack},
|
||||||
|
IndexColumn: text.Colors{text.FgHiBlue, text.BgHiBlack},
|
||||||
|
Row: text.Colors{text.FgHiWhite, text.BgBlack},
|
||||||
|
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorOptionsCyanWhiteOnBlack renders Cyan/White text on Black background.
|
||||||
|
ColorOptionsCyanWhiteOnBlack = ColorOptions{
|
||||||
|
Footer: text.Colors{text.FgCyan, text.BgHiBlack},
|
||||||
|
Header: text.Colors{text.FgHiCyan, text.BgHiBlack},
|
||||||
|
IndexColumn: text.Colors{text.FgHiCyan, text.BgHiBlack},
|
||||||
|
Row: text.Colors{text.FgHiWhite, text.BgBlack},
|
||||||
|
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorOptionsGreenWhiteOnBlack renders Green/White text on Black
|
||||||
|
// background.
|
||||||
|
ColorOptionsGreenWhiteOnBlack = ColorOptions{
|
||||||
|
Footer: text.Colors{text.FgGreen, text.BgHiBlack},
|
||||||
|
Header: text.Colors{text.FgHiGreen, text.BgHiBlack},
|
||||||
|
IndexColumn: text.Colors{text.FgHiGreen, text.BgHiBlack},
|
||||||
|
Row: text.Colors{text.FgHiWhite, text.BgBlack},
|
||||||
|
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorOptionsMagentaWhiteOnBlack renders Magenta/White text on Black
|
||||||
|
// background.
|
||||||
|
ColorOptionsMagentaWhiteOnBlack = ColorOptions{
|
||||||
|
Footer: text.Colors{text.FgMagenta, text.BgHiBlack},
|
||||||
|
Header: text.Colors{text.FgHiMagenta, text.BgHiBlack},
|
||||||
|
IndexColumn: text.Colors{text.FgHiMagenta, text.BgHiBlack},
|
||||||
|
Row: text.Colors{text.FgHiWhite, text.BgBlack},
|
||||||
|
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorOptionsRedWhiteOnBlack renders Red/White text on Black background.
|
||||||
|
ColorOptionsRedWhiteOnBlack = ColorOptions{
|
||||||
|
Footer: text.Colors{text.FgRed, text.BgHiBlack},
|
||||||
|
Header: text.Colors{text.FgHiRed, text.BgHiBlack},
|
||||||
|
IndexColumn: text.Colors{text.FgHiRed, text.BgHiBlack},
|
||||||
|
Row: text.Colors{text.FgHiWhite, text.BgBlack},
|
||||||
|
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorOptionsYellowWhiteOnBlack renders Yellow/White text on Black
|
||||||
|
// background.
|
||||||
|
ColorOptionsYellowWhiteOnBlack = ColorOptions{
|
||||||
|
Footer: text.Colors{text.FgYellow, text.BgHiBlack},
|
||||||
|
Header: text.Colors{text.FgHiYellow, text.BgHiBlack},
|
||||||
|
IndexColumn: text.Colors{text.FgHiYellow, text.BgHiBlack},
|
||||||
|
Row: text.Colors{text.FgHiWhite, text.BgBlack},
|
||||||
|
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// FormatOptions defines the text-formatting to perform on parts of the Table.
|
||||||
|
type FormatOptions struct {
|
||||||
|
Direction text.Direction // (forced) BiDi direction for each Column
|
||||||
|
Footer text.Format // footer row(s) text format
|
||||||
|
Header text.Format // header row(s) text format
|
||||||
|
Row text.Format // (data) row(s) text format
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// FormatOptionsDefault defines sensible formatting options.
|
||||||
|
FormatOptionsDefault = FormatOptions{
|
||||||
|
Footer: text.FormatUpper,
|
||||||
|
Header: text.FormatUpper,
|
||||||
|
Row: text.FormatDefault,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTMLOptions defines the global options to control HTML rendering.
|
||||||
|
type HTMLOptions struct {
|
||||||
|
CSSClass string // CSS class to set on the overall <table> tag
|
||||||
|
EmptyColumn string // string to replace "" columns with (entire content being "")
|
||||||
|
EscapeText bool // escape text into HTML-safe content?
|
||||||
|
Newline string // string to replace "\n" characters with
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DefaultHTMLOptions defines sensible HTML rendering defaults.
|
||||||
|
DefaultHTMLOptions = HTMLOptions{
|
||||||
|
CSSClass: DefaultHTMLCSSClass,
|
||||||
|
EmptyColumn: " ",
|
||||||
|
EscapeText: true,
|
||||||
|
Newline: "<br/>",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Options defines the global options that determine how the Table is
|
||||||
|
// rendered.
|
||||||
|
type Options struct {
|
||||||
|
// DoNotColorBordersAndSeparators disables coloring all the borders and row
|
||||||
|
// or column separators.
|
||||||
|
DoNotColorBordersAndSeparators bool
|
||||||
|
|
||||||
|
// DrawBorder enables or disables drawing the border around the Table.
|
||||||
|
// Example of a table where it is disabled:
|
||||||
|
// # │ FIRST NAME │ LAST NAME │ SALARY │
|
||||||
|
// ─────┼────────────┼───────────┼────────┼─────────────────────────────
|
||||||
|
// 1 │ Arya │ Stark │ 3000 │
|
||||||
|
// 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow!
|
||||||
|
// 300 │ Tyrion │ Lannister │ 5000 │
|
||||||
|
// ─────┼────────────┼───────────┼────────┼─────────────────────────────
|
||||||
|
// │ │ TOTAL │ 10000 │
|
||||||
|
DrawBorder bool
|
||||||
|
|
||||||
|
// SeparateColumns enables or disable drawing border between columns.
|
||||||
|
// Example of a table where it is disabled:
|
||||||
|
// ┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
// │ # FIRST NAME LAST NAME SALARY │
|
||||||
|
// ├─────────────────────────────────────────────────────────────────┤
|
||||||
|
// │ 1 Arya Stark 3000 │
|
||||||
|
// │ 20 Jon Snow 2000 You know nothing, Jon Snow! │
|
||||||
|
// │ 300 Tyrion Lannister 5000 │
|
||||||
|
// │ TOTAL 10000 │
|
||||||
|
// └─────────────────────────────────────────────────────────────────┘
|
||||||
|
SeparateColumns bool
|
||||||
|
|
||||||
|
// SeparateFooter enables or disable drawing border between the footer and
|
||||||
|
// the rows. Example of a table where it is disabled:
|
||||||
|
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
|
||||||
|
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||||
|
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||||
|
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||||
|
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||||
|
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||||
|
// │ │ │ TOTAL │ 10000 │ │
|
||||||
|
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
|
||||||
|
SeparateFooter bool
|
||||||
|
|
||||||
|
// SeparateHeader enables or disable drawing border between the header and
|
||||||
|
// the rows. Example of a table where it is disabled:
|
||||||
|
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
|
||||||
|
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||||
|
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||||
|
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||||
|
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||||
|
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||||
|
// │ │ │ TOTAL │ 10000 │ │
|
||||||
|
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
|
||||||
|
SeparateHeader bool
|
||||||
|
|
||||||
|
// SeparateRows enables or disables drawing separators between each row.
|
||||||
|
// Example of a table where it is enabled:
|
||||||
|
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
|
||||||
|
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||||
|
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||||
|
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||||
|
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||||
|
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||||
|
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||||
|
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||||
|
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||||
|
// │ │ │ TOTAL │ 10000 │ │
|
||||||
|
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
|
||||||
|
SeparateRows bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// OptionsDefault defines sensible global options.
|
||||||
|
OptionsDefault = Options{
|
||||||
|
DrawBorder: true,
|
||||||
|
SeparateColumns: true,
|
||||||
|
SeparateFooter: true,
|
||||||
|
SeparateHeader: true,
|
||||||
|
SeparateRows: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionsNoBorders sets up a table without any borders.
|
||||||
|
OptionsNoBorders = Options{
|
||||||
|
DrawBorder: false,
|
||||||
|
SeparateColumns: true,
|
||||||
|
SeparateFooter: true,
|
||||||
|
SeparateHeader: true,
|
||||||
|
SeparateRows: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionsNoBordersAndSeparators sets up a table without any borders or
|
||||||
|
// separators.
|
||||||
|
OptionsNoBordersAndSeparators = Options{
|
||||||
|
DrawBorder: false,
|
||||||
|
SeparateColumns: false,
|
||||||
|
SeparateFooter: false,
|
||||||
|
SeparateHeader: false,
|
||||||
|
SeparateRows: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// TitleOptions defines the way the title text is to be rendered.
|
||||||
|
type TitleOptions struct {
|
||||||
|
Align text.Align
|
||||||
|
Colors text.Colors
|
||||||
|
Format text.Format
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// TitleOptionsDefault defines sensible title options - basically NONE.
|
||||||
|
TitleOptionsDefault = TitleOptions{}
|
||||||
|
|
||||||
|
// TitleOptionsBright renders Bright Bold text on Dark background.
|
||||||
|
TitleOptionsBright = TitleOptionsBlackOnCyan
|
||||||
|
|
||||||
|
// TitleOptionsDark renders Dark Bold text on Bright background.
|
||||||
|
TitleOptionsDark = TitleOptionsCyanOnBlack
|
||||||
|
|
||||||
|
// TitleOptionsBlackOnBlue renders Black text on Blue background.
|
||||||
|
TitleOptionsBlackOnBlue = TitleOptions{
|
||||||
|
Colors: append(ColorOptionsBlackOnBlueWhite.Header, text.Bold),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TitleOptionsBlackOnCyan renders Black Bold text on Cyan background.
|
||||||
|
TitleOptionsBlackOnCyan = TitleOptions{
|
||||||
|
Colors: append(ColorOptionsBlackOnCyanWhite.Header, text.Bold),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TitleOptionsBlackOnGreen renders Black Bold text onGreen background.
|
||||||
|
TitleOptionsBlackOnGreen = TitleOptions{
|
||||||
|
Colors: append(ColorOptionsBlackOnGreenWhite.Header, text.Bold),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TitleOptionsBlackOnMagenta renders Black Bold text on Magenta background.
|
||||||
|
TitleOptionsBlackOnMagenta = TitleOptions{
|
||||||
|
Colors: append(ColorOptionsBlackOnMagentaWhite.Header, text.Bold),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TitleOptionsBlackOnRed renders Black Bold text on Red background.
|
||||||
|
TitleOptionsBlackOnRed = TitleOptions{
|
||||||
|
Colors: append(ColorOptionsBlackOnRedWhite.Header, text.Bold),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TitleOptionsBlackOnYellow renders Black Bold text on Yellow background.
|
||||||
|
TitleOptionsBlackOnYellow = TitleOptions{
|
||||||
|
Colors: append(ColorOptionsBlackOnYellowWhite.Header, text.Bold),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TitleOptionsBlueOnBlack renders Blue Bold text on Black background.
|
||||||
|
TitleOptionsBlueOnBlack = TitleOptions{
|
||||||
|
Colors: append(ColorOptionsBlueWhiteOnBlack.Header, text.Bold),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TitleOptionsCyanOnBlack renders Cyan Bold text on Black background.
|
||||||
|
TitleOptionsCyanOnBlack = TitleOptions{
|
||||||
|
Colors: append(ColorOptionsCyanWhiteOnBlack.Header, text.Bold),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TitleOptionsGreenOnBlack renders Green Bold text on Black background.
|
||||||
|
TitleOptionsGreenOnBlack = TitleOptions{
|
||||||
|
Colors: append(ColorOptionsGreenWhiteOnBlack.Header, text.Bold),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TitleOptionsMagentaOnBlack renders Magenta Bold text on Black background.
|
||||||
|
TitleOptionsMagentaOnBlack = TitleOptions{
|
||||||
|
Colors: append(ColorOptionsMagentaWhiteOnBlack.Header, text.Bold),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TitleOptionsRedOnBlack renders Red Bold text on Black background.
|
||||||
|
TitleOptionsRedOnBlack = TitleOptions{
|
||||||
|
Colors: append(ColorOptionsRedWhiteOnBlack.Header, text.Bold),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TitleOptionsYellowOnBlack renders Yellow Bold text on Black background.
|
||||||
|
TitleOptionsYellowOnBlack = TitleOptions{
|
||||||
|
Colors: append(ColorOptionsYellowWhiteOnBlack.Header, text.Bold),
|
||||||
|
}
|
||||||
|
)
|
784
helper/table/table.go
Normal file
784
helper/table/table.go
Normal file
@ -0,0 +1,784 @@
|
|||||||
|
package table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.hexq.cn/tiglog/golib/helper/text"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Row defines a single row in the Table.
|
||||||
|
type Row []interface{}
|
||||||
|
|
||||||
|
func (r Row) findColumnNumber(colName string) int {
|
||||||
|
for colIdx, col := range r {
|
||||||
|
if fmt.Sprint(col) == colName {
|
||||||
|
return colIdx + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// RowPainter is a custom function that takes a Row as input and returns the
|
||||||
|
// text.Colors{} to use on the entire row
|
||||||
|
type RowPainter func(row Row) text.Colors
|
||||||
|
|
||||||
|
// rowStr defines a single row in the Table comprised of just string objects.
|
||||||
|
type rowStr []string
|
||||||
|
|
||||||
|
// areEqual returns true if the contents of the 2 given columns are the same
|
||||||
|
func (row rowStr) areEqual(colIdx1 int, colIdx2 int) bool {
|
||||||
|
return colIdx1 >= 0 && colIdx2 < len(row) && row[colIdx1] == row[colIdx2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table helps print a 2-dimensional array in a human readable pretty-table.
|
||||||
|
type Table struct {
|
||||||
|
// allowedRowLength is the max allowed length for a row (or line of output)
|
||||||
|
allowedRowLength int
|
||||||
|
// enable automatic indexing of the rows and columns like a spreadsheet?
|
||||||
|
autoIndex bool
|
||||||
|
// autoIndexVIndexMaxLength denotes the length in chars for the last rownum
|
||||||
|
autoIndexVIndexMaxLength int
|
||||||
|
// caption stores the text to be rendered just below the table; and doesn't
|
||||||
|
// get used when rendered as a CSV
|
||||||
|
caption string
|
||||||
|
// columnIsNonNumeric stores if a column contains non-numbers in all rows
|
||||||
|
columnIsNonNumeric []bool
|
||||||
|
// columnConfigs stores the custom-configuration for 1 or more columns
|
||||||
|
columnConfigs []ColumnConfig
|
||||||
|
// columnConfigMap stores the custom-configuration by column
|
||||||
|
// number and is generated before rendering
|
||||||
|
columnConfigMap map[int]ColumnConfig
|
||||||
|
// htmlCSSClass stores the HTML CSS Class to use on the <table> node
|
||||||
|
htmlCSSClass string
|
||||||
|
// indexColumn stores the number of the column considered as the "index"
|
||||||
|
indexColumn int
|
||||||
|
// maxColumnLengths stores the length of the longest line in each column
|
||||||
|
maxColumnLengths []int
|
||||||
|
// maxRowLength stores the length of the longest row
|
||||||
|
maxRowLength int
|
||||||
|
// numColumns stores the (max.) number of columns seen
|
||||||
|
numColumns int
|
||||||
|
// numLinesRendered keeps track of the number of lines rendered and helps in
|
||||||
|
// paginating long tables
|
||||||
|
numLinesRendered int
|
||||||
|
// outputMirror stores an io.Writer where the "Render" functions would write
|
||||||
|
outputMirror io.Writer
|
||||||
|
// pageSize stores the maximum lines to render before rendering the header
|
||||||
|
// again (to denote a page break) - useful when you are dealing with really
|
||||||
|
// long tables
|
||||||
|
pageSize int
|
||||||
|
// rows stores the rows that make up the body (in string form)
|
||||||
|
rows []rowStr
|
||||||
|
// rowsColors stores the text.Colors over-rides for each row as defined by
|
||||||
|
// rowPainter
|
||||||
|
rowsColors []text.Colors
|
||||||
|
// rowsConfigs stores RowConfig for each row
|
||||||
|
rowsConfigMap map[int]RowConfig
|
||||||
|
// rowsRaw stores the rows that make up the body
|
||||||
|
rowsRaw []Row
|
||||||
|
// rowsFooter stores the rows that make up the footer (in string form)
|
||||||
|
rowsFooter []rowStr
|
||||||
|
// rowsFooterConfigs stores RowConfig for each footer row
|
||||||
|
rowsFooterConfigMap map[int]RowConfig
|
||||||
|
// rowsFooterRaw stores the rows that make up the footer
|
||||||
|
rowsFooterRaw []Row
|
||||||
|
// rowsHeader stores the rows that make up the header (in string form)
|
||||||
|
rowsHeader []rowStr
|
||||||
|
// rowsHeaderConfigs stores RowConfig for each header row
|
||||||
|
rowsHeaderConfigMap map[int]RowConfig
|
||||||
|
// rowsHeaderRaw stores the rows that make up the header
|
||||||
|
rowsHeaderRaw []Row
|
||||||
|
// rowPainter is a custom function that given a Row, returns the colors to
|
||||||
|
// use on the entire row
|
||||||
|
rowPainter RowPainter
|
||||||
|
// rowSeparator is a dummy row that contains the separator columns (dashes
|
||||||
|
// that make up the separator between header/body/footer
|
||||||
|
rowSeparator rowStr
|
||||||
|
// separators is used to keep track of all rowIndices after which a
|
||||||
|
// separator has to be rendered
|
||||||
|
separators map[int]bool
|
||||||
|
// sortBy stores a map of Column
|
||||||
|
sortBy []SortBy
|
||||||
|
// style contains all the strings used to draw the table, and more
|
||||||
|
style *Style
|
||||||
|
// suppressEmptyColumns hides columns which have no content on all regular
|
||||||
|
// rows
|
||||||
|
suppressEmptyColumns bool
|
||||||
|
// title contains the text to appear above the table
|
||||||
|
title string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendFooter appends the row to the List of footers to render.
|
||||||
|
//
|
||||||
|
// Only the first item in the "config" will be tagged against this row.
|
||||||
|
func (t *Table) AppendFooter(row Row, config ...RowConfig) {
|
||||||
|
t.rowsFooterRaw = append(t.rowsFooterRaw, row)
|
||||||
|
if len(config) > 0 {
|
||||||
|
if t.rowsFooterConfigMap == nil {
|
||||||
|
t.rowsFooterConfigMap = make(map[int]RowConfig)
|
||||||
|
}
|
||||||
|
t.rowsFooterConfigMap[len(t.rowsFooterRaw)-1] = config[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendHeader appends the row to the List of headers to render.
|
||||||
|
//
|
||||||
|
// Only the first item in the "config" will be tagged against this row.
|
||||||
|
func (t *Table) AppendHeader(row Row, config ...RowConfig) {
|
||||||
|
t.rowsHeaderRaw = append(t.rowsHeaderRaw, row)
|
||||||
|
if len(config) > 0 {
|
||||||
|
if t.rowsHeaderConfigMap == nil {
|
||||||
|
t.rowsHeaderConfigMap = make(map[int]RowConfig)
|
||||||
|
}
|
||||||
|
t.rowsHeaderConfigMap[len(t.rowsHeaderRaw)-1] = config[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendRow appends the row to the List of rows to render.
|
||||||
|
//
|
||||||
|
// Only the first item in the "config" will be tagged against this row.
|
||||||
|
func (t *Table) AppendRow(row Row, config ...RowConfig) {
|
||||||
|
t.rowsRaw = append(t.rowsRaw, row)
|
||||||
|
if len(config) > 0 {
|
||||||
|
if t.rowsConfigMap == nil {
|
||||||
|
t.rowsConfigMap = make(map[int]RowConfig)
|
||||||
|
}
|
||||||
|
t.rowsConfigMap[len(t.rowsRaw)-1] = config[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendRows appends the rows to the List of rows to render.
|
||||||
|
//
|
||||||
|
// Only the first item in the "config" will be tagged against all the rows.
|
||||||
|
func (t *Table) AppendRows(rows []Row, config ...RowConfig) {
|
||||||
|
for _, row := range rows {
|
||||||
|
t.AppendRow(row, config...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendSeparator helps render a separator row after the current last row. You
|
||||||
|
// could call this function over and over, but it will be a no-op unless you
|
||||||
|
// call AppendRow or AppendRows in between. Likewise, if the last thing you
|
||||||
|
// append is a separator, it will not be rendered in addition to the usual table
|
||||||
|
// separator.
|
||||||
|
//
|
||||||
|
// ******************************************************************************
|
||||||
|
// Please note the following caveats:
|
||||||
|
// 1. SetPageSize(): this may end up creating consecutive separator rows near
|
||||||
|
// the end of a page or at the beginning of a page
|
||||||
|
// 2. SortBy(): since SortBy could inherently alter the ordering of rows, the
|
||||||
|
// separators may not appear after the row it was originally intended to
|
||||||
|
// follow
|
||||||
|
//
|
||||||
|
// ******************************************************************************
|
||||||
|
func (t *Table) AppendSeparator() {
|
||||||
|
if t.separators == nil {
|
||||||
|
t.separators = make(map[int]bool)
|
||||||
|
}
|
||||||
|
if len(t.rowsRaw) > 0 {
|
||||||
|
t.separators[len(t.rowsRaw)-1] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Length returns the number of rows to be rendered.
|
||||||
|
func (t *Table) Length() int {
|
||||||
|
return len(t.rowsRaw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetFooters resets and clears all the Footer rows appended earlier.
|
||||||
|
func (t *Table) ResetFooters() {
|
||||||
|
t.rowsFooterRaw = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetHeaders resets and clears all the Header rows appended earlier.
|
||||||
|
func (t *Table) ResetHeaders() {
|
||||||
|
t.rowsHeaderRaw = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetRows resets and clears all the rows appended earlier.
|
||||||
|
func (t *Table) ResetRows() {
|
||||||
|
t.rowsRaw = nil
|
||||||
|
t.separators = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAllowedRowLength sets the maximum allowed length or a row (or line of
|
||||||
|
// output) when rendered as a table. Rows that are longer than this limit will
|
||||||
|
// be "snipped" to the length. Length has to be a positive value to take effect.
|
||||||
|
func (t *Table) SetAllowedRowLength(length int) {
|
||||||
|
t.allowedRowLength = length
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAutoIndex adds a generated header with columns such as "A", "B", "C", etc.
|
||||||
|
// and a leading column with the row number similar to what you'd see on any
|
||||||
|
// spreadsheet application. NOTE: Appending a Header will void this
|
||||||
|
// functionality.
|
||||||
|
func (t *Table) SetAutoIndex(autoIndex bool) {
|
||||||
|
t.autoIndex = autoIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCaption sets the text to be rendered just below the table. This will not
|
||||||
|
// show up when the Table is rendered as a CSV.
|
||||||
|
func (t *Table) SetCaption(format string, a ...interface{}) {
|
||||||
|
t.caption = fmt.Sprintf(format, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetColumnConfigs sets the configs for each Column.
|
||||||
|
func (t *Table) SetColumnConfigs(configs []ColumnConfig) {
|
||||||
|
t.columnConfigs = configs
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHTMLCSSClass sets the the HTML CSS Class to use on the <table> node
|
||||||
|
// when rendering the Table in HTML format.
|
||||||
|
//
|
||||||
|
// Deprecated: in favor of Style().HTML.CSSClass
|
||||||
|
func (t *Table) SetHTMLCSSClass(cssClass string) {
|
||||||
|
t.htmlCSSClass = cssClass
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetIndexColumn sets the given Column # as the column that has the row
|
||||||
|
// "Number". Valid values range from 1 to N. Note that this is not 0-indexed.
|
||||||
|
func (t *Table) SetIndexColumn(colNum int) {
|
||||||
|
t.indexColumn = colNum
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOutputMirror sets an io.Writer for all the Render functions to "Write" to
|
||||||
|
// in addition to returning a string.
|
||||||
|
func (t *Table) SetOutputMirror(mirror io.Writer) {
|
||||||
|
t.outputMirror = mirror
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPageSize sets the maximum number of lines to render before rendering the
|
||||||
|
// header rows again. This can be useful when dealing with tables containing a
|
||||||
|
// long list of rows that can span pages. Please note that the pagination logic
|
||||||
|
// will not consider Header/Footer lines for paging.
|
||||||
|
func (t *Table) SetPageSize(numLines int) {
|
||||||
|
t.pageSize = numLines
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRowPainter sets the RowPainter function which determines the colors to use
|
||||||
|
// on a row. Before rendering, this function is invoked on all rows and the
|
||||||
|
// color of each row is determined. This color takes precedence over other ways
|
||||||
|
// to set color (ColumnConfig.Color*, SetColor*()).
|
||||||
|
func (t *Table) SetRowPainter(painter RowPainter) {
|
||||||
|
t.rowPainter = painter
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStyle overrides the DefaultStyle with the provided one.
|
||||||
|
func (t *Table) SetStyle(style Style) {
|
||||||
|
t.style = &style
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTitle sets the title text to be rendered above the table.
|
||||||
|
func (t *Table) SetTitle(format string, a ...interface{}) {
|
||||||
|
t.title = fmt.Sprintf(format, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortBy sets the rules for sorting the Rows in the order specified. i.e., the
|
||||||
|
// first SortBy instruction takes precedence over the second and so on. Any
|
||||||
|
// duplicate instructions on the same column will be discarded while sorting.
|
||||||
|
func (t *Table) SortBy(sortBy []SortBy) {
|
||||||
|
t.sortBy = sortBy
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style returns the current style.
|
||||||
|
func (t *Table) Style() *Style {
|
||||||
|
if t.style == nil {
|
||||||
|
tempStyle := StyleDefault
|
||||||
|
t.style = &tempStyle
|
||||||
|
}
|
||||||
|
return t.style
|
||||||
|
}
|
||||||
|
|
||||||
|
// SuppressEmptyColumns hides columns when the column is empty in ALL the
|
||||||
|
// regular rows.
|
||||||
|
func (t *Table) SuppressEmptyColumns() {
|
||||||
|
t.suppressEmptyColumns = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) getAlign(colIdx int, hint renderHint) text.Align {
|
||||||
|
align := text.AlignDefault
|
||||||
|
if cfg, ok := t.columnConfigMap[colIdx]; ok {
|
||||||
|
if hint.isHeaderRow {
|
||||||
|
align = cfg.AlignHeader
|
||||||
|
} else if hint.isFooterRow {
|
||||||
|
align = cfg.AlignFooter
|
||||||
|
} else {
|
||||||
|
align = cfg.Align
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if align == text.AlignDefault {
|
||||||
|
if !t.columnIsNonNumeric[colIdx] {
|
||||||
|
align = text.AlignRight
|
||||||
|
} else if hint.isAutoIndexRow {
|
||||||
|
align = text.AlignCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return align
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) getAutoIndexColumnIDs() rowStr {
|
||||||
|
row := make(rowStr, t.numColumns)
|
||||||
|
for colIdx := range row {
|
||||||
|
row[colIdx] = AutoIndexColumnID(colIdx)
|
||||||
|
}
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) getBorderColors(hint renderHint) text.Colors {
|
||||||
|
if t.style.Options.DoNotColorBordersAndSeparators {
|
||||||
|
return nil
|
||||||
|
} else if t.style.Color.Border != nil {
|
||||||
|
return t.style.Color.Border
|
||||||
|
} else if hint.isTitleRow {
|
||||||
|
return t.style.Title.Colors
|
||||||
|
} else if hint.isHeaderRow {
|
||||||
|
return t.style.Color.Header
|
||||||
|
} else if hint.isFooterRow {
|
||||||
|
return t.style.Color.Footer
|
||||||
|
} else if t.autoIndex {
|
||||||
|
return t.style.Color.IndexColumn
|
||||||
|
} else if hint.rowNumber%2 == 0 && t.style.Color.RowAlternate != nil {
|
||||||
|
return t.style.Color.RowAlternate
|
||||||
|
}
|
||||||
|
return t.style.Color.Row
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) getBorderLeft(hint renderHint) string {
|
||||||
|
border := t.style.Box.Left
|
||||||
|
if hint.isBorderTop {
|
||||||
|
if t.title != "" {
|
||||||
|
border = t.style.Box.LeftSeparator
|
||||||
|
} else {
|
||||||
|
border = t.style.Box.TopLeft
|
||||||
|
}
|
||||||
|
} else if hint.isBorderBottom {
|
||||||
|
border = t.style.Box.BottomLeft
|
||||||
|
} else if hint.isSeparatorRow {
|
||||||
|
if t.autoIndex && hint.isHeaderOrFooterSeparator() {
|
||||||
|
border = t.style.Box.Left
|
||||||
|
} else if !t.autoIndex && t.shouldMergeCellsVertically(0, hint) {
|
||||||
|
border = t.style.Box.Left
|
||||||
|
} else {
|
||||||
|
border = t.style.Box.LeftSeparator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return border
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) getBorderRight(hint renderHint) string {
|
||||||
|
border := t.style.Box.Right
|
||||||
|
if hint.isBorderTop {
|
||||||
|
if t.title != "" {
|
||||||
|
border = t.style.Box.RightSeparator
|
||||||
|
} else {
|
||||||
|
border = t.style.Box.TopRight
|
||||||
|
}
|
||||||
|
} else if hint.isBorderBottom {
|
||||||
|
border = t.style.Box.BottomRight
|
||||||
|
} else if hint.isSeparatorRow {
|
||||||
|
if t.shouldMergeCellsVertically(t.numColumns-1, hint) {
|
||||||
|
border = t.style.Box.Right
|
||||||
|
} else {
|
||||||
|
border = t.style.Box.RightSeparator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return border
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) getColumnColors(colIdx int, hint renderHint) text.Colors {
|
||||||
|
if hint.isBorderOrSeparator() {
|
||||||
|
if colors := t.getColumnColorsForBorderOrSeparator(colIdx, hint); colors != nil {
|
||||||
|
return colors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t.rowPainter != nil && hint.isRegularNonSeparatorRow() && !t.isIndexColumn(colIdx, hint) {
|
||||||
|
if colors := t.rowsColors[hint.rowNumber-1]; colors != nil {
|
||||||
|
return colors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cfg, ok := t.columnConfigMap[colIdx]; ok {
|
||||||
|
if hint.isSeparatorRow {
|
||||||
|
return nil
|
||||||
|
} else if hint.isHeaderRow {
|
||||||
|
return cfg.ColorsHeader
|
||||||
|
} else if hint.isFooterRow {
|
||||||
|
return cfg.ColorsFooter
|
||||||
|
}
|
||||||
|
return cfg.Colors
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) getColumnColorsForBorderOrSeparator(colIdx int, hint renderHint) text.Colors {
|
||||||
|
if t.style.Options.DoNotColorBordersAndSeparators {
|
||||||
|
return text.Colors{} // not nil to force caller to paint with no colors
|
||||||
|
}
|
||||||
|
if (hint.isBorderBottom || hint.isBorderTop) && t.style.Color.Border != nil {
|
||||||
|
return t.style.Color.Border
|
||||||
|
}
|
||||||
|
if hint.isSeparatorRow && t.style.Color.Separator != nil {
|
||||||
|
return t.style.Color.Separator
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) getColumnSeparator(row rowStr, colIdx int, hint renderHint) string {
|
||||||
|
separator := t.style.Box.MiddleVertical
|
||||||
|
if hint.isSeparatorRow {
|
||||||
|
if hint.isBorderTop {
|
||||||
|
if t.shouldMergeCellsHorizontallyBelow(row, colIdx, hint) {
|
||||||
|
separator = t.style.Box.MiddleHorizontal
|
||||||
|
} else {
|
||||||
|
separator = t.style.Box.TopSeparator
|
||||||
|
}
|
||||||
|
} else if hint.isBorderBottom {
|
||||||
|
if t.shouldMergeCellsHorizontallyAbove(row, colIdx, hint) {
|
||||||
|
separator = t.style.Box.MiddleHorizontal
|
||||||
|
} else {
|
||||||
|
separator = t.style.Box.BottomSeparator
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
separator = t.getColumnSeparatorNonBorder(
|
||||||
|
t.shouldMergeCellsHorizontallyAbove(row, colIdx, hint),
|
||||||
|
t.shouldMergeCellsHorizontallyBelow(row, colIdx, hint),
|
||||||
|
colIdx,
|
||||||
|
hint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return separator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) getColumnSeparatorNonBorder(mergeCellsAbove bool, mergeCellsBelow bool, colIdx int, hint renderHint) string {
|
||||||
|
mergeNextCol := t.shouldMergeCellsVertically(colIdx, hint)
|
||||||
|
if hint.isAutoIndexColumn {
|
||||||
|
return t.getColumnSeparatorNonBorderAutoIndex(mergeNextCol, hint)
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeCurrCol := t.shouldMergeCellsVertically(colIdx-1, hint)
|
||||||
|
return t.getColumnSeparatorNonBorderNonAutoIndex(mergeCellsAbove, mergeCellsBelow, mergeCurrCol, mergeNextCol)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) getColumnSeparatorNonBorderAutoIndex(mergeNextCol bool, hint renderHint) string {
|
||||||
|
if hint.isHeaderOrFooterSeparator() {
|
||||||
|
if mergeNextCol {
|
||||||
|
return t.style.Box.MiddleVertical
|
||||||
|
}
|
||||||
|
return t.style.Box.LeftSeparator
|
||||||
|
} else if mergeNextCol {
|
||||||
|
return t.style.Box.RightSeparator
|
||||||
|
}
|
||||||
|
return t.style.Box.MiddleSeparator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) getColumnSeparatorNonBorderNonAutoIndex(mergeCellsAbove bool, mergeCellsBelow bool, mergeCurrCol bool, mergeNextCol bool) string {
|
||||||
|
if mergeCellsAbove && mergeCellsBelow && mergeCurrCol && mergeNextCol {
|
||||||
|
return t.style.Box.EmptySeparator
|
||||||
|
} else if mergeCellsAbove && mergeCellsBelow {
|
||||||
|
return t.style.Box.MiddleHorizontal
|
||||||
|
} else if mergeCellsAbove {
|
||||||
|
return t.style.Box.TopSeparator
|
||||||
|
} else if mergeCellsBelow {
|
||||||
|
return t.style.Box.BottomSeparator
|
||||||
|
} else if mergeCurrCol && mergeNextCol {
|
||||||
|
return t.style.Box.MiddleVertical
|
||||||
|
} else if mergeCurrCol {
|
||||||
|
return t.style.Box.LeftSeparator
|
||||||
|
} else if mergeNextCol {
|
||||||
|
return t.style.Box.RightSeparator
|
||||||
|
}
|
||||||
|
return t.style.Box.MiddleSeparator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) getColumnTransformer(colIdx int, hint renderHint) text.Transformer {
|
||||||
|
var transformer text.Transformer
|
||||||
|
if cfg, ok := t.columnConfigMap[colIdx]; ok {
|
||||||
|
if hint.isHeaderRow {
|
||||||
|
transformer = cfg.TransformerHeader
|
||||||
|
} else if hint.isFooterRow {
|
||||||
|
transformer = cfg.TransformerFooter
|
||||||
|
} else {
|
||||||
|
transformer = cfg.Transformer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return transformer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) getColumnWidthMax(colIdx int) int {
|
||||||
|
if cfg, ok := t.columnConfigMap[colIdx]; ok {
|
||||||
|
return cfg.WidthMax
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) getColumnWidthMin(colIdx int) int {
|
||||||
|
if cfg, ok := t.columnConfigMap[colIdx]; ok {
|
||||||
|
return cfg.WidthMin
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) getFormat(hint renderHint) text.Format {
|
||||||
|
if hint.isSeparatorRow {
|
||||||
|
return text.FormatDefault
|
||||||
|
} else if hint.isHeaderRow {
|
||||||
|
return t.style.Format.Header
|
||||||
|
} else if hint.isFooterRow {
|
||||||
|
return t.style.Format.Footer
|
||||||
|
}
|
||||||
|
return t.style.Format.Row
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) getMaxColumnLengthForMerging(colIdx int) int {
|
||||||
|
maxColumnLength := t.maxColumnLengths[colIdx]
|
||||||
|
maxColumnLength += text.RuneWidthWithoutEscSequences(t.style.Box.PaddingRight + t.style.Box.PaddingLeft)
|
||||||
|
if t.style.Options.SeparateColumns {
|
||||||
|
maxColumnLength += text.RuneWidthWithoutEscSequences(t.style.Box.EmptySeparator)
|
||||||
|
}
|
||||||
|
return maxColumnLength
|
||||||
|
}
|
||||||
|
|
||||||
|
// getMergedColumnIndices returns a map of colIdx values to all the other colIdx
|
||||||
|
// values (that are being merged) and their lengths.
|
||||||
|
func (t *Table) getMergedColumnIndices(row rowStr, hint renderHint) mergedColumnIndices {
|
||||||
|
if !t.getRowConfig(hint).AutoMerge {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mci := make(mergedColumnIndices)
|
||||||
|
for colIdx := 0; colIdx < t.numColumns-1; colIdx++ {
|
||||||
|
// look backward
|
||||||
|
for otherColIdx := colIdx - 1; colIdx >= 0 && otherColIdx >= 0; otherColIdx-- {
|
||||||
|
if row[colIdx] != row[otherColIdx] {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
mci.safeAppend(colIdx, otherColIdx)
|
||||||
|
}
|
||||||
|
// look forward
|
||||||
|
for otherColIdx := colIdx + 1; colIdx < len(row) && otherColIdx < len(row); otherColIdx++ {
|
||||||
|
if row[colIdx] != row[otherColIdx] {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
mci.safeAppend(colIdx, otherColIdx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mci
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) getRow(rowIdx int, hint renderHint) rowStr {
|
||||||
|
switch {
|
||||||
|
case hint.isHeaderRow:
|
||||||
|
if rowIdx >= 0 && rowIdx < len(t.rowsHeader) {
|
||||||
|
return t.rowsHeader[rowIdx]
|
||||||
|
}
|
||||||
|
case hint.isFooterRow:
|
||||||
|
if rowIdx >= 0 && rowIdx < len(t.rowsFooter) {
|
||||||
|
return t.rowsFooter[rowIdx]
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if rowIdx >= 0 && rowIdx < len(t.rows) {
|
||||||
|
return t.rows[rowIdx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rowStr{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) getRowConfig(hint renderHint) RowConfig {
|
||||||
|
rowIdx := hint.rowNumber - 1
|
||||||
|
if rowIdx < 0 {
|
||||||
|
rowIdx = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case hint.isHeaderRow:
|
||||||
|
return t.rowsHeaderConfigMap[rowIdx]
|
||||||
|
case hint.isFooterRow:
|
||||||
|
return t.rowsFooterConfigMap[rowIdx]
|
||||||
|
default:
|
||||||
|
return t.rowsConfigMap[rowIdx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) getSeparatorColors(hint renderHint) text.Colors {
|
||||||
|
if t.style.Options.DoNotColorBordersAndSeparators {
|
||||||
|
return nil
|
||||||
|
} else if (hint.isBorderBottom || hint.isBorderTop) && t.style.Color.Border != nil {
|
||||||
|
return t.style.Color.Border
|
||||||
|
} else if t.style.Color.Separator != nil {
|
||||||
|
return t.style.Color.Separator
|
||||||
|
} else if hint.isHeaderRow {
|
||||||
|
return t.style.Color.Header
|
||||||
|
} else if hint.isFooterRow {
|
||||||
|
return t.style.Color.Footer
|
||||||
|
} else if hint.isAutoIndexColumn {
|
||||||
|
return t.style.Color.IndexColumn
|
||||||
|
} else if hint.rowNumber > 0 && hint.rowNumber%2 == 0 {
|
||||||
|
return t.style.Color.RowAlternate
|
||||||
|
}
|
||||||
|
return t.style.Color.Row
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) getVAlign(colIdx int, hint renderHint) text.VAlign {
|
||||||
|
vAlign := text.VAlignDefault
|
||||||
|
if cfg, ok := t.columnConfigMap[colIdx]; ok {
|
||||||
|
if hint.isHeaderRow {
|
||||||
|
vAlign = cfg.VAlignHeader
|
||||||
|
} else if hint.isFooterRow {
|
||||||
|
vAlign = cfg.VAlignFooter
|
||||||
|
} else {
|
||||||
|
vAlign = cfg.VAlign
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vAlign
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) hasHiddenColumns() bool {
|
||||||
|
for _, cc := range t.columnConfigMap {
|
||||||
|
if cc.Hidden {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) hideColumns() map[int]int {
|
||||||
|
colIdxMap := make(map[int]int)
|
||||||
|
numColumns := 0
|
||||||
|
hideColumnsInRows := func(rows []rowStr) []rowStr {
|
||||||
|
var rsp []rowStr
|
||||||
|
for _, row := range rows {
|
||||||
|
var rowNew rowStr
|
||||||
|
for colIdx, col := range row {
|
||||||
|
cc := t.columnConfigMap[colIdx]
|
||||||
|
if !cc.Hidden {
|
||||||
|
rowNew = append(rowNew, col)
|
||||||
|
colIdxMap[colIdx] = len(rowNew) - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(rowNew) > numColumns {
|
||||||
|
numColumns = len(rowNew)
|
||||||
|
}
|
||||||
|
rsp = append(rsp, rowNew)
|
||||||
|
}
|
||||||
|
return rsp
|
||||||
|
}
|
||||||
|
|
||||||
|
// hide columns as directed
|
||||||
|
t.rows = hideColumnsInRows(t.rows)
|
||||||
|
t.rowsFooter = hideColumnsInRows(t.rowsFooter)
|
||||||
|
t.rowsHeader = hideColumnsInRows(t.rowsHeader)
|
||||||
|
|
||||||
|
// reset numColumns to the new number of columns
|
||||||
|
t.numColumns = numColumns
|
||||||
|
|
||||||
|
return colIdxMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) isIndexColumn(colIdx int, hint renderHint) bool {
|
||||||
|
return t.indexColumn == colIdx+1 || hint.isAutoIndexColumn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) render(out *strings.Builder) string {
|
||||||
|
outStr := out.String()
|
||||||
|
if t.outputMirror != nil && len(outStr) > 0 {
|
||||||
|
_, _ = t.outputMirror.Write([]byte(outStr))
|
||||||
|
_, _ = t.outputMirror.Write([]byte("\n"))
|
||||||
|
}
|
||||||
|
return outStr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) shouldMergeCellsHorizontallyAbove(row rowStr, colIdx int, hint renderHint) bool {
|
||||||
|
if hint.isAutoIndexColumn || hint.isAutoIndexRow {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
rowConfig := t.getRowConfig(hint)
|
||||||
|
if hint.isSeparatorRow {
|
||||||
|
if hint.isHeaderRow && hint.rowNumber == 1 {
|
||||||
|
rowConfig = t.getRowConfig(hint)
|
||||||
|
row = t.getRow(hint.rowNumber-1, hint)
|
||||||
|
} else if hint.isFooterRow && hint.isFirstRow {
|
||||||
|
rowConfig = t.getRowConfig(renderHint{isLastRow: true, rowNumber: len(t.rows)})
|
||||||
|
row = t.getRow(len(t.rows)-1, renderHint{})
|
||||||
|
} else if hint.isFooterRow && hint.isBorderBottom {
|
||||||
|
row = t.getRow(len(t.rowsFooter)-1, renderHint{isFooterRow: true})
|
||||||
|
} else {
|
||||||
|
row = t.getRow(hint.rowNumber-1, hint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowConfig.AutoMerge {
|
||||||
|
return row.areEqual(colIdx-1, colIdx)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) shouldMergeCellsHorizontallyBelow(row rowStr, colIdx int, hint renderHint) bool {
|
||||||
|
if hint.isAutoIndexColumn || hint.isAutoIndexRow {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var rowConfig RowConfig
|
||||||
|
if hint.isSeparatorRow {
|
||||||
|
if hint.isHeaderRow && hint.rowNumber == 0 {
|
||||||
|
rowConfig = t.getRowConfig(renderHint{isHeaderRow: true, rowNumber: 1})
|
||||||
|
row = t.getRow(0, hint)
|
||||||
|
} else if hint.isHeaderRow && hint.isLastRow {
|
||||||
|
rowConfig = t.getRowConfig(renderHint{rowNumber: 1})
|
||||||
|
row = t.getRow(0, renderHint{})
|
||||||
|
} else if hint.isHeaderRow {
|
||||||
|
rowConfig = t.getRowConfig(renderHint{isHeaderRow: true, rowNumber: hint.rowNumber + 1})
|
||||||
|
row = t.getRow(hint.rowNumber, hint)
|
||||||
|
} else if hint.isFooterRow && hint.rowNumber >= 0 {
|
||||||
|
rowConfig = t.getRowConfig(renderHint{isFooterRow: true, rowNumber: 1})
|
||||||
|
row = t.getRow(hint.rowNumber, renderHint{isFooterRow: true})
|
||||||
|
} else if hint.isRegularRow() {
|
||||||
|
rowConfig = t.getRowConfig(renderHint{rowNumber: hint.rowNumber + 1})
|
||||||
|
row = t.getRow(hint.rowNumber, renderHint{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowConfig.AutoMerge {
|
||||||
|
return row.areEqual(colIdx-1, colIdx)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) shouldMergeCellsVertically(colIdx int, hint renderHint) bool {
|
||||||
|
if t.columnConfigMap[colIdx].AutoMerge && colIdx < t.numColumns {
|
||||||
|
if hint.isSeparatorRow {
|
||||||
|
rowPrev := t.getRow(hint.rowNumber-1, hint)
|
||||||
|
rowNext := t.getRow(hint.rowNumber, hint)
|
||||||
|
if colIdx < len(rowPrev) && colIdx < len(rowNext) {
|
||||||
|
return rowPrev[colIdx] == rowNext[colIdx] || "" == rowNext[colIdx]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rowPrev := t.getRow(hint.rowNumber-2, hint)
|
||||||
|
rowCurr := t.getRow(hint.rowNumber-1, hint)
|
||||||
|
if colIdx < len(rowPrev) && colIdx < len(rowCurr) {
|
||||||
|
return rowPrev[colIdx] == rowCurr[colIdx] || "" == rowCurr[colIdx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) wrapRow(row rowStr) (int, rowStr) {
|
||||||
|
colMaxLines := 0
|
||||||
|
rowWrapped := make(rowStr, len(row))
|
||||||
|
for colIdx, colStr := range row {
|
||||||
|
widthEnforcer := t.columnConfigMap[colIdx].getWidthMaxEnforcer()
|
||||||
|
maxWidth := t.getColumnWidthMax(colIdx)
|
||||||
|
if maxWidth == 0 {
|
||||||
|
maxWidth = t.maxColumnLengths[colIdx]
|
||||||
|
}
|
||||||
|
rowWrapped[colIdx] = widthEnforcer(colStr, maxWidth)
|
||||||
|
colNumLines := strings.Count(rowWrapped[colIdx], "\n") + 1
|
||||||
|
if colNumLines > colMaxLines {
|
||||||
|
colMaxLines = colNumLines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return colMaxLines, rowWrapped
|
||||||
|
}
|
375
helper/table/table_test.go
Normal file
375
helper/table/table_test.go
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
package table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"git.hexq.cn/tiglog/golib/helper/text"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testCaption = "A Song of Ice and Fire"
|
||||||
|
testColor = text.Colors{text.FgGreen}
|
||||||
|
testColorHiRedBold = text.Colors{text.FgHiRed, text.Bold}
|
||||||
|
testColorHiBlueBold = text.Colors{text.FgHiBlue, text.Bold}
|
||||||
|
testCSSClass = "test-css-class"
|
||||||
|
testFooter = Row{"", "", "Total", 10000}
|
||||||
|
testFooterMultiLine = Row{"", "", "Total\nSalary", 10000}
|
||||||
|
testHeader = Row{"#", "First Name", "Last Name", "Salary"}
|
||||||
|
testHeaderMultiLine = Row{"#", "First\nName", "Last\nName", "Salary"}
|
||||||
|
testRows = []Row{
|
||||||
|
{1, "Arya", "Stark", 3000},
|
||||||
|
{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"},
|
||||||
|
{300, "Tyrion", "Lannister", 5000},
|
||||||
|
}
|
||||||
|
testRowMultiLine = Row{0, "Winter", "Is", 0, "Coming.\r\nThe North Remembers!\nThis is known."}
|
||||||
|
testRowNewLines = Row{0, "Valar", "Morghulis", 0, "Faceless\nMen"}
|
||||||
|
testRowPipes = Row{0, "Valar", "Morghulis", 0, "Faceless|Men"}
|
||||||
|
testRowTabs = Row{0, "Valar", "Morghulis", 0, "Faceless\tMen"}
|
||||||
|
testTitle1 = "Game of Thrones"
|
||||||
|
testTitle2 = "When you play the Game of Thrones, you win or you die. There is no middle ground."
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
text.EnableColors()
|
||||||
|
}
|
||||||
|
|
||||||
|
type myMockOutputMirror struct {
|
||||||
|
mirroredOutput string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *myMockOutputMirror) Write(p []byte) (n int, err error) {
|
||||||
|
t.mirroredOutput += string(p)
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewWriter(t *testing.T) {
|
||||||
|
tw := NewWriter()
|
||||||
|
assert.NotNil(t, tw.Style())
|
||||||
|
assert.Equal(t, StyleDefault, *tw.Style())
|
||||||
|
|
||||||
|
tw.SetStyle(StyleBold)
|
||||||
|
assert.NotNil(t, tw.Style())
|
||||||
|
assert.Equal(t, StyleBold, *tw.Style())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_AppendFooter(t *testing.T) {
|
||||||
|
table := Table{}
|
||||||
|
assert.Equal(t, 0, len(table.rowsFooterRaw))
|
||||||
|
|
||||||
|
table.AppendFooter([]interface{}{})
|
||||||
|
assert.Equal(t, 0, table.Length())
|
||||||
|
assert.Equal(t, 1, len(table.rowsFooterRaw))
|
||||||
|
assert.Equal(t, 0, len(table.rowsHeaderRaw))
|
||||||
|
|
||||||
|
table.AppendFooter([]interface{}{})
|
||||||
|
assert.Equal(t, 0, table.Length())
|
||||||
|
assert.Equal(t, 2, len(table.rowsFooterRaw))
|
||||||
|
assert.Equal(t, 0, len(table.rowsHeaderRaw))
|
||||||
|
|
||||||
|
table.AppendFooter([]interface{}{}, RowConfig{AutoMerge: true})
|
||||||
|
assert.Equal(t, 0, table.Length())
|
||||||
|
assert.Equal(t, 3, len(table.rowsFooterRaw))
|
||||||
|
assert.Equal(t, 0, len(table.rowsHeaderRaw))
|
||||||
|
assert.False(t, table.rowsFooterConfigMap[0].AutoMerge)
|
||||||
|
assert.False(t, table.rowsFooterConfigMap[1].AutoMerge)
|
||||||
|
assert.True(t, table.rowsFooterConfigMap[2].AutoMerge)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_AppendHeader(t *testing.T) {
|
||||||
|
table := Table{}
|
||||||
|
assert.Equal(t, 0, len(table.rowsHeaderRaw))
|
||||||
|
|
||||||
|
table.AppendHeader([]interface{}{})
|
||||||
|
assert.Equal(t, 0, table.Length())
|
||||||
|
assert.Equal(t, 0, len(table.rowsFooterRaw))
|
||||||
|
assert.Equal(t, 1, len(table.rowsHeaderRaw))
|
||||||
|
|
||||||
|
table.AppendHeader([]interface{}{})
|
||||||
|
assert.Equal(t, 0, table.Length())
|
||||||
|
assert.Equal(t, 0, len(table.rowsFooterRaw))
|
||||||
|
assert.Equal(t, 2, len(table.rowsHeaderRaw))
|
||||||
|
|
||||||
|
table.AppendHeader([]interface{}{}, RowConfig{AutoMerge: true})
|
||||||
|
assert.Equal(t, 0, table.Length())
|
||||||
|
assert.Equal(t, 0, len(table.rowsFooterRaw))
|
||||||
|
assert.Equal(t, 3, len(table.rowsHeaderRaw))
|
||||||
|
assert.False(t, table.rowsHeaderConfigMap[0].AutoMerge)
|
||||||
|
assert.False(t, table.rowsHeaderConfigMap[1].AutoMerge)
|
||||||
|
assert.True(t, table.rowsHeaderConfigMap[2].AutoMerge)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_AppendRow(t *testing.T) {
|
||||||
|
table := Table{}
|
||||||
|
assert.Equal(t, 0, table.Length())
|
||||||
|
|
||||||
|
table.AppendRow([]interface{}{})
|
||||||
|
assert.Equal(t, 1, table.Length())
|
||||||
|
assert.Equal(t, 0, len(table.rowsFooter))
|
||||||
|
assert.Equal(t, 0, len(table.rowsHeader))
|
||||||
|
|
||||||
|
table.AppendRow([]interface{}{})
|
||||||
|
assert.Equal(t, 2, table.Length())
|
||||||
|
assert.Equal(t, 0, len(table.rowsFooter))
|
||||||
|
assert.Equal(t, 0, len(table.rowsHeader))
|
||||||
|
|
||||||
|
table.AppendRow([]interface{}{}, RowConfig{AutoMerge: true})
|
||||||
|
assert.Equal(t, 3, table.Length())
|
||||||
|
assert.Equal(t, 0, len(table.rowsFooterRaw))
|
||||||
|
assert.Equal(t, 0, len(table.rowsHeaderRaw))
|
||||||
|
assert.False(t, table.rowsConfigMap[0].AutoMerge)
|
||||||
|
assert.False(t, table.rowsConfigMap[1].AutoMerge)
|
||||||
|
assert.True(t, table.rowsConfigMap[2].AutoMerge)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_AppendRows(t *testing.T) {
|
||||||
|
table := Table{}
|
||||||
|
assert.Equal(t, 0, table.Length())
|
||||||
|
|
||||||
|
table.AppendRows([]Row{{}})
|
||||||
|
assert.Equal(t, 1, table.Length())
|
||||||
|
assert.Equal(t, 0, len(table.rowsFooter))
|
||||||
|
assert.Equal(t, 0, len(table.rowsHeader))
|
||||||
|
|
||||||
|
table.AppendRows([]Row{{}})
|
||||||
|
assert.Equal(t, 2, table.Length())
|
||||||
|
assert.Equal(t, 0, len(table.rowsFooter))
|
||||||
|
assert.Equal(t, 0, len(table.rowsHeader))
|
||||||
|
|
||||||
|
table.AppendRows([]Row{{}, {}}, RowConfig{AutoMerge: true})
|
||||||
|
assert.Equal(t, 4, table.Length())
|
||||||
|
assert.Equal(t, 0, len(table.rowsFooterRaw))
|
||||||
|
assert.Equal(t, 0, len(table.rowsHeaderRaw))
|
||||||
|
assert.False(t, table.rowsConfigMap[0].AutoMerge)
|
||||||
|
assert.False(t, table.rowsConfigMap[1].AutoMerge)
|
||||||
|
assert.True(t, table.rowsConfigMap[2].AutoMerge)
|
||||||
|
assert.True(t, table.rowsConfigMap[3].AutoMerge)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_Length(t *testing.T) {
|
||||||
|
table := Table{}
|
||||||
|
assert.Zero(t, table.Length())
|
||||||
|
|
||||||
|
table.AppendRow(testRows[0])
|
||||||
|
assert.Equal(t, 1, table.Length())
|
||||||
|
table.AppendRow(testRows[1])
|
||||||
|
assert.Equal(t, 2, table.Length())
|
||||||
|
|
||||||
|
table.AppendHeader(testHeader)
|
||||||
|
assert.Equal(t, 2, table.Length())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_ResetFooters(t *testing.T) {
|
||||||
|
table := Table{}
|
||||||
|
table.AppendFooter(testFooter)
|
||||||
|
assert.NotEmpty(t, table.rowsFooterRaw)
|
||||||
|
|
||||||
|
table.ResetFooters()
|
||||||
|
assert.Empty(t, table.rowsFooterRaw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_ResetHeaders(t *testing.T) {
|
||||||
|
table := Table{}
|
||||||
|
table.AppendHeader(testHeader)
|
||||||
|
assert.NotEmpty(t, table.rowsHeaderRaw)
|
||||||
|
|
||||||
|
table.ResetHeaders()
|
||||||
|
assert.Empty(t, table.rowsHeaderRaw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_ResetRows(t *testing.T) {
|
||||||
|
table := Table{}
|
||||||
|
table.AppendRows(testRows)
|
||||||
|
assert.NotEmpty(t, table.rowsRaw)
|
||||||
|
|
||||||
|
table.ResetRows()
|
||||||
|
assert.Empty(t, table.rowsRaw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_SetAllowedRowLength(t *testing.T) {
|
||||||
|
table := Table{}
|
||||||
|
table.AppendRows(testRows)
|
||||||
|
table.SetStyle(styleTest)
|
||||||
|
|
||||||
|
expectedOutWithNoRowLimit := `(-----^--------^-----------^------^-----------------------------)
|
||||||
|
[< 1>|<Arya >|<Stark >|<3000>|< >]
|
||||||
|
[< 20>|<Jon >|<Snow >|<2000>|<You know nothing, Jon Snow!>]
|
||||||
|
[<300>|<Tyrion>|<Lannister>|<5000>|< >]
|
||||||
|
\-----v--------v-----------v------v-----------------------------/`
|
||||||
|
assert.Zero(t, table.allowedRowLength)
|
||||||
|
assert.Equal(t, expectedOutWithNoRowLimit, table.Render())
|
||||||
|
|
||||||
|
table.SetAllowedRowLength(utf8.RuneCountInString(table.style.Box.UnfinishedRow))
|
||||||
|
assert.Equal(t, utf8.RuneCountInString(table.style.Box.UnfinishedRow), table.allowedRowLength)
|
||||||
|
assert.Equal(t, "", table.Render())
|
||||||
|
|
||||||
|
table.SetAllowedRowLength(5)
|
||||||
|
expectedOutWithRowLimit := `( ~~~
|
||||||
|
[ ~~~
|
||||||
|
[ ~~~
|
||||||
|
[ ~~~
|
||||||
|
\ ~~~`
|
||||||
|
assert.Equal(t, 5, table.allowedRowLength)
|
||||||
|
assert.Equal(t, expectedOutWithRowLimit, table.Render())
|
||||||
|
|
||||||
|
table.SetAllowedRowLength(30)
|
||||||
|
expectedOutWithRowLimit = `(-----^--------^---------- ~~~
|
||||||
|
[< 1>|<Arya >|<Stark ~~~
|
||||||
|
[< 20>|<Jon >|<Snow ~~~
|
||||||
|
[<300>|<Tyrion>|<Lannister ~~~
|
||||||
|
\-----v--------v---------- ~~~`
|
||||||
|
assert.Equal(t, 30, table.allowedRowLength)
|
||||||
|
assert.Equal(t, expectedOutWithRowLimit, table.Render())
|
||||||
|
|
||||||
|
table.SetAllowedRowLength(300)
|
||||||
|
assert.Equal(t, 300, table.allowedRowLength)
|
||||||
|
assert.Equal(t, expectedOutWithNoRowLimit, table.Render())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_SetAutoIndex(t *testing.T) {
|
||||||
|
table := Table{}
|
||||||
|
table.AppendRows(testRows)
|
||||||
|
table.SetStyle(styleTest)
|
||||||
|
|
||||||
|
expectedOut := `(-----^--------^-----------^------^-----------------------------)
|
||||||
|
[< 1>|<Arya >|<Stark >|<3000>|< >]
|
||||||
|
[< 20>|<Jon >|<Snow >|<2000>|<You know nothing, Jon Snow!>]
|
||||||
|
[<300>|<Tyrion>|<Lannister>|<5000>|< >]
|
||||||
|
\-----v--------v-----------v------v-----------------------------/`
|
||||||
|
assert.False(t, table.autoIndex)
|
||||||
|
assert.Equal(t, expectedOut, table.Render())
|
||||||
|
|
||||||
|
table.SetAutoIndex(true)
|
||||||
|
expectedOut = `(---^-----^--------^-----------^------^-----------------------------)
|
||||||
|
[< >|< A>|< B >|< C >|< D>|< E >]
|
||||||
|
{---+-----+--------+-----------+------+-----------------------------}
|
||||||
|
[<1>|< 1>|<Arya >|<Stark >|<3000>|< >]
|
||||||
|
[<2>|< 20>|<Jon >|<Snow >|<2000>|<You know nothing, Jon Snow!>]
|
||||||
|
[<3>|<300>|<Tyrion>|<Lannister>|<5000>|< >]
|
||||||
|
\---v-----v--------v-----------v------v-----------------------------/`
|
||||||
|
assert.True(t, table.autoIndex)
|
||||||
|
assert.Equal(t, expectedOut, table.Render())
|
||||||
|
|
||||||
|
table.AppendHeader(testHeader)
|
||||||
|
expectedOut = `(---^-----^------------^-----------^--------^-----------------------------)
|
||||||
|
[< >|< #>|<FIRST NAME>|<LAST NAME>|<SALARY>|< >]
|
||||||
|
{---+-----+------------+-----------+--------+-----------------------------}
|
||||||
|
[<1>|< 1>|<Arya >|<Stark >|< 3000>|< >]
|
||||||
|
[<2>|< 20>|<Jon >|<Snow >|< 2000>|<You know nothing, Jon Snow!>]
|
||||||
|
[<3>|<300>|<Tyrion >|<Lannister>|< 5000>|< >]
|
||||||
|
\---v-----v------------v-----------v--------v-----------------------------/`
|
||||||
|
assert.True(t, table.autoIndex)
|
||||||
|
assert.Equal(t, expectedOut, table.Render())
|
||||||
|
|
||||||
|
table.AppendRow(testRowMultiLine)
|
||||||
|
expectedOut = `(---^-----^------------^-----------^--------^-----------------------------)
|
||||||
|
[< >|< #>|<FIRST NAME>|<LAST NAME>|<SALARY>|< >]
|
||||||
|
{---+-----+------------+-----------+--------+-----------------------------}
|
||||||
|
[<1>|< 1>|<Arya >|<Stark >|< 3000>|< >]
|
||||||
|
[<2>|< 20>|<Jon >|<Snow >|< 2000>|<You know nothing, Jon Snow!>]
|
||||||
|
[<3>|<300>|<Tyrion >|<Lannister>|< 5000>|< >]
|
||||||
|
[<4>|< 0>|<Winter >|<Is >|< 0>|<Coming. >]
|
||||||
|
[< >|< >|< >|< >|< >|<The North Remembers! >]
|
||||||
|
[< >|< >|< >|< >|< >|<This is known. >]
|
||||||
|
\---v-----v------------v-----------v--------v-----------------------------/`
|
||||||
|
assert.Equal(t, expectedOut, table.Render())
|
||||||
|
|
||||||
|
table.SetStyle(StyleLight)
|
||||||
|
expectedOut = `┌───┬─────┬────────────┬───────────┬────────┬─────────────────────────────┐
|
||||||
|
│ │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||||
|
├───┼─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||||
|
│ 1 │ 1 │ Arya │ Stark │ 3000 │ │
|
||||||
|
│ 2 │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||||
|
│ 3 │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||||
|
│ 4 │ 0 │ Winter │ Is │ 0 │ Coming. │
|
||||||
|
│ │ │ │ │ │ The North Remembers! │
|
||||||
|
│ │ │ │ │ │ This is known. │
|
||||||
|
└───┴─────┴────────────┴───────────┴────────┴─────────────────────────────┘`
|
||||||
|
assert.Equal(t, expectedOut, table.Render())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_SetCaption(t *testing.T) {
|
||||||
|
table := Table{}
|
||||||
|
assert.Empty(t, table.caption)
|
||||||
|
|
||||||
|
table.SetCaption(testCaption)
|
||||||
|
assert.NotEmpty(t, table.caption)
|
||||||
|
assert.Equal(t, testCaption, table.caption)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_SetColumnConfigs(t *testing.T) {
|
||||||
|
table := Table{}
|
||||||
|
assert.Empty(t, table.columnConfigs)
|
||||||
|
|
||||||
|
table.SetColumnConfigs([]ColumnConfig{{}, {}, {}})
|
||||||
|
assert.NotEmpty(t, table.columnConfigs)
|
||||||
|
assert.Equal(t, 3, len(table.columnConfigs))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_SetHTMLCSSClass(t *testing.T) {
|
||||||
|
table := Table{}
|
||||||
|
table.AppendRow(testRows[0])
|
||||||
|
expectedHTML := `<table class="` + DefaultHTMLCSSClass + `">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="right">1</td>
|
||||||
|
<td>Arya</td>
|
||||||
|
<td>Stark</td>
|
||||||
|
<td align="right">3000</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>`
|
||||||
|
assert.Equal(t, "", table.htmlCSSClass)
|
||||||
|
assert.Equal(t, expectedHTML, table.RenderHTML())
|
||||||
|
|
||||||
|
table.SetHTMLCSSClass(testCSSClass)
|
||||||
|
assert.Equal(t, testCSSClass, table.htmlCSSClass)
|
||||||
|
assert.Equal(t, strings.Replace(expectedHTML, DefaultHTMLCSSClass, testCSSClass, -1), table.RenderHTML())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_SetOutputMirror(t *testing.T) {
|
||||||
|
table := Table{}
|
||||||
|
table.AppendRow(testRows[0])
|
||||||
|
expectedOut := `+---+------+-------+------+
|
||||||
|
| 1 | Arya | Stark | 3000 |
|
||||||
|
+---+------+-------+------+`
|
||||||
|
assert.Equal(t, nil, table.outputMirror)
|
||||||
|
assert.Equal(t, expectedOut, table.Render())
|
||||||
|
|
||||||
|
mockOutputMirror := &myMockOutputMirror{}
|
||||||
|
table.SetOutputMirror(mockOutputMirror)
|
||||||
|
assert.Equal(t, mockOutputMirror, table.outputMirror)
|
||||||
|
assert.Equal(t, expectedOut, table.Render())
|
||||||
|
assert.Equal(t, expectedOut+"\n", mockOutputMirror.mirroredOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_SePageSize(t *testing.T) {
|
||||||
|
table := Table{}
|
||||||
|
assert.Equal(t, 0, table.pageSize)
|
||||||
|
|
||||||
|
table.SetPageSize(13)
|
||||||
|
assert.Equal(t, 13, table.pageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_SortByColumn(t *testing.T) {
|
||||||
|
table := Table{}
|
||||||
|
assert.Empty(t, table.sortBy)
|
||||||
|
|
||||||
|
table.SortBy([]SortBy{{Name: "#", Mode: Asc}})
|
||||||
|
assert.Equal(t, 1, len(table.sortBy))
|
||||||
|
|
||||||
|
table.SortBy([]SortBy{{Name: "First Name", Mode: Dsc}, {Name: "Last Name", Mode: Asc}})
|
||||||
|
assert.Equal(t, 2, len(table.sortBy))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTable_SetStyle(t *testing.T) {
|
||||||
|
table := Table{}
|
||||||
|
assert.NotNil(t, table.Style())
|
||||||
|
assert.Equal(t, StyleDefault, *table.Style())
|
||||||
|
|
||||||
|
table.SetStyle(StyleDefault)
|
||||||
|
assert.NotNil(t, table.Style())
|
||||||
|
assert.Equal(t, StyleDefault, *table.Style())
|
||||||
|
}
|
69
helper/table/util.go
Normal file
69
helper/table/util.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AutoIndexColumnID returns a unique Column ID/Name for the given Column Number.
|
||||||
|
// The functionality is similar to what you get in an Excel spreadsheet w.r.t.
|
||||||
|
// the Column ID/Name.
|
||||||
|
func AutoIndexColumnID(colIdx int) string {
|
||||||
|
charIdx := colIdx % 26
|
||||||
|
out := string(rune(65 + charIdx))
|
||||||
|
colIdx = colIdx / 26
|
||||||
|
if colIdx > 0 {
|
||||||
|
return AutoIndexColumnID(colIdx-1) + out
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// WidthEnforcer is a function that helps enforce a width condition on a string.
|
||||||
|
type WidthEnforcer func(col string, maxLen int) string
|
||||||
|
|
||||||
|
// widthEnforcerNone returns the input string as is without any modifications.
|
||||||
|
func widthEnforcerNone(col string, maxLen int) string {
|
||||||
|
return col
|
||||||
|
}
|
||||||
|
|
||||||
|
// isNumber returns true if the argument is a numeric type; false otherwise.
|
||||||
|
func isNumber(x interface{}) bool {
|
||||||
|
if x == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch reflect.TypeOf(x).Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||||
|
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||||||
|
reflect.Float32, reflect.Float64:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type mergedColumnIndices map[int]map[int]bool
|
||||||
|
|
||||||
|
func (m mergedColumnIndices) mergedLength(colIdx int, maxColumnLengths []int) int {
|
||||||
|
mergedLength := maxColumnLengths[colIdx]
|
||||||
|
for otherColIdx := range m[colIdx] {
|
||||||
|
mergedLength += maxColumnLengths[otherColIdx]
|
||||||
|
}
|
||||||
|
return mergedLength
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mergedColumnIndices) len(colIdx int) int {
|
||||||
|
return len(m[colIdx]) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mergedColumnIndices) safeAppend(colIdx, otherColIdx int) {
|
||||||
|
// map
|
||||||
|
if m[colIdx] == nil {
|
||||||
|
m[colIdx] = make(map[int]bool)
|
||||||
|
}
|
||||||
|
m[colIdx][otherColIdx] = true
|
||||||
|
|
||||||
|
// reverse map
|
||||||
|
if m[otherColIdx] == nil {
|
||||||
|
m[otherColIdx] = make(map[int]bool)
|
||||||
|
}
|
||||||
|
m[otherColIdx][colIdx] = true
|
||||||
|
}
|
53
helper/table/util_test.go
Normal file
53
helper/table/util_test.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleAutoIndexColumnID() {
|
||||||
|
fmt.Printf("AutoIndexColumnID( 0): \"%s\"\n", AutoIndexColumnID(0))
|
||||||
|
fmt.Printf("AutoIndexColumnID( 1): \"%s\"\n", AutoIndexColumnID(1))
|
||||||
|
fmt.Printf("AutoIndexColumnID( 2): \"%s\"\n", AutoIndexColumnID(2))
|
||||||
|
fmt.Printf("AutoIndexColumnID( 25): \"%s\"\n", AutoIndexColumnID(25))
|
||||||
|
fmt.Printf("AutoIndexColumnID( 26): \"%s\"\n", AutoIndexColumnID(26))
|
||||||
|
fmt.Printf("AutoIndexColumnID( 702): \"%s\"\n", AutoIndexColumnID(702))
|
||||||
|
fmt.Printf("AutoIndexColumnID(18278): \"%s\"\n", AutoIndexColumnID(18278))
|
||||||
|
|
||||||
|
// Output: AutoIndexColumnID( 0): "A"
|
||||||
|
// AutoIndexColumnID( 1): "B"
|
||||||
|
// AutoIndexColumnID( 2): "C"
|
||||||
|
// AutoIndexColumnID( 25): "Z"
|
||||||
|
// AutoIndexColumnID( 26): "AA"
|
||||||
|
// AutoIndexColumnID( 702): "AAA"
|
||||||
|
// AutoIndexColumnID(18278): "AAAA"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoIndexColumnID(t *testing.T) {
|
||||||
|
assert.Equal(t, "A", AutoIndexColumnID(0))
|
||||||
|
assert.Equal(t, "Z", AutoIndexColumnID(25))
|
||||||
|
assert.Equal(t, "AA", AutoIndexColumnID(26))
|
||||||
|
assert.Equal(t, "ZZ", AutoIndexColumnID(701))
|
||||||
|
assert.Equal(t, "AAA", AutoIndexColumnID(702))
|
||||||
|
assert.Equal(t, "ZZZ", AutoIndexColumnID(18277))
|
||||||
|
assert.Equal(t, "AAAA", AutoIndexColumnID(18278))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsNumber(t *testing.T) {
|
||||||
|
assert.True(t, isNumber(int(1)))
|
||||||
|
assert.True(t, isNumber(int8(1)))
|
||||||
|
assert.True(t, isNumber(int16(1)))
|
||||||
|
assert.True(t, isNumber(int32(1)))
|
||||||
|
assert.True(t, isNumber(int64(1)))
|
||||||
|
assert.True(t, isNumber(uint(1)))
|
||||||
|
assert.True(t, isNumber(uint8(1)))
|
||||||
|
assert.True(t, isNumber(uint16(1)))
|
||||||
|
assert.True(t, isNumber(uint32(1)))
|
||||||
|
assert.True(t, isNumber(uint64(1)))
|
||||||
|
assert.True(t, isNumber(float32(1)))
|
||||||
|
assert.True(t, isNumber(float64(1)))
|
||||||
|
assert.False(t, isNumber("1"))
|
||||||
|
assert.False(t, isNumber(nil))
|
||||||
|
}
|
43
helper/table/writer.go
Normal file
43
helper/table/writer.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Writer declares the interfaces that can be used to setup and render a table.
|
||||||
|
type Writer interface {
|
||||||
|
AppendFooter(row Row, configs ...RowConfig)
|
||||||
|
AppendHeader(row Row, configs ...RowConfig)
|
||||||
|
AppendRow(row Row, configs ...RowConfig)
|
||||||
|
AppendRows(rows []Row, configs ...RowConfig)
|
||||||
|
AppendSeparator()
|
||||||
|
Length() int
|
||||||
|
Render() string
|
||||||
|
RenderCSV() string
|
||||||
|
RenderHTML() string
|
||||||
|
RenderMarkdown() string
|
||||||
|
ResetFooters()
|
||||||
|
ResetHeaders()
|
||||||
|
ResetRows()
|
||||||
|
SetAllowedRowLength(length int)
|
||||||
|
SetAutoIndex(autoIndex bool)
|
||||||
|
SetCaption(format string, a ...interface{})
|
||||||
|
SetColumnConfigs(configs []ColumnConfig)
|
||||||
|
SetIndexColumn(colNum int)
|
||||||
|
SetOutputMirror(mirror io.Writer)
|
||||||
|
SetPageSize(numLines int)
|
||||||
|
SetRowPainter(painter RowPainter)
|
||||||
|
SetStyle(style Style)
|
||||||
|
SetTitle(format string, a ...interface{})
|
||||||
|
SortBy(sortBy []SortBy)
|
||||||
|
Style() *Style
|
||||||
|
SuppressEmptyColumns()
|
||||||
|
|
||||||
|
// deprecated; in favor of Style().HTML.CSSClass
|
||||||
|
SetHTMLCSSClass(cssClass string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWriter initializes and returns a Writer.
|
||||||
|
func NewWriter() Writer {
|
||||||
|
return &Table{}
|
||||||
|
}
|
78
helper/table/writer_test.go
Normal file
78
helper/table/writer_test.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.hexq.cn/tiglog/golib/helper/text"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Example_simple() {
|
||||||
|
// simple table with zero customizations
|
||||||
|
tw := NewWriter()
|
||||||
|
// append a header row
|
||||||
|
tw.AppendHeader(Row{"#", "First Name", "Last Name", "Salary"})
|
||||||
|
// append some data rows
|
||||||
|
tw.AppendRows([]Row{
|
||||||
|
{1, "Arya", "Stark", 3000},
|
||||||
|
{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"},
|
||||||
|
{300, "Tyrion", "Lannister", 5000},
|
||||||
|
})
|
||||||
|
// append a footer row
|
||||||
|
tw.AppendFooter(Row{"", "", "Total", 10000})
|
||||||
|
// render it
|
||||||
|
fmt.Printf("Table without any customizations:\n%s", tw.Render())
|
||||||
|
|
||||||
|
// Output: Table without any customizations:
|
||||||
|
// +-----+------------+-----------+--------+-----------------------------+
|
||||||
|
// | # | FIRST NAME | LAST NAME | SALARY | |
|
||||||
|
// +-----+------------+-----------+--------+-----------------------------+
|
||||||
|
// | 1 | Arya | Stark | 3000 | |
|
||||||
|
// | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
|
||||||
|
// | 300 | Tyrion | Lannister | 5000 | |
|
||||||
|
// +-----+------------+-----------+--------+-----------------------------+
|
||||||
|
// | | | TOTAL | 10000 | |
|
||||||
|
// +-----+------------+-----------+--------+-----------------------------+
|
||||||
|
}
|
||||||
|
|
||||||
|
func Example_styled() {
|
||||||
|
// table with some amount of customization
|
||||||
|
tw := NewWriter()
|
||||||
|
// append a header row
|
||||||
|
tw.AppendHeader(Row{"First Name", "Last Name", "Salary"})
|
||||||
|
// append some data rows
|
||||||
|
tw.AppendRows([]Row{
|
||||||
|
{"Jaime", "Lannister", 5000},
|
||||||
|
{"Arya", "Stark", 3000, "A girl has no name."},
|
||||||
|
{"Sansa", "Stark", 4000},
|
||||||
|
{"Jon", "Snow", 2000, "You know nothing, Jon Snow!"},
|
||||||
|
{"Tyrion", "Lannister", 5000, "A Lannister always pays his debts."},
|
||||||
|
})
|
||||||
|
// append a footer row
|
||||||
|
tw.AppendFooter(Row{"", "Total", 10000})
|
||||||
|
// auto-index rows
|
||||||
|
tw.SetAutoIndex(true)
|
||||||
|
// sort by last name and then by salary
|
||||||
|
tw.SortBy([]SortBy{{Name: "Last Name", Mode: Dsc}, {Name: "Salary", Mode: AscNumeric}})
|
||||||
|
// use a ready-to-use style
|
||||||
|
tw.SetStyle(StyleLight)
|
||||||
|
// customize the style and change some stuff
|
||||||
|
tw.Style().Format.Header = text.FormatLower
|
||||||
|
tw.Style().Format.Row = text.FormatLower
|
||||||
|
tw.Style().Format.Footer = text.FormatLower
|
||||||
|
tw.Style().Options.SeparateColumns = false
|
||||||
|
// render it
|
||||||
|
fmt.Printf("Table with customizations:\n%s", tw.Render())
|
||||||
|
|
||||||
|
// Output: Table with customizations:
|
||||||
|
// ┌──────────────────────────────────────────────────────────────────────┐
|
||||||
|
// │ first name last name salary │
|
||||||
|
// ├──────────────────────────────────────────────────────────────────────┤
|
||||||
|
// │ 1 arya stark 3000 a girl has no name. │
|
||||||
|
// │ 2 sansa stark 4000 │
|
||||||
|
// │ 3 jon snow 2000 you know nothing, jon snow! │
|
||||||
|
// │ 4 jaime lannister 5000 │
|
||||||
|
// │ 5 tyrion lannister 5000 a lannister always pays his debts. │
|
||||||
|
// ├──────────────────────────────────────────────────────────────────────┤
|
||||||
|
// │ total 10000 │
|
||||||
|
// └──────────────────────────────────────────────────────────────────────┘
|
||||||
|
}
|
137
helper/text/align.go
Normal file
137
helper/text/align.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Align denotes how text is to be aligned horizontally.
|
||||||
|
type Align int
|
||||||
|
|
||||||
|
// Align enumerations
|
||||||
|
const (
|
||||||
|
AlignDefault Align = iota // same as AlignLeft
|
||||||
|
AlignLeft // "left "
|
||||||
|
AlignCenter // " center "
|
||||||
|
AlignJustify // "justify it"
|
||||||
|
AlignRight // " right"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Apply aligns the text as directed. For ex.:
|
||||||
|
// * AlignDefault.Apply("Jon Snow", 12) returns "Jon Snow "
|
||||||
|
// * AlignLeft.Apply("Jon Snow", 12) returns "Jon Snow "
|
||||||
|
// * AlignCenter.Apply("Jon Snow", 12) returns " Jon Snow "
|
||||||
|
// * AlignJustify.Apply("Jon Snow", 12) returns "Jon Snow"
|
||||||
|
// * AlignRight.Apply("Jon Snow", 12) returns " Jon Snow"
|
||||||
|
func (a Align) Apply(text string, maxLength int) string {
|
||||||
|
text = a.trimString(text)
|
||||||
|
sLen := utf8.RuneCountInString(text)
|
||||||
|
sLenWoE := RuneWidthWithoutEscSequences(text)
|
||||||
|
numEscChars := sLen - sLenWoE
|
||||||
|
|
||||||
|
// now, align the text
|
||||||
|
switch a {
|
||||||
|
case AlignDefault, AlignLeft:
|
||||||
|
return fmt.Sprintf("%-"+strconv.Itoa(maxLength+numEscChars)+"s", text)
|
||||||
|
case AlignCenter:
|
||||||
|
if sLenWoE < maxLength {
|
||||||
|
// left pad with half the number of spaces needed before using %text
|
||||||
|
return fmt.Sprintf("%"+strconv.Itoa(maxLength+numEscChars)+"s",
|
||||||
|
text+strings.Repeat(" ", int((maxLength-sLenWoE)/2)))
|
||||||
|
}
|
||||||
|
case AlignJustify:
|
||||||
|
return a.justifyText(text, sLenWoE, maxLength)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%"+strconv.Itoa(maxLength+numEscChars)+"s", text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTMLProperty returns the equivalent HTML horizontal-align tag property.
|
||||||
|
func (a Align) HTMLProperty() string {
|
||||||
|
switch a {
|
||||||
|
case AlignLeft:
|
||||||
|
return "align=\"left\""
|
||||||
|
case AlignCenter:
|
||||||
|
return "align=\"center\""
|
||||||
|
case AlignJustify:
|
||||||
|
return "align=\"justify\""
|
||||||
|
case AlignRight:
|
||||||
|
return "align=\"right\""
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkdownProperty returns the equivalent Markdown horizontal-align separator.
|
||||||
|
func (a Align) MarkdownProperty() string {
|
||||||
|
switch a {
|
||||||
|
case AlignLeft:
|
||||||
|
return ":--- "
|
||||||
|
case AlignCenter:
|
||||||
|
return ":---:"
|
||||||
|
case AlignRight:
|
||||||
|
return " ---:"
|
||||||
|
default:
|
||||||
|
return " --- "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Align) justifyText(text string, textLength int, maxLength int) string {
|
||||||
|
// split the text into individual words
|
||||||
|
wordsUnfiltered := strings.Split(text, " ")
|
||||||
|
words := Filter(wordsUnfiltered, func(item string) bool {
|
||||||
|
return item != ""
|
||||||
|
})
|
||||||
|
// empty string implies spaces for maxLength
|
||||||
|
if len(words) == 0 {
|
||||||
|
return strings.Repeat(" ", maxLength)
|
||||||
|
}
|
||||||
|
// get the number of spaces to insert into the text
|
||||||
|
numSpacesNeeded := maxLength - textLength + strings.Count(text, " ")
|
||||||
|
numSpacesNeededBetweenWords := 0
|
||||||
|
if len(words) > 1 {
|
||||||
|
numSpacesNeededBetweenWords = numSpacesNeeded / (len(words) - 1)
|
||||||
|
}
|
||||||
|
// create the output string word by word with spaces in between
|
||||||
|
var outText strings.Builder
|
||||||
|
outText.Grow(maxLength)
|
||||||
|
for idx, word := range words {
|
||||||
|
if idx > 0 {
|
||||||
|
// insert spaces only after the first word
|
||||||
|
if idx == len(words)-1 {
|
||||||
|
// insert all the remaining space before the last word
|
||||||
|
outText.WriteString(strings.Repeat(" ", numSpacesNeeded))
|
||||||
|
numSpacesNeeded = 0
|
||||||
|
} else {
|
||||||
|
// insert the determined number of spaces between each word
|
||||||
|
outText.WriteString(strings.Repeat(" ", numSpacesNeededBetweenWords))
|
||||||
|
// and reduce the number of spaces needed after this
|
||||||
|
numSpacesNeeded -= numSpacesNeededBetweenWords
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outText.WriteString(word)
|
||||||
|
if idx == len(words)-1 && numSpacesNeeded > 0 {
|
||||||
|
outText.WriteString(strings.Repeat(" ", numSpacesNeeded))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outText.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Align) trimString(text string) string {
|
||||||
|
switch a {
|
||||||
|
case AlignDefault, AlignLeft:
|
||||||
|
if strings.HasSuffix(text, " ") {
|
||||||
|
return strings.TrimRight(text, " ")
|
||||||
|
}
|
||||||
|
case AlignRight:
|
||||||
|
if strings.HasPrefix(text, " ") {
|
||||||
|
return strings.TrimLeft(text, " ")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if strings.HasPrefix(text, " ") || strings.HasSuffix(text, " ") {
|
||||||
|
return strings.Trim(text, " ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
138
helper/text/align_test.go
Normal file
138
helper/text/align_test.go
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleAlign_Apply() {
|
||||||
|
fmt.Printf("AlignDefault: '%s'\n", AlignDefault.Apply("Jon Snow", 12))
|
||||||
|
fmt.Printf("AlignLeft : '%s'\n", AlignLeft.Apply("Jon Snow", 12))
|
||||||
|
fmt.Printf("AlignCenter : '%s'\n", AlignCenter.Apply("Jon Snow", 12))
|
||||||
|
fmt.Printf("AlignJustify: '%s'\n", AlignJustify.Apply("Jon Snow", 12))
|
||||||
|
fmt.Printf("AlignRight : '%s'\n", AlignRight.Apply("Jon Snow", 12))
|
||||||
|
|
||||||
|
// Output: AlignDefault: 'Jon Snow '
|
||||||
|
// AlignLeft : 'Jon Snow '
|
||||||
|
// AlignCenter : ' Jon Snow '
|
||||||
|
// AlignJustify: 'Jon Snow'
|
||||||
|
// AlignRight : ' Jon Snow'
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlign_Apply(t *testing.T) {
|
||||||
|
// AlignDefault & AlignLeft are the same
|
||||||
|
assert.Equal(t, "Jon Snow ", AlignDefault.Apply("Jon Snow", 12))
|
||||||
|
assert.Equal(t, " Jon Snow ", AlignDefault.Apply(" Jon Snow", 12))
|
||||||
|
assert.Equal(t, " ", AlignDefault.Apply("", 12))
|
||||||
|
assert.Equal(t, "Jon Snow ", AlignLeft.Apply("Jon Snow ", 12))
|
||||||
|
assert.Equal(t, " Jon Snow ", AlignLeft.Apply(" Jon Snow ", 12))
|
||||||
|
assert.Equal(t, " ", AlignLeft.Apply("", 12))
|
||||||
|
|
||||||
|
// AlignCenter
|
||||||
|
assert.Equal(t, " Jon Snow ", AlignCenter.Apply("Jon Snow ", 12))
|
||||||
|
assert.Equal(t, " Jon Snow ", AlignCenter.Apply(" Jon Snow", 12))
|
||||||
|
assert.Equal(t, " Jon Snow ", AlignCenter.Apply(" Jon Snow ", 12))
|
||||||
|
assert.Equal(t, " ", AlignCenter.Apply("", 12))
|
||||||
|
|
||||||
|
// AlignJustify
|
||||||
|
assert.Equal(t, "Jon Snow", AlignJustify.Apply("Jon Snow", 12))
|
||||||
|
assert.Equal(t, "JS vs. DT", AlignJustify.Apply("JS vs. DT", 12))
|
||||||
|
assert.Equal(t, "JS is AT", AlignJustify.Apply("JS is AT", 12))
|
||||||
|
assert.Equal(t, "JS is AT", AlignJustify.Apply("JS is AT", 12))
|
||||||
|
assert.Equal(t, "JonSnow ", AlignJustify.Apply("JonSnow", 12))
|
||||||
|
assert.Equal(t, "JonSnow ", AlignJustify.Apply(" JonSnow", 12))
|
||||||
|
assert.Equal(t, " ", AlignJustify.Apply("", 12))
|
||||||
|
|
||||||
|
// Align Right
|
||||||
|
assert.Equal(t, " Jon Snow", AlignRight.Apply("Jon Snow", 12))
|
||||||
|
assert.Equal(t, " Jon Snow ", AlignRight.Apply("Jon Snow ", 12))
|
||||||
|
assert.Equal(t, " Jon Snow ", AlignRight.Apply(" Jon Snow ", 12))
|
||||||
|
assert.Equal(t, " ", AlignRight.Apply("", 12))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlign_Apply_ColoredText(t *testing.T) {
|
||||||
|
// AlignDefault & AlignLeft are the same
|
||||||
|
assert.Equal(t, "\x1b[33mJon Snow\x1b[0m ", AlignDefault.Apply("\x1b[33mJon Snow\x1b[0m", 12))
|
||||||
|
assert.Equal(t, "\x1b[33m Jon Snow\x1b[0m ", AlignDefault.Apply("\x1b[33m Jon Snow\x1b[0m", 12))
|
||||||
|
assert.Equal(t, "\x1b[33m\x1b[0m ", AlignDefault.Apply("\x1b[33m\x1b[0m", 12))
|
||||||
|
assert.Equal(t, "\x1b[33mJon Snow \x1b[0m ", AlignLeft.Apply("\x1b[33mJon Snow \x1b[0m", 12))
|
||||||
|
assert.Equal(t, "\x1b[33m Jon Snow \x1b[0m ", AlignLeft.Apply("\x1b[33m Jon Snow \x1b[0m", 12))
|
||||||
|
assert.Equal(t, "\x1b[33m\x1b[0m ", AlignLeft.Apply("\x1b[33m\x1b[0m", 12))
|
||||||
|
|
||||||
|
// AlignCenter
|
||||||
|
assert.Equal(t, " \x1b[33mJon Snow \x1b[0m ", AlignCenter.Apply("\x1b[33mJon Snow \x1b[0m", 12))
|
||||||
|
assert.Equal(t, " \x1b[33m Jon Snow\x1b[0m ", AlignCenter.Apply("\x1b[33m Jon Snow\x1b[0m", 12))
|
||||||
|
assert.Equal(t, " \x1b[33m Jon Snow \x1b[0m", AlignCenter.Apply("\x1b[33m Jon Snow \x1b[0m", 12))
|
||||||
|
assert.Equal(t, " \x1b[33m\x1b[0m ", AlignCenter.Apply("\x1b[33m\x1b[0m", 12))
|
||||||
|
|
||||||
|
// AlignJustify
|
||||||
|
assert.Equal(t, "\x1b[33mJon Snow\x1b[0m", AlignJustify.Apply("\x1b[33mJon Snow\x1b[0m", 12))
|
||||||
|
assert.Equal(t, "\x1b[33mJS vs. DT\x1b[0m", AlignJustify.Apply("\x1b[33mJS vs. DT\x1b[0m", 12))
|
||||||
|
assert.Equal(t, "\x1b[33mJS is AT\x1b[0m", AlignJustify.Apply("\x1b[33mJS is AT\x1b[0m", 12))
|
||||||
|
assert.Equal(t, "\x1b[33mJS is AT\x1b[0m", AlignJustify.Apply("\x1b[33mJS is AT\x1b[0m", 12))
|
||||||
|
assert.Equal(t, "\x1b[33mJonSnow\x1b[0m ", AlignJustify.Apply("\x1b[33mJonSnow\x1b[0m", 12))
|
||||||
|
assert.Equal(t, "\x1b[33m JonSnow\x1b[0m", AlignJustify.Apply("\x1b[33m JonSnow\x1b[0m", 12))
|
||||||
|
assert.Equal(t, "\x1b[33m\x1b[0m ", AlignJustify.Apply("\x1b[33m\x1b[0m", 12))
|
||||||
|
|
||||||
|
// Align Right
|
||||||
|
assert.Equal(t, " \x1b[33mJon Snow\x1b[0m", AlignRight.Apply("\x1b[33mJon Snow\x1b[0m", 12))
|
||||||
|
assert.Equal(t, " \x1b[33mJon Snow \x1b[0m", AlignRight.Apply("\x1b[33mJon Snow \x1b[0m", 12))
|
||||||
|
assert.Equal(t, " \x1b[33m Jon Snow \x1b[0m", AlignRight.Apply("\x1b[33m Jon Snow \x1b[0m", 12))
|
||||||
|
assert.Equal(t, " \x1b[33m\x1b[0m", AlignRight.Apply("\x1b[33m\x1b[0m", 12))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleAlign_HTMLProperty() {
|
||||||
|
fmt.Printf("AlignDefault: '%s'\n", AlignDefault.HTMLProperty())
|
||||||
|
fmt.Printf("AlignLeft : '%s'\n", AlignLeft.HTMLProperty())
|
||||||
|
fmt.Printf("AlignCenter : '%s'\n", AlignCenter.HTMLProperty())
|
||||||
|
fmt.Printf("AlignJustify: '%s'\n", AlignJustify.HTMLProperty())
|
||||||
|
fmt.Printf("AlignRight : '%s'\n", AlignRight.HTMLProperty())
|
||||||
|
|
||||||
|
// Output: AlignDefault: ''
|
||||||
|
// AlignLeft : 'align="left"'
|
||||||
|
// AlignCenter : 'align="center"'
|
||||||
|
// AlignJustify: 'align="justify"'
|
||||||
|
// AlignRight : 'align="right"'
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlign_HTMLProperty(t *testing.T) {
|
||||||
|
aligns := map[Align]string{
|
||||||
|
AlignDefault: "",
|
||||||
|
AlignLeft: "left",
|
||||||
|
AlignCenter: "center",
|
||||||
|
AlignJustify: "justify",
|
||||||
|
AlignRight: "right",
|
||||||
|
}
|
||||||
|
for align, htmlStyle := range aligns {
|
||||||
|
assert.Contains(t, align.HTMLProperty(), htmlStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleAlign_MarkdownProperty() {
|
||||||
|
fmt.Printf("AlignDefault: '%s'\n", AlignDefault.MarkdownProperty())
|
||||||
|
fmt.Printf("AlignLeft : '%s'\n", AlignLeft.MarkdownProperty())
|
||||||
|
fmt.Printf("AlignCenter : '%s'\n", AlignCenter.MarkdownProperty())
|
||||||
|
fmt.Printf("AlignJustify: '%s'\n", AlignJustify.MarkdownProperty())
|
||||||
|
fmt.Printf("AlignRight : '%s'\n", AlignRight.MarkdownProperty())
|
||||||
|
|
||||||
|
// Output: AlignDefault: ' --- '
|
||||||
|
// AlignLeft : ':--- '
|
||||||
|
// AlignCenter : ':---:'
|
||||||
|
// AlignJustify: ' --- '
|
||||||
|
// AlignRight : ' ---:'
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlign_MarkdownProperty(t *testing.T) {
|
||||||
|
aligns := map[Align]string{
|
||||||
|
AlignDefault: " --- ",
|
||||||
|
AlignLeft: ":--- ",
|
||||||
|
AlignCenter: ":---:",
|
||||||
|
AlignJustify: " --- ",
|
||||||
|
AlignRight: " ---:",
|
||||||
|
}
|
||||||
|
for align, markdownSeparator := range aligns {
|
||||||
|
assert.Contains(t, align.MarkdownProperty(), markdownSeparator)
|
||||||
|
}
|
||||||
|
}
|
55
helper/text/ansi.go
Normal file
55
helper/text/ansi.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// ANSICodesSupported will be true on consoles where ANSI Escape Codes/Sequences
|
||||||
|
// are supported.
|
||||||
|
var ANSICodesSupported = areANSICodesSupported()
|
||||||
|
|
||||||
|
// Escape encodes the string with the ANSI Escape Sequence.
|
||||||
|
// For ex.:
|
||||||
|
// Escape("Ghost", "") == "Ghost"
|
||||||
|
// Escape("Ghost", "\x1b[91m") == "\x1b[91mGhost\x1b[0m"
|
||||||
|
// Escape("\x1b[94mGhost\x1b[0mLady", "\x1b[91m") == "\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m"
|
||||||
|
// Escape("Nymeria\x1b[94mGhost\x1b[0mLady", "\x1b[91m") == "\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m"
|
||||||
|
// Escape("Nymeria \x1b[94mGhost\x1b[0m Lady", "\x1b[91m") == "\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m"
|
||||||
|
func Escape(str string, escapeSeq string) string {
|
||||||
|
out := ""
|
||||||
|
if !strings.HasPrefix(str, EscapeStart) {
|
||||||
|
out += escapeSeq
|
||||||
|
}
|
||||||
|
out += strings.Replace(str, EscapeReset, EscapeReset+escapeSeq, -1)
|
||||||
|
if !strings.HasSuffix(out, EscapeReset) {
|
||||||
|
out += EscapeReset
|
||||||
|
}
|
||||||
|
if strings.Contains(out, escapeSeq+EscapeReset) {
|
||||||
|
out = strings.Replace(out, escapeSeq+EscapeReset, "", -1)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// StripEscape strips all ANSI Escape Sequence from the string.
|
||||||
|
// For ex.:
|
||||||
|
// StripEscape("Ghost") == "Ghost"
|
||||||
|
// StripEscape("\x1b[91mGhost\x1b[0m") == "Ghost"
|
||||||
|
// StripEscape("\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m") == "GhostLady"
|
||||||
|
// StripEscape("\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m") == "NymeriaGhostLady"
|
||||||
|
// StripEscape("\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m") == "Nymeria Ghost Lady"
|
||||||
|
func StripEscape(str string) string {
|
||||||
|
var out strings.Builder
|
||||||
|
out.Grow(RuneWidthWithoutEscSequences(str))
|
||||||
|
|
||||||
|
isEscSeq := false
|
||||||
|
for _, sChr := range str {
|
||||||
|
if sChr == EscapeStartRune {
|
||||||
|
isEscSeq = true
|
||||||
|
}
|
||||||
|
if !isEscSeq {
|
||||||
|
out.WriteRune(sChr)
|
||||||
|
}
|
||||||
|
if isEscSeq && sChr == EscapeStopRune {
|
||||||
|
isEscSeq = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out.String()
|
||||||
|
}
|
50
helper/text/ansi_test.go
Normal file
50
helper/text/ansi_test.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEscape(t *testing.T) {
|
||||||
|
assert.Equal(t, "\x1b[91mGhost\x1b[0m", Escape("Ghost", FgHiRed.EscapeSeq()))
|
||||||
|
assert.Equal(t, "\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m", Escape(FgHiBlue.Sprint("Ghost")+"Lady", FgHiRed.EscapeSeq()))
|
||||||
|
assert.Equal(t, "\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m", Escape("Nymeria"+FgHiBlue.Sprint("Ghost")+"Lady", FgHiRed.EscapeSeq()))
|
||||||
|
assert.Equal(t, "\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m", Escape("Nymeria "+FgHiBlue.Sprint("Ghost")+" Lady", FgHiRed.EscapeSeq()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleEscape() {
|
||||||
|
fmt.Printf("Escape(%#v, %#v) == %#v\n", "Ghost", "", Escape("Ghost", ""))
|
||||||
|
fmt.Printf("Escape(%#v, %#v) == %#v\n", "Ghost", FgHiRed.EscapeSeq(), Escape("Ghost", FgHiRed.EscapeSeq()))
|
||||||
|
fmt.Printf("Escape(%#v, %#v) == %#v\n", FgHiBlue.Sprint("Ghost")+"Lady", FgHiRed.EscapeSeq(), Escape(FgHiBlue.Sprint("Ghost")+"Lady", FgHiRed.EscapeSeq()))
|
||||||
|
fmt.Printf("Escape(%#v, %#v) == %#v\n", "Nymeria"+FgHiBlue.Sprint("Ghost")+"Lady", FgHiRed.EscapeSeq(), Escape("Nymeria"+FgHiBlue.Sprint("Ghost")+"Lady", FgHiRed.EscapeSeq()))
|
||||||
|
fmt.Printf("Escape(%#v, %#v) == %#v\n", "Nymeria "+FgHiBlue.Sprint("Ghost")+" Lady", FgHiRed.EscapeSeq(), Escape("Nymeria "+FgHiBlue.Sprint("Ghost")+" Lady", FgHiRed.EscapeSeq()))
|
||||||
|
|
||||||
|
// Output: Escape("Ghost", "") == "Ghost"
|
||||||
|
// Escape("Ghost", "\x1b[91m") == "\x1b[91mGhost\x1b[0m"
|
||||||
|
// Escape("\x1b[94mGhost\x1b[0mLady", "\x1b[91m") == "\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m"
|
||||||
|
// Escape("Nymeria\x1b[94mGhost\x1b[0mLady", "\x1b[91m") == "\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m"
|
||||||
|
// Escape("Nymeria \x1b[94mGhost\x1b[0m Lady", "\x1b[91m") == "\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStripEscape(t *testing.T) {
|
||||||
|
assert.Equal(t, "Ghost", StripEscape(FgHiRed.Sprint("Ghost")))
|
||||||
|
assert.Equal(t, "GhostLady", StripEscape(FgHiBlue.Sprint("Ghost")+"Lady"))
|
||||||
|
assert.Equal(t, "NymeriaGhostLady", StripEscape("Nymeria"+FgHiBlue.Sprint("Ghost")+"Lady"))
|
||||||
|
assert.Equal(t, "Nymeria Ghost Lady", StripEscape("Nymeria "+FgHiBlue.Sprint("Ghost")+" Lady"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleStripEscape() {
|
||||||
|
fmt.Printf("StripEscape(%#v) == %#v\n", "Ghost", StripEscape("Ghost"))
|
||||||
|
fmt.Printf("StripEscape(%#v) == %#v\n", "\x1b[91mGhost\x1b[0m", StripEscape("\x1b[91mGhost\x1b[0m"))
|
||||||
|
fmt.Printf("StripEscape(%#v) == %#v\n", "\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m", StripEscape("\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m"))
|
||||||
|
fmt.Printf("StripEscape(%#v) == %#v\n", "\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m", StripEscape("\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m"))
|
||||||
|
fmt.Printf("StripEscape(%#v) == %#v\n", "\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m", StripEscape("\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m"))
|
||||||
|
|
||||||
|
// Output: StripEscape("Ghost") == "Ghost"
|
||||||
|
// StripEscape("\x1b[91mGhost\x1b[0m") == "Ghost"
|
||||||
|
// StripEscape("\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m") == "GhostLady"
|
||||||
|
// StripEscape("\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m") == "NymeriaGhostLady"
|
||||||
|
// StripEscape("\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m") == "Nymeria Ghost Lady"
|
||||||
|
}
|
8
helper/text/ansi_unix.go
Normal file
8
helper/text/ansi_unix.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package text
|
||||||
|
|
||||||
|
func areANSICodesSupported() bool {
|
||||||
|
return true
|
||||||
|
}
|
32
helper/text/ansi_windows.go
Normal file
32
helper/text/ansi_windows.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
enableVTPMutex = sync.Mutex{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func areANSICodesSupported() bool {
|
||||||
|
enableVTPMutex.Lock()
|
||||||
|
defer enableVTPMutex.Unlock()
|
||||||
|
|
||||||
|
outHandle := windows.Handle(os.Stdout.Fd())
|
||||||
|
var outMode uint32
|
||||||
|
if err := windows.GetConsoleMode(outHandle, &outMode); err == nil {
|
||||||
|
if outMode&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if err := windows.SetConsoleMode(outHandle, outMode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
183
helper/text/color.go
Normal file
183
helper/text/color.go
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
colorsEnabled = areANSICodesSupported()
|
||||||
|
)
|
||||||
|
|
||||||
|
// DisableColors (forcefully) disables color coding globally.
|
||||||
|
func DisableColors() {
|
||||||
|
colorsEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableColors (forcefully) enables color coding globally.
|
||||||
|
func EnableColors() {
|
||||||
|
colorsEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// The logic here is inspired from github.com/fatih/color; the following is
|
||||||
|
// the the bare minimum logic required to print Colored to the console.
|
||||||
|
// The differences:
|
||||||
|
// * This one caches the escape sequences for cases with multiple colors
|
||||||
|
// * This one handles cases where the incoming already has colors in the
|
||||||
|
// form of escape sequences; in which case, text that does not have any
|
||||||
|
// escape sequences are colored/escaped
|
||||||
|
|
||||||
|
// Color represents a single color to render with.
|
||||||
|
type Color int
|
||||||
|
|
||||||
|
// Base colors -- attributes in reality
|
||||||
|
const (
|
||||||
|
Reset Color = iota
|
||||||
|
Bold
|
||||||
|
Faint
|
||||||
|
Italic
|
||||||
|
Underline
|
||||||
|
BlinkSlow
|
||||||
|
BlinkRapid
|
||||||
|
ReverseVideo
|
||||||
|
Concealed
|
||||||
|
CrossedOut
|
||||||
|
)
|
||||||
|
|
||||||
|
// Foreground colors
|
||||||
|
const (
|
||||||
|
FgBlack Color = iota + 30
|
||||||
|
FgRed
|
||||||
|
FgGreen
|
||||||
|
FgYellow
|
||||||
|
FgBlue
|
||||||
|
FgMagenta
|
||||||
|
FgCyan
|
||||||
|
FgWhite
|
||||||
|
)
|
||||||
|
|
||||||
|
// Foreground Hi-Intensity colors
|
||||||
|
const (
|
||||||
|
FgHiBlack Color = iota + 90
|
||||||
|
FgHiRed
|
||||||
|
FgHiGreen
|
||||||
|
FgHiYellow
|
||||||
|
FgHiBlue
|
||||||
|
FgHiMagenta
|
||||||
|
FgHiCyan
|
||||||
|
FgHiWhite
|
||||||
|
)
|
||||||
|
|
||||||
|
// Background colors
|
||||||
|
const (
|
||||||
|
BgBlack Color = iota + 40
|
||||||
|
BgRed
|
||||||
|
BgGreen
|
||||||
|
BgYellow
|
||||||
|
BgBlue
|
||||||
|
BgMagenta
|
||||||
|
BgCyan
|
||||||
|
BgWhite
|
||||||
|
)
|
||||||
|
|
||||||
|
// Background Hi-Intensity colors
|
||||||
|
const (
|
||||||
|
BgHiBlack Color = iota + 100
|
||||||
|
BgHiRed
|
||||||
|
BgHiGreen
|
||||||
|
BgHiYellow
|
||||||
|
BgHiBlue
|
||||||
|
BgHiMagenta
|
||||||
|
BgHiCyan
|
||||||
|
BgHiWhite
|
||||||
|
)
|
||||||
|
|
||||||
|
// EscapeSeq returns the ANSI escape sequence for the color.
|
||||||
|
func (c Color) EscapeSeq() string {
|
||||||
|
return EscapeStart + strconv.Itoa(int(c)) + EscapeStop
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTMLProperty returns the "class" attribute for the color.
|
||||||
|
func (c Color) HTMLProperty() string {
|
||||||
|
out := ""
|
||||||
|
if class, ok := colorCSSClassMap[c]; ok {
|
||||||
|
out = fmt.Sprintf("class=\"%s\"", class)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprint colorizes and prints the given string(s).
|
||||||
|
func (c Color) Sprint(a ...interface{}) string {
|
||||||
|
return colorize(fmt.Sprint(a...), c.EscapeSeq())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprintf formats and colorizes and prints the given string(s).
|
||||||
|
func (c Color) Sprintf(format string, a ...interface{}) string {
|
||||||
|
return colorize(fmt.Sprintf(format, a...), c.EscapeSeq())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Colors represents an array of Color objects to render with.
|
||||||
|
// Example: Colors{FgCyan, BgBlack}
|
||||||
|
type Colors []Color
|
||||||
|
|
||||||
|
var (
|
||||||
|
// colorsSeqMap caches the escape sequence for a set of colors
|
||||||
|
colorsSeqMap = sync.Map{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// EscapeSeq returns the ANSI escape sequence for the colors set.
|
||||||
|
func (c Colors) EscapeSeq() string {
|
||||||
|
if len(c) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
colorsKey := fmt.Sprintf("%#v", c)
|
||||||
|
escapeSeq, ok := colorsSeqMap.Load(colorsKey)
|
||||||
|
if !ok || escapeSeq == "" {
|
||||||
|
colorNums := make([]string, len(c))
|
||||||
|
for idx, color := range c {
|
||||||
|
colorNums[idx] = strconv.Itoa(int(color))
|
||||||
|
}
|
||||||
|
escapeSeq = EscapeStart + strings.Join(colorNums, ";") + EscapeStop
|
||||||
|
colorsSeqMap.Store(colorsKey, escapeSeq)
|
||||||
|
}
|
||||||
|
return escapeSeq.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTMLProperty returns the "class" attribute for the colors.
|
||||||
|
func (c Colors) HTMLProperty() string {
|
||||||
|
if len(c) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var classes []string
|
||||||
|
for _, color := range c {
|
||||||
|
if class, ok := colorCSSClassMap[color]; ok {
|
||||||
|
classes = append(classes, class)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(classes) > 1 {
|
||||||
|
sort.Strings(classes)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("class=\"%s\"", strings.Join(classes, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprint colorizes and prints the given string(s).
|
||||||
|
func (c Colors) Sprint(a ...interface{}) string {
|
||||||
|
return colorize(fmt.Sprint(a...), c.EscapeSeq())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprintf formats and colorizes and prints the given string(s).
|
||||||
|
func (c Colors) Sprintf(format string, a ...interface{}) string {
|
||||||
|
return colorize(fmt.Sprintf(format, a...), c.EscapeSeq())
|
||||||
|
}
|
||||||
|
|
||||||
|
func colorize(s string, escapeSeq string) string {
|
||||||
|
if !colorsEnabled || escapeSeq == "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return Escape(s, escapeSeq)
|
||||||
|
}
|
48
helper/text/color_html.go
Normal file
48
helper/text/color_html.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
var (
|
||||||
|
// colorCSSClassMap contains the equivalent CSS-class for all colors
|
||||||
|
colorCSSClassMap = map[Color]string{
|
||||||
|
Bold: "bold",
|
||||||
|
Faint: "faint",
|
||||||
|
Italic: "italic",
|
||||||
|
Underline: "underline",
|
||||||
|
BlinkSlow: "blink-slow",
|
||||||
|
BlinkRapid: "blink-rapid",
|
||||||
|
ReverseVideo: "reverse-video",
|
||||||
|
Concealed: "concealed",
|
||||||
|
CrossedOut: "crossed-out",
|
||||||
|
FgBlack: "fg-black",
|
||||||
|
FgRed: "fg-red",
|
||||||
|
FgGreen: "fg-green",
|
||||||
|
FgYellow: "fg-yellow",
|
||||||
|
FgBlue: "fg-blue",
|
||||||
|
FgMagenta: "fg-magenta",
|
||||||
|
FgCyan: "fg-cyan",
|
||||||
|
FgWhite: "fg-white",
|
||||||
|
FgHiBlack: "fg-hi-black",
|
||||||
|
FgHiRed: "fg-hi-red",
|
||||||
|
FgHiGreen: "fg-hi-green",
|
||||||
|
FgHiYellow: "fg-hi-yellow",
|
||||||
|
FgHiBlue: "fg-hi-blue",
|
||||||
|
FgHiMagenta: "fg-hi-magenta",
|
||||||
|
FgHiCyan: "fg-hi-cyan",
|
||||||
|
FgHiWhite: "fg-hi-white",
|
||||||
|
BgBlack: "bg-black",
|
||||||
|
BgRed: "bg-red",
|
||||||
|
BgGreen: "bg-green",
|
||||||
|
BgYellow: "bg-yellow",
|
||||||
|
BgBlue: "bg-blue",
|
||||||
|
BgMagenta: "bg-magenta",
|
||||||
|
BgCyan: "bg-cyan",
|
||||||
|
BgWhite: "bg-white",
|
||||||
|
BgHiBlack: "bg-hi-black",
|
||||||
|
BgHiRed: "bg-hi-red",
|
||||||
|
BgHiGreen: "bg-hi-green",
|
||||||
|
BgHiYellow: "bg-hi-yellow",
|
||||||
|
BgHiBlue: "bg-hi-blue",
|
||||||
|
BgHiMagenta: "bg-hi-magenta",
|
||||||
|
BgHiCyan: "bg-hi-cyan",
|
||||||
|
BgHiWhite: "bg-hi-white",
|
||||||
|
}
|
||||||
|
)
|
158
helper/text/color_test.go
Normal file
158
helper/text/color_test.go
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
EnableColors()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColor_EnableAndDisable(t *testing.T) {
|
||||||
|
defer EnableColors()
|
||||||
|
|
||||||
|
EnableColors()
|
||||||
|
assert.Equal(t, "\x1b[31mtest\x1b[0m", FgRed.Sprint("test"))
|
||||||
|
|
||||||
|
DisableColors()
|
||||||
|
assert.Equal(t, "test", FgRed.Sprint("test"))
|
||||||
|
|
||||||
|
EnableColors()
|
||||||
|
assert.Equal(t, "\x1b[31mtest\x1b[0m", FgRed.Sprint("test"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleColor_EscapeSeq() {
|
||||||
|
fmt.Printf("Black Background: %#v\n", BgBlack.EscapeSeq())
|
||||||
|
fmt.Printf("Black Foreground: %#v\n", FgBlack.EscapeSeq())
|
||||||
|
|
||||||
|
// Output: Black Background: "\x1b[40m"
|
||||||
|
// Black Foreground: "\x1b[30m"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColor_EscapeSeq(t *testing.T) {
|
||||||
|
assert.Equal(t, "\x1b[40m", BgBlack.EscapeSeq())
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleColor_HTMLProperty() {
|
||||||
|
fmt.Printf("Bold: %#v\n", Bold.HTMLProperty())
|
||||||
|
fmt.Printf("Black Background: %#v\n", BgBlack.HTMLProperty())
|
||||||
|
fmt.Printf("Black Foreground: %#v\n", FgBlack.HTMLProperty())
|
||||||
|
|
||||||
|
// Output: Bold: "class=\"bold\""
|
||||||
|
// Black Background: "class=\"bg-black\""
|
||||||
|
// Black Foreground: "class=\"fg-black\""
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColor_HTMLProperty(t *testing.T) {
|
||||||
|
assert.Equal(t, "class=\"bold\"", Bold.HTMLProperty())
|
||||||
|
assert.Equal(t, "class=\"bg-black\"", BgBlack.HTMLProperty())
|
||||||
|
assert.Equal(t, "class=\"fg-black\"", FgBlack.HTMLProperty())
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleColor_Sprint() {
|
||||||
|
fmt.Printf("%#v\n", BgBlack.Sprint("Black Background"))
|
||||||
|
fmt.Printf("%#v\n", FgBlack.Sprint("Black Foreground"))
|
||||||
|
|
||||||
|
// Output: "\x1b[40mBlack Background\x1b[0m"
|
||||||
|
// "\x1b[30mBlack Foreground\x1b[0m"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColor_Sprint(t *testing.T) {
|
||||||
|
assert.Equal(t, "\x1b[31mtest true\x1b[0m", FgRed.Sprint("test ", true))
|
||||||
|
|
||||||
|
assert.Equal(t, "\x1b[32mtest\x1b[0m\x1b[31mtrue\x1b[0m", FgRed.Sprint("\x1b[32mtest\x1b[0m", true))
|
||||||
|
assert.Equal(t, "\x1b[32mtest true\x1b[0m", FgRed.Sprint("\x1b[32mtest ", true))
|
||||||
|
assert.Equal(t, "\x1b[32mtest\x1b[0m\x1b[31m \x1b[0m", FgRed.Sprint("\x1b[32mtest\x1b[0m "))
|
||||||
|
assert.Equal(t, "\x1b[32mtest\x1b[0m", FgRed.Sprint("\x1b[32mtest\x1b[0m"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleColor_Sprintf() {
|
||||||
|
fmt.Printf("%#v\n", BgBlack.Sprintf("%s %s", "Black", "Background"))
|
||||||
|
fmt.Printf("%#v\n", FgBlack.Sprintf("%s %s", "Black", "Foreground"))
|
||||||
|
|
||||||
|
// Output: "\x1b[40mBlack Background\x1b[0m"
|
||||||
|
// "\x1b[30mBlack Foreground\x1b[0m"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColor_Sprintf(t *testing.T) {
|
||||||
|
assert.Equal(t, "\x1b[31mtest true\x1b[0m", FgRed.Sprintf("test %s", "true"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleColors_EscapeSeq() {
|
||||||
|
fmt.Printf("Black Background: %#v\n", Colors{BgBlack}.EscapeSeq())
|
||||||
|
fmt.Printf("Black Foreground: %#v\n", Colors{FgBlack}.EscapeSeq())
|
||||||
|
fmt.Printf("Black Background, White Foreground: %#v\n", Colors{BgBlack, FgWhite}.EscapeSeq())
|
||||||
|
fmt.Printf("Black Foreground, White Background: %#v\n", Colors{FgBlack, BgWhite}.EscapeSeq())
|
||||||
|
|
||||||
|
// Output: Black Background: "\x1b[40m"
|
||||||
|
// Black Foreground: "\x1b[30m"
|
||||||
|
// Black Background, White Foreground: "\x1b[40;37m"
|
||||||
|
// Black Foreground, White Background: "\x1b[30;47m"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColors_EscapeSeq(t *testing.T) {
|
||||||
|
assert.Equal(t, "", Colors{}.EscapeSeq())
|
||||||
|
assert.Equal(t, "\x1b[40;37m", Colors{BgBlack, FgWhite}.EscapeSeq())
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleColors_HTMLProperty() {
|
||||||
|
fmt.Printf("Black Background: %#v\n", Colors{BgBlack}.HTMLProperty())
|
||||||
|
fmt.Printf("Black Foreground: %#v\n", Colors{FgBlack}.HTMLProperty())
|
||||||
|
fmt.Printf("Black Background, White Foreground: %#v\n", Colors{BgBlack, FgWhite}.HTMLProperty())
|
||||||
|
fmt.Printf("Black Foreground, White Background: %#v\n", Colors{FgBlack, BgWhite}.HTMLProperty())
|
||||||
|
fmt.Printf("Bold Italic Underline Red Text: %#v\n", Colors{Bold, Italic, Underline, FgRed}.HTMLProperty())
|
||||||
|
|
||||||
|
// Output: Black Background: "class=\"bg-black\""
|
||||||
|
// Black Foreground: "class=\"fg-black\""
|
||||||
|
// Black Background, White Foreground: "class=\"bg-black fg-white\""
|
||||||
|
// Black Foreground, White Background: "class=\"bg-white fg-black\""
|
||||||
|
// Bold Italic Underline Red Text: "class=\"bold fg-red italic underline\""
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColors_HTMLProperty(t *testing.T) {
|
||||||
|
assert.Equal(t, "", Colors{}.HTMLProperty())
|
||||||
|
assert.Equal(t, "class=\"bg-black fg-white\"", Colors{BgBlack, FgWhite}.HTMLProperty())
|
||||||
|
assert.Equal(t, "class=\"bold fg-red\"", Colors{Bold, FgRed}.HTMLProperty())
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleColors_Sprint() {
|
||||||
|
fmt.Printf("%#v\n", Colors{BgBlack}.Sprint("Black Background"))
|
||||||
|
fmt.Printf("%#v\n", Colors{BgBlack, FgWhite}.Sprint("Black Background, White Foreground"))
|
||||||
|
fmt.Printf("%#v\n", Colors{FgBlack}.Sprint("Black Foreground"))
|
||||||
|
fmt.Printf("%#v\n", Colors{FgBlack, BgWhite}.Sprint("Black Foreground, White Background"))
|
||||||
|
|
||||||
|
// Output: "\x1b[40mBlack Background\x1b[0m"
|
||||||
|
// "\x1b[40;37mBlack Background, White Foreground\x1b[0m"
|
||||||
|
// "\x1b[30mBlack Foreground\x1b[0m"
|
||||||
|
// "\x1b[30;47mBlack Foreground, White Background\x1b[0m"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColors_Sprint(t *testing.T) {
|
||||||
|
assert.Equal(t, "test true", Colors{}.Sprint("test ", true))
|
||||||
|
assert.Equal(t, "\x1b[31mtest true\x1b[0m", Colors{FgRed}.Sprint("test ", true))
|
||||||
|
|
||||||
|
assert.Equal(t, "\x1b[32mtest\x1b[0m\x1b[31mtrue\x1b[0m", Colors{FgRed}.Sprint("\x1b[32mtest\x1b[0m", true))
|
||||||
|
assert.Equal(t, "\x1b[32mtest true\x1b[0m", Colors{FgRed}.Sprint("\x1b[32mtest ", true))
|
||||||
|
assert.Equal(t, "\x1b[32mtest\x1b[0m\x1b[31m \x1b[0m", Colors{FgRed}.Sprint("\x1b[32mtest\x1b[0m "))
|
||||||
|
assert.Equal(t, "\x1b[32mtest\x1b[0m", Colors{FgRed}.Sprint("\x1b[32mtest\x1b[0m"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleColors_Sprintf() {
|
||||||
|
fmt.Printf("%#v\n", Colors{BgBlack}.Sprintf("%s %s", "Black", "Background"))
|
||||||
|
fmt.Printf("%#v\n", Colors{BgBlack, FgWhite}.Sprintf("%s, %s", "Black Background", "White Foreground"))
|
||||||
|
fmt.Printf("%#v\n", Colors{FgBlack}.Sprintf("%s %s", "Black", "Foreground"))
|
||||||
|
fmt.Printf("%#v\n", Colors{FgBlack, BgWhite}.Sprintf("%s, %s", "Black Foreground", "White Background"))
|
||||||
|
|
||||||
|
// Output: "\x1b[40mBlack Background\x1b[0m"
|
||||||
|
// "\x1b[40;37mBlack Background, White Foreground\x1b[0m"
|
||||||
|
// "\x1b[30mBlack Foreground\x1b[0m"
|
||||||
|
// "\x1b[30;47mBlack Foreground, White Background\x1b[0m"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColors_Sprintf(t *testing.T) {
|
||||||
|
assert.Equal(t, "test true", Colors{}.Sprintf("test %s", "true"))
|
||||||
|
assert.Equal(t, "\x1b[31mtest true\x1b[0m", Colors{FgRed}.Sprintf("test %s", "true"))
|
||||||
|
}
|
39
helper/text/cursor.go
Normal file
39
helper/text/cursor.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cursor helps move the cursor on the console in multiple directions.
|
||||||
|
type Cursor rune
|
||||||
|
|
||||||
|
const (
|
||||||
|
// CursorDown helps move the Cursor Down X lines
|
||||||
|
CursorDown Cursor = 'B'
|
||||||
|
|
||||||
|
// CursorLeft helps move the Cursor Left X characters
|
||||||
|
CursorLeft Cursor = 'D'
|
||||||
|
|
||||||
|
// CursorRight helps move the Cursor Right X characters
|
||||||
|
CursorRight Cursor = 'C'
|
||||||
|
|
||||||
|
// CursorUp helps move the Cursor Up X lines
|
||||||
|
CursorUp Cursor = 'A'
|
||||||
|
|
||||||
|
// EraseLine helps erase all characters to the Right of the Cursor in the
|
||||||
|
// current line
|
||||||
|
EraseLine Cursor = 'K'
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sprint prints the Escape Sequence to move the Cursor once.
|
||||||
|
func (c Cursor) Sprint() string {
|
||||||
|
return fmt.Sprintf("%s%c", EscapeStart, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprintn prints the Escape Sequence to move the Cursor "n" times.
|
||||||
|
func (c Cursor) Sprintn(n int) string {
|
||||||
|
if c == EraseLine {
|
||||||
|
return c.Sprint()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s%d%c", EscapeStart, n, c)
|
||||||
|
}
|
52
helper/text/cursor_test.go
Normal file
52
helper/text/cursor_test.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleCursor_Sprint() {
|
||||||
|
fmt.Printf("CursorDown : %#v\n", CursorDown.Sprint())
|
||||||
|
fmt.Printf("CursorLeft : %#v\n", CursorLeft.Sprint())
|
||||||
|
fmt.Printf("CursorRight: %#v\n", CursorRight.Sprint())
|
||||||
|
fmt.Printf("CursorUp : %#v\n", CursorUp.Sprint())
|
||||||
|
fmt.Printf("EraseLine : %#v\n", EraseLine.Sprint())
|
||||||
|
|
||||||
|
// Output: CursorDown : "\x1b[B"
|
||||||
|
// CursorLeft : "\x1b[D"
|
||||||
|
// CursorRight: "\x1b[C"
|
||||||
|
// CursorUp : "\x1b[A"
|
||||||
|
// EraseLine : "\x1b[K"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCursor_Sprint(t *testing.T) {
|
||||||
|
assert.Equal(t, "\x1b[B", CursorDown.Sprint())
|
||||||
|
assert.Equal(t, "\x1b[D", CursorLeft.Sprint())
|
||||||
|
assert.Equal(t, "\x1b[C", CursorRight.Sprint())
|
||||||
|
assert.Equal(t, "\x1b[A", CursorUp.Sprint())
|
||||||
|
assert.Equal(t, "\x1b[K", EraseLine.Sprint())
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleCursor_Sprintn() {
|
||||||
|
fmt.Printf("CursorDown : %#v\n", CursorDown.Sprintn(5))
|
||||||
|
fmt.Printf("CursorLeft : %#v\n", CursorLeft.Sprintn(5))
|
||||||
|
fmt.Printf("CursorRight: %#v\n", CursorRight.Sprintn(5))
|
||||||
|
fmt.Printf("CursorUp : %#v\n", CursorUp.Sprintn(5))
|
||||||
|
fmt.Printf("EraseLine : %#v\n", EraseLine.Sprintn(5))
|
||||||
|
|
||||||
|
// Output: CursorDown : "\x1b[5B"
|
||||||
|
// CursorLeft : "\x1b[5D"
|
||||||
|
// CursorRight: "\x1b[5C"
|
||||||
|
// CursorUp : "\x1b[5A"
|
||||||
|
// EraseLine : "\x1b[K"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCursor_Sprintn(t *testing.T) {
|
||||||
|
assert.Equal(t, "\x1b[5B", CursorDown.Sprintn(5))
|
||||||
|
assert.Equal(t, "\x1b[5D", CursorLeft.Sprintn(5))
|
||||||
|
assert.Equal(t, "\x1b[5C", CursorRight.Sprintn(5))
|
||||||
|
assert.Equal(t, "\x1b[5A", CursorUp.Sprintn(5))
|
||||||
|
assert.Equal(t, "\x1b[K", EraseLine.Sprintn(5))
|
||||||
|
}
|
24
helper/text/direction.go
Normal file
24
helper/text/direction.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
// Direction defines the overall flow of text. Similar to bidi.Direction, but
|
||||||
|
// simplified and specific to this package.
|
||||||
|
type Direction int
|
||||||
|
|
||||||
|
// Available Directions.
|
||||||
|
const (
|
||||||
|
Default Direction = iota
|
||||||
|
LeftToRight
|
||||||
|
RightToLeft
|
||||||
|
)
|
||||||
|
|
||||||
|
// Modifier returns a character to force the given direction for the text that
|
||||||
|
// follows the modifier.
|
||||||
|
func (d Direction) Modifier() string {
|
||||||
|
switch d {
|
||||||
|
case LeftToRight:
|
||||||
|
return "\u202a"
|
||||||
|
case RightToLeft:
|
||||||
|
return "\u202b"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
13
helper/text/direction_test.go
Normal file
13
helper/text/direction_test.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDirection_Modifier(t *testing.T) {
|
||||||
|
assert.Equal(t, "", Default.Modifier())
|
||||||
|
assert.Equal(t, "\u202a", LeftToRight.Modifier())
|
||||||
|
assert.Equal(t, "\u202b", RightToLeft.Modifier())
|
||||||
|
}
|
51
helper/text/escape.go
Normal file
51
helper/text/escape.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const (
|
||||||
|
CSIStartRune = rune(91) // [
|
||||||
|
CSIStopRune = 'm'
|
||||||
|
EscapeReset = EscapeStart + "0" + EscapeStop
|
||||||
|
EscapeStart = "\x1b["
|
||||||
|
EscapeStartRune = rune(27) // \x1b
|
||||||
|
EscapeStop = "m"
|
||||||
|
EscapeStopRune = 'm'
|
||||||
|
OSIStartRune = rune(93) // ]
|
||||||
|
OSIStopRune = '\\'
|
||||||
|
)
|
||||||
|
|
||||||
|
type escKind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
escKindUnknown escKind = iota
|
||||||
|
escKindCSI
|
||||||
|
escKindOSI
|
||||||
|
)
|
||||||
|
|
||||||
|
type escSeq struct {
|
||||||
|
isIn bool
|
||||||
|
content strings.Builder
|
||||||
|
kind escKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *escSeq) InspectRune(r rune) {
|
||||||
|
if !e.isIn && r == EscapeStartRune {
|
||||||
|
e.isIn = true
|
||||||
|
e.kind = escKindUnknown
|
||||||
|
e.content.Reset()
|
||||||
|
e.content.WriteRune(r)
|
||||||
|
} else if e.isIn {
|
||||||
|
switch {
|
||||||
|
case e.kind == escKindUnknown && r == CSIStartRune:
|
||||||
|
e.kind = escKindCSI
|
||||||
|
case e.kind == escKindUnknown && r == OSIStartRune:
|
||||||
|
e.kind = escKindOSI
|
||||||
|
case e.kind == escKindCSI && r == CSIStopRune || e.kind == escKindOSI && r == OSIStopRune:
|
||||||
|
e.isIn = false
|
||||||
|
e.kind = escKindUnknown
|
||||||
|
}
|
||||||
|
e.content.WriteRune(r)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
12
helper/text/filter.go
Normal file
12
helper/text/filter.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
// Filter filters the slice 's' to items which return truth when passed to 'f'.
|
||||||
|
func Filter(s []string, f func(string) bool) []string {
|
||||||
|
var out []string
|
||||||
|
for _, item := range s {
|
||||||
|
if f(item) {
|
||||||
|
out = append(out, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
30
helper/text/filter_test.go
Normal file
30
helper/text/filter_test.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleFilter() {
|
||||||
|
slice := []string{"Arya Stark", "Bran Stark", "Jon Snow", "Sansa Stark"}
|
||||||
|
filter := func(item string) bool {
|
||||||
|
return strings.HasSuffix(item, "Stark")
|
||||||
|
}
|
||||||
|
fmt.Printf("%#v\n", Filter(slice, filter))
|
||||||
|
|
||||||
|
// Output: []string{"Arya Stark", "Bran Stark", "Sansa Stark"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilter(t *testing.T) {
|
||||||
|
slice := []string{"Arya Stark", "Bran Stark", "Jon Snow", "Sansa Stark"}
|
||||||
|
filter := func(item string) bool {
|
||||||
|
return strings.HasSuffix(item, "Stark")
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredSlice := Filter(slice, filter)
|
||||||
|
assert.Equal(t, 3, len(filteredSlice))
|
||||||
|
assert.NotContains(t, filteredSlice, "Jon Snow")
|
||||||
|
}
|
100
helper/text/format.go
Normal file
100
helper/text/format.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Format lets you transform the text in supported methods while keeping escape
|
||||||
|
// sequences in the string intact and untouched.
|
||||||
|
type Format int
|
||||||
|
|
||||||
|
// Format enumerations
|
||||||
|
const (
|
||||||
|
FormatDefault Format = iota // default_Case
|
||||||
|
FormatLower // lower
|
||||||
|
FormatTitle // Title
|
||||||
|
FormatUpper // UPPER
|
||||||
|
)
|
||||||
|
|
||||||
|
// Apply converts the text as directed.
|
||||||
|
func (tc Format) Apply(text string) string {
|
||||||
|
switch tc {
|
||||||
|
case FormatLower:
|
||||||
|
return strings.ToLower(text)
|
||||||
|
case FormatTitle:
|
||||||
|
return toTitle(text)
|
||||||
|
case FormatUpper:
|
||||||
|
return toUpper(text)
|
||||||
|
default:
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toTitle(text string) string {
|
||||||
|
prev, inEscSeq := ' ', false
|
||||||
|
return strings.Map(
|
||||||
|
func(r rune) rune {
|
||||||
|
if r == EscapeStartRune {
|
||||||
|
inEscSeq = true
|
||||||
|
}
|
||||||
|
if !inEscSeq {
|
||||||
|
if isSeparator(prev) {
|
||||||
|
prev = r
|
||||||
|
r = unicode.ToUpper(r)
|
||||||
|
} else {
|
||||||
|
prev = r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if inEscSeq && r == EscapeStopRune {
|
||||||
|
inEscSeq = false
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
},
|
||||||
|
text,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toUpper(text string) string {
|
||||||
|
inEscSeq := false
|
||||||
|
return strings.Map(
|
||||||
|
func(r rune) rune {
|
||||||
|
if r == EscapeStartRune {
|
||||||
|
inEscSeq = true
|
||||||
|
}
|
||||||
|
if !inEscSeq {
|
||||||
|
r = unicode.ToUpper(r)
|
||||||
|
}
|
||||||
|
if inEscSeq && r == EscapeStopRune {
|
||||||
|
inEscSeq = false
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
},
|
||||||
|
text,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSeparator returns true if the given rune is a separator. This function is
|
||||||
|
// lifted straight out of the standard library @ strings/strings.go.
|
||||||
|
func isSeparator(r rune) bool {
|
||||||
|
// ASCII alphanumerics and underscore are not separators
|
||||||
|
if r <= 0x7F {
|
||||||
|
switch {
|
||||||
|
case '0' <= r && r <= '9':
|
||||||
|
return false
|
||||||
|
case 'a' <= r && r <= 'z':
|
||||||
|
return false
|
||||||
|
case 'A' <= r && r <= 'Z':
|
||||||
|
return false
|
||||||
|
case r == '_':
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Letters and digits are not separators
|
||||||
|
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Otherwise, all we can do for now is treat spaces as separators.
|
||||||
|
return unicode.IsSpace(r)
|
||||||
|
}
|
45
helper/text/format_test.go
Normal file
45
helper/text/format_test.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleFormat_Apply() {
|
||||||
|
fmt.Printf("FormatDefault: %#v\n", FormatDefault.Apply("jon Snow"))
|
||||||
|
fmt.Printf("FormatLower : %#v\n", FormatLower.Apply("jon Snow"))
|
||||||
|
fmt.Printf("FormatTitle : %#v\n", FormatTitle.Apply("jon Snow"))
|
||||||
|
fmt.Printf("FormatUpper : %#v\n", FormatUpper.Apply("jon Snow"))
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("FormatDefault (w/EscSeq): %#v\n", FormatDefault.Apply(Bold.Sprint("jon Snow")))
|
||||||
|
fmt.Printf("FormatLower (w/EscSeq): %#v\n", FormatLower.Apply(Bold.Sprint("jon Snow")))
|
||||||
|
fmt.Printf("FormatTitle (w/EscSeq): %#v\n", FormatTitle.Apply(Bold.Sprint("jon Snow")))
|
||||||
|
fmt.Printf("FormatUpper (w/EscSeq): %#v\n", FormatUpper.Apply(Bold.Sprint("jon Snow")))
|
||||||
|
|
||||||
|
// Output: FormatDefault: "jon Snow"
|
||||||
|
// FormatLower : "jon snow"
|
||||||
|
// FormatTitle : "Jon Snow"
|
||||||
|
// FormatUpper : "JON SNOW"
|
||||||
|
//
|
||||||
|
// FormatDefault (w/EscSeq): "\x1b[1mjon Snow\x1b[0m"
|
||||||
|
// FormatLower (w/EscSeq): "\x1b[1mjon snow\x1b[0m"
|
||||||
|
// FormatTitle (w/EscSeq): "\x1b[1mJon Snow\x1b[0m"
|
||||||
|
// FormatUpper (w/EscSeq): "\x1b[1mJON SNOW\x1b[0m"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormat_Apply(t *testing.T) {
|
||||||
|
text := "A big croc0dile; Died - Empty_fanged ツ \u2008."
|
||||||
|
assert.Equal(t, text, FormatDefault.Apply(text))
|
||||||
|
assert.Equal(t, "a big croc0dile; died - empty_fanged ツ \u2008.", FormatLower.Apply(text))
|
||||||
|
assert.Equal(t, "A Big Croc0dile; Died - Empty_fanged ツ \u2008.", FormatTitle.Apply(text))
|
||||||
|
assert.Equal(t, "A BIG CROC0DILE; DIED - EMPTY_FANGED ツ \u2008.", FormatUpper.Apply(text))
|
||||||
|
|
||||||
|
// test with escape sequences
|
||||||
|
text = Colors{Bold}.Sprint(text)
|
||||||
|
assert.Equal(t, "\x1b[1mA big croc0dile; Died - Empty_fanged ツ \u2008.\x1b[0m", FormatDefault.Apply(text))
|
||||||
|
assert.Equal(t, "\x1b[1ma big croc0dile; died - empty_fanged ツ \u2008.\x1b[0m", FormatLower.Apply(text))
|
||||||
|
assert.Equal(t, "\x1b[1mA Big Croc0dile; Died - Empty_fanged ツ \u2008.\x1b[0m", FormatTitle.Apply(text))
|
||||||
|
assert.Equal(t, "\x1b[1mA BIG CROC0DILE; DIED - EMPTY_FANGED ツ \u2008.\x1b[0m", FormatUpper.Apply(text))
|
||||||
|
}
|
14
helper/text/hyperlink.go
Normal file
14
helper/text/hyperlink.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func Hyperlink(url, text string) string {
|
||||||
|
if url == "" {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
if text == "" {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
// source https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
|
||||||
|
return fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\", url, text)
|
||||||
|
}
|
13
helper/text/hyperlink_test.go
Normal file
13
helper/text/hyperlink_test.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHyperlink(t *testing.T) {
|
||||||
|
assert.Equal(t, "Ghost", Hyperlink("", "Ghost"))
|
||||||
|
assert.Equal(t, "https://example.com", Hyperlink("https://example.com", ""))
|
||||||
|
assert.Equal(t, "\x1b]8;;https://example.com\x1b\\Ghost\x1b]8;;\x1b\\", Hyperlink("https://example.com", "Ghost"))
|
||||||
|
}
|
219
helper/text/string.go
Normal file
219
helper/text/string.go
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/mattn/go-runewidth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RuneWidth stuff
|
||||||
|
var (
|
||||||
|
rwCondition = runewidth.NewCondition()
|
||||||
|
)
|
||||||
|
|
||||||
|
// InsertEveryN inserts the rune every N characters in the string. For ex.:
|
||||||
|
// InsertEveryN("Ghost", '-', 1) == "G-h-o-s-t"
|
||||||
|
// InsertEveryN("Ghost", '-', 2) == "Gh-os-t"
|
||||||
|
// InsertEveryN("Ghost", '-', 3) == "Gho-st"
|
||||||
|
// InsertEveryN("Ghost", '-', 4) == "Ghos-t"
|
||||||
|
// InsertEveryN("Ghost", '-', 5) == "Ghost"
|
||||||
|
func InsertEveryN(str string, runeToInsert rune, n int) string {
|
||||||
|
if n <= 0 {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
sLen := RuneWidthWithoutEscSequences(str)
|
||||||
|
var out strings.Builder
|
||||||
|
out.Grow(sLen + (sLen / n))
|
||||||
|
outLen, eSeq := 0, escSeq{}
|
||||||
|
for idx, c := range str {
|
||||||
|
if eSeq.isIn {
|
||||||
|
eSeq.InspectRune(c)
|
||||||
|
out.WriteRune(c)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
eSeq.InspectRune(c)
|
||||||
|
if !eSeq.isIn && outLen > 0 && (outLen%n) == 0 && idx != sLen {
|
||||||
|
out.WriteRune(runeToInsert)
|
||||||
|
}
|
||||||
|
out.WriteRune(c)
|
||||||
|
if !eSeq.isIn {
|
||||||
|
outLen += RuneWidth(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LongestLineLen returns the length of the longest "line" within the
|
||||||
|
// argument string. For ex.:
|
||||||
|
// LongestLineLen("Ghost!\nCome back here!\nRight now!") == 15
|
||||||
|
func LongestLineLen(str string) int {
|
||||||
|
maxLength, currLength, eSeq := 0, 0, escSeq{}
|
||||||
|
for _, c := range str {
|
||||||
|
if eSeq.isIn {
|
||||||
|
eSeq.InspectRune(c)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
eSeq.InspectRune(c)
|
||||||
|
if c == '\n' {
|
||||||
|
if currLength > maxLength {
|
||||||
|
maxLength = currLength
|
||||||
|
}
|
||||||
|
currLength = 0
|
||||||
|
} else if !eSeq.isIn {
|
||||||
|
currLength += RuneWidth(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if currLength > maxLength {
|
||||||
|
maxLength = currLength
|
||||||
|
}
|
||||||
|
return maxLength
|
||||||
|
}
|
||||||
|
|
||||||
|
// OverrideRuneWidthEastAsianWidth can *probably* help with alignment, and
|
||||||
|
// length calculation issues when dealing with Unicode character-set and a
|
||||||
|
// non-English language set in the LANG variable.
|
||||||
|
//
|
||||||
|
// Set this to 'false' to force the "runewidth" library to pretend to deal with
|
||||||
|
// English character-set. Be warned that if the text/content you are dealing
|
||||||
|
// with contains East Asian character-set, this may result in unexpected
|
||||||
|
// behavior.
|
||||||
|
//
|
||||||
|
// References:
|
||||||
|
// * https://github.com/mattn/go-runewidth/issues/64#issuecomment-1221642154
|
||||||
|
// * https://github.com/jedib0t/go-pretty/issues/220
|
||||||
|
// * https://github.com/jedib0t/go-pretty/issues/204
|
||||||
|
func OverrideRuneWidthEastAsianWidth(val bool) {
|
||||||
|
rwCondition.EastAsianWidth = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pad pads the given string with as many characters as needed to make it as
|
||||||
|
// long as specified (maxLen). This function does not count escape sequences
|
||||||
|
// while calculating length of the string. Ex.:
|
||||||
|
// Pad("Ghost", 0, ' ') == "Ghost"
|
||||||
|
// Pad("Ghost", 3, ' ') == "Ghost"
|
||||||
|
// Pad("Ghost", 5, ' ') == "Ghost"
|
||||||
|
// Pad("Ghost", 7, ' ') == "Ghost "
|
||||||
|
// Pad("Ghost", 10, '.') == "Ghost....."
|
||||||
|
func Pad(str string, maxLen int, paddingChar rune) string {
|
||||||
|
strLen := RuneWidthWithoutEscSequences(str)
|
||||||
|
if strLen < maxLen {
|
||||||
|
str += strings.Repeat(string(paddingChar), maxLen-strLen)
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
// RepeatAndTrim repeats the given string until it is as long as maxRunes.
|
||||||
|
// For ex.:
|
||||||
|
// RepeatAndTrim("", 5) == ""
|
||||||
|
// RepeatAndTrim("Ghost", 0) == ""
|
||||||
|
// RepeatAndTrim("Ghost", 5) == "Ghost"
|
||||||
|
// RepeatAndTrim("Ghost", 7) == "GhostGh"
|
||||||
|
// RepeatAndTrim("Ghost", 10) == "GhostGhost"
|
||||||
|
func RepeatAndTrim(str string, maxRunes int) string {
|
||||||
|
if str == "" || maxRunes == 0 {
|
||||||
|
return ""
|
||||||
|
} else if maxRunes == utf8.RuneCountInString(str) {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
repeatedS := strings.Repeat(str, int(maxRunes/utf8.RuneCountInString(str))+1)
|
||||||
|
return Trim(repeatedS, maxRunes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RuneCount is similar to utf8.RuneCountInString, except for the fact that it
|
||||||
|
// ignores escape sequences while counting. For ex.:
|
||||||
|
// RuneCount("") == 0
|
||||||
|
// RuneCount("Ghost") == 5
|
||||||
|
// RuneCount("\x1b[33mGhost\x1b[0m") == 5
|
||||||
|
// RuneCount("\x1b[33mGhost\x1b[0") == 5
|
||||||
|
// Deprecated: in favor of RuneWidthWithoutEscSequences
|
||||||
|
func RuneCount(str string) int {
|
||||||
|
return RuneWidthWithoutEscSequences(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RuneWidth returns the mostly accurate character-width of the rune. This is
|
||||||
|
// not 100% accurate as the character width is usually dependent on the
|
||||||
|
// typeface (font) used in the console/terminal. For ex.:
|
||||||
|
// RuneWidth('A') == 1
|
||||||
|
// RuneWidth('ツ') == 2
|
||||||
|
// RuneWidth('⊙') == 1
|
||||||
|
// RuneWidth('︿') == 2
|
||||||
|
// RuneWidth(0x27) == 0
|
||||||
|
func RuneWidth(r rune) int {
|
||||||
|
return rwCondition.RuneWidth(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RuneWidthWithoutEscSequences is similar to RuneWidth, except for the fact
|
||||||
|
// that it ignores escape sequences while counting. For ex.:
|
||||||
|
// RuneWidthWithoutEscSequences("") == 0
|
||||||
|
// RuneWidthWithoutEscSequences("Ghost") == 5
|
||||||
|
// RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0m") == 5
|
||||||
|
// RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0") == 5
|
||||||
|
func RuneWidthWithoutEscSequences(str string) int {
|
||||||
|
count, eSeq := 0, escSeq{}
|
||||||
|
for _, c := range str {
|
||||||
|
if eSeq.isIn {
|
||||||
|
eSeq.InspectRune(c)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
eSeq.InspectRune(c)
|
||||||
|
if !eSeq.isIn {
|
||||||
|
count += RuneWidth(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snip returns the given string with a fixed length. For ex.:
|
||||||
|
// Snip("Ghost", 0, "~") == "Ghost"
|
||||||
|
// Snip("Ghost", 1, "~") == "~"
|
||||||
|
// Snip("Ghost", 3, "~") == "Gh~"
|
||||||
|
// Snip("Ghost", 5, "~") == "Ghost"
|
||||||
|
// Snip("Ghost", 7, "~") == "Ghost "
|
||||||
|
// Snip("\x1b[33mGhost\x1b[0m", 7, "~") == "\x1b[33mGhost\x1b[0m "
|
||||||
|
func Snip(str string, length int, snipIndicator string) string {
|
||||||
|
if length > 0 {
|
||||||
|
lenStr := RuneWidthWithoutEscSequences(str)
|
||||||
|
if lenStr > length {
|
||||||
|
lenStrFinal := length - RuneWidthWithoutEscSequences(snipIndicator)
|
||||||
|
return Trim(str, lenStrFinal) + snipIndicator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim trims a string to the given length while ignoring escape sequences. For
|
||||||
|
// ex.:
|
||||||
|
// Trim("Ghost", 3) == "Gho"
|
||||||
|
// Trim("Ghost", 6) == "Ghost"
|
||||||
|
// Trim("\x1b[33mGhost\x1b[0m", 3) == "\x1b[33mGho\x1b[0m"
|
||||||
|
// Trim("\x1b[33mGhost\x1b[0m", 6) == "\x1b[33mGhost\x1b[0m"
|
||||||
|
func Trim(str string, maxLen int) string {
|
||||||
|
if maxLen <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var out strings.Builder
|
||||||
|
out.Grow(maxLen)
|
||||||
|
|
||||||
|
outLen, eSeq := 0, escSeq{}
|
||||||
|
for _, sChr := range str {
|
||||||
|
if eSeq.isIn {
|
||||||
|
eSeq.InspectRune(sChr)
|
||||||
|
out.WriteRune(sChr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
eSeq.InspectRune(sChr)
|
||||||
|
if eSeq.isIn {
|
||||||
|
out.WriteRune(sChr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if outLen < maxLen {
|
||||||
|
outLen++
|
||||||
|
out.WriteRune(sChr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out.String()
|
||||||
|
}
|
282
helper/text/string_test.go
Normal file
282
helper/text/string_test.go
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleInsertEveryN() {
|
||||||
|
fmt.Printf("InsertEveryN(\"Ghost\", '-', 0): %#v\n", InsertEveryN("Ghost", '-', 0))
|
||||||
|
fmt.Printf("InsertEveryN(\"Ghost\", '-', 1): %#v\n", InsertEveryN("Ghost", '-', 1))
|
||||||
|
fmt.Printf("InsertEveryN(\"Ghost\", '-', 2): %#v\n", InsertEveryN("Ghost", '-', 2))
|
||||||
|
fmt.Printf("InsertEveryN(\"Ghost\", '-', 3): %#v\n", InsertEveryN("Ghost", '-', 3))
|
||||||
|
fmt.Printf("InsertEveryN(\"Ghost\", '-', 4): %#v\n", InsertEveryN("Ghost", '-', 4))
|
||||||
|
fmt.Printf("InsertEveryN(\"Ghost\", '-', 5): %#v\n", InsertEveryN("Ghost", '-', 5))
|
||||||
|
fmt.Printf("InsertEveryN(\"\\x1b[33mGhost\\x1b[0m\", '-', 0): %#v\n", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 0))
|
||||||
|
fmt.Printf("InsertEveryN(\"\\x1b[33mGhost\\x1b[0m\", '-', 1): %#v\n", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 1))
|
||||||
|
fmt.Printf("InsertEveryN(\"\\x1b[33mGhost\\x1b[0m\", '-', 2): %#v\n", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 2))
|
||||||
|
fmt.Printf("InsertEveryN(\"\\x1b[33mGhost\\x1b[0m\", '-', 3): %#v\n", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 3))
|
||||||
|
fmt.Printf("InsertEveryN(\"\\x1b[33mGhost\\x1b[0m\", '-', 4): %#v\n", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 4))
|
||||||
|
fmt.Printf("InsertEveryN(\"\\x1b[33mGhost\\x1b[0m\", '-', 5): %#v\n", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 5))
|
||||||
|
|
||||||
|
// Output: InsertEveryN("Ghost", '-', 0): "Ghost"
|
||||||
|
// InsertEveryN("Ghost", '-', 1): "G-h-o-s-t"
|
||||||
|
// InsertEveryN("Ghost", '-', 2): "Gh-os-t"
|
||||||
|
// InsertEveryN("Ghost", '-', 3): "Gho-st"
|
||||||
|
// InsertEveryN("Ghost", '-', 4): "Ghos-t"
|
||||||
|
// InsertEveryN("Ghost", '-', 5): "Ghost"
|
||||||
|
// InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 0): "\x1b[33mGhost\x1b[0m"
|
||||||
|
// InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 1): "\x1b[33mG-h-o-s-t\x1b[0m"
|
||||||
|
// InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 2): "\x1b[33mGh-os-t\x1b[0m"
|
||||||
|
// InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 3): "\x1b[33mGho-st\x1b[0m"
|
||||||
|
// InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 4): "\x1b[33mGhos-t\x1b[0m"
|
||||||
|
// InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 5): "\x1b[33mGhost\x1b[0m"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInsertEveryN(t *testing.T) {
|
||||||
|
assert.Equal(t, "Ghost", InsertEveryN("Ghost", '-', 0))
|
||||||
|
assert.Equal(t, "Gツhツoツsツt", InsertEveryN("Ghost", 'ツ', 1))
|
||||||
|
assert.Equal(t, "G-h-o-s-t", InsertEveryN("Ghost", '-', 1))
|
||||||
|
assert.Equal(t, "Gh-os-t", InsertEveryN("Ghost", '-', 2))
|
||||||
|
assert.Equal(t, "Gho-st", InsertEveryN("Ghost", '-', 3))
|
||||||
|
assert.Equal(t, "Ghos-t", InsertEveryN("Ghost", '-', 4))
|
||||||
|
assert.Equal(t, "Ghost", InsertEveryN("Ghost", '-', 5))
|
||||||
|
assert.Equal(t, "\x1b[33mGhost\x1b[0m", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 0))
|
||||||
|
assert.Equal(t, "\x1b[33mGツhツoツsツt\x1b[0m", InsertEveryN("\x1b[33mGhost\x1b[0m", 'ツ', 1))
|
||||||
|
assert.Equal(t, "\x1b[33mG-h-o-s-t\x1b[0m", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 1))
|
||||||
|
assert.Equal(t, "\x1b[33mGh-os-t\x1b[0m", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 2))
|
||||||
|
assert.Equal(t, "\x1b[33mGho-st\x1b[0m", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 3))
|
||||||
|
assert.Equal(t, "\x1b[33mGhos-t\x1b[0m", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 4))
|
||||||
|
assert.Equal(t, "\x1b[33mGhost\x1b[0m", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 5))
|
||||||
|
assert.Equal(t, "G\x1b]8;;http://example.com\x1b\\-h-o-s-t\x1b]8;;\x1b\\", InsertEveryN("G\x1b]8;;http://example.com\x1b\\host\x1b]8;;\x1b\\", '-', 1))
|
||||||
|
assert.Equal(t, "\x1b]8;;http://example.com\x1b\\G-h-o-s-t\x1b]8;;\x1b\\", InsertEveryN("\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\", '-', 1))
|
||||||
|
assert.Equal(t, "\x1b]8;;http://example.com\x1b\\G-h-o-s\x1b]8;;\x1b\\-t", InsertEveryN("\x1b]8;;http://example.com\x1b\\Ghos\x1b]8;;\x1b\\t", '-', 1))
|
||||||
|
assert.Equal(t, "\x1b]8;;http://example.com\x1b\\Gツhツoツsツt\x1b]8;;\x1b\\", InsertEveryN("\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\", 'ツ', 1))
|
||||||
|
assert.Equal(t, "\x1b]8;;http://example.com\x1b\\Ghツosツt\x1b]8;;\x1b\\", InsertEveryN("\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\", 'ツ', 2))
|
||||||
|
assert.Equal(t, "\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\", InsertEveryN("\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\", '-', 5))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleLongestLineLen() {
|
||||||
|
fmt.Printf("LongestLineLen(\"\"): %d\n", LongestLineLen(""))
|
||||||
|
fmt.Printf("LongestLineLen(\"\\n\\n\"): %d\n", LongestLineLen("\n\n"))
|
||||||
|
fmt.Printf("LongestLineLen(\"Ghost\"): %d\n", LongestLineLen("Ghost"))
|
||||||
|
fmt.Printf("LongestLineLen(\"Ghostツ\"): %d\n", LongestLineLen("Ghostツ"))
|
||||||
|
fmt.Printf("LongestLineLen(\"Winter\\nIs\\nComing\"): %d\n", LongestLineLen("Winter\nIs\nComing"))
|
||||||
|
fmt.Printf("LongestLineLen(\"Mother\\nOf\\nDragons\"): %d\n", LongestLineLen("Mother\nOf\nDragons"))
|
||||||
|
fmt.Printf("LongestLineLen(\"\\x1b[33mMother\\x1b[0m\\nOf\\nDragons\"): %d\n", LongestLineLen("\x1b[33mMother\x1b[0m\nOf\nDragons"))
|
||||||
|
|
||||||
|
// Output: LongestLineLen(""): 0
|
||||||
|
// LongestLineLen("\n\n"): 0
|
||||||
|
// LongestLineLen("Ghost"): 5
|
||||||
|
// LongestLineLen("Ghostツ"): 7
|
||||||
|
// LongestLineLen("Winter\nIs\nComing"): 6
|
||||||
|
// LongestLineLen("Mother\nOf\nDragons"): 7
|
||||||
|
// LongestLineLen("\x1b[33mMother\x1b[0m\nOf\nDragons"): 7
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLongestLineLen(t *testing.T) {
|
||||||
|
assert.Equal(t, 0, LongestLineLen(""))
|
||||||
|
assert.Equal(t, 0, LongestLineLen("\n\n"))
|
||||||
|
assert.Equal(t, 5, LongestLineLen("Ghost"))
|
||||||
|
assert.Equal(t, 7, LongestLineLen("Ghostツ"))
|
||||||
|
assert.Equal(t, 6, LongestLineLen("Winter\nIs\nComing"))
|
||||||
|
assert.Equal(t, 7, LongestLineLen("Mother\nOf\nDragons"))
|
||||||
|
assert.Equal(t, 7, LongestLineLen("\x1b[33mMother\x1b[0m\nOf\nDragons"))
|
||||||
|
assert.Equal(t, 7, LongestLineLen("Mother\nOf\n\x1b]8;;http://example.com\x1b\\Dragons\x1b]8;;\x1b\\"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOverrideRuneWidthEastAsianWidth(t *testing.T) {
|
||||||
|
originalValue := rwCondition.EastAsianWidth
|
||||||
|
defer func() {
|
||||||
|
rwCondition.EastAsianWidth = originalValue
|
||||||
|
}()
|
||||||
|
|
||||||
|
OverrideRuneWidthEastAsianWidth(true)
|
||||||
|
assert.Equal(t, 2, RuneWidthWithoutEscSequences("╋"))
|
||||||
|
OverrideRuneWidthEastAsianWidth(false)
|
||||||
|
assert.Equal(t, 1, RuneWidthWithoutEscSequences("╋"))
|
||||||
|
|
||||||
|
// Note for posterity. We want the length of the box drawing character to
|
||||||
|
// be reported as 1. However, with an environment where LANG is set to
|
||||||
|
// something like 'zh_CN.UTF-8', the value being returned is 2, which breaks
|
||||||
|
// text alignment/padding logic in this library.
|
||||||
|
//
|
||||||
|
// If a future version of runewidth is able to address this internally and
|
||||||
|
// return 1 for the above, the function being tested can be marked for
|
||||||
|
// deprecation.
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExamplePad() {
|
||||||
|
fmt.Printf("%#v\n", Pad("Ghost", 0, ' '))
|
||||||
|
fmt.Printf("%#v\n", Pad("Ghost", 3, ' '))
|
||||||
|
fmt.Printf("%#v\n", Pad("Ghost", 5, ' '))
|
||||||
|
fmt.Printf("%#v\n", Pad("\x1b[33mGhost\x1b[0m", 7, '-'))
|
||||||
|
fmt.Printf("%#v\n", Pad("\x1b[33mGhost\x1b[0m", 10, '.'))
|
||||||
|
|
||||||
|
// Output: "Ghost"
|
||||||
|
// "Ghost"
|
||||||
|
// "Ghost"
|
||||||
|
// "\x1b[33mGhost\x1b[0m--"
|
||||||
|
// "\x1b[33mGhost\x1b[0m....."
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPad(t *testing.T) {
|
||||||
|
assert.Equal(t, "Ghost", Pad("Ghost", 0, ' '))
|
||||||
|
assert.Equal(t, "Ghost", Pad("Ghost", 3, ' '))
|
||||||
|
assert.Equal(t, "Ghost", Pad("Ghost", 5, ' '))
|
||||||
|
assert.Equal(t, "Ghost ", Pad("Ghost", 7, ' '))
|
||||||
|
assert.Equal(t, "Ghost.....", Pad("Ghost", 10, '.'))
|
||||||
|
assert.Equal(t, "\x1b[33mGhost\x1b[0 ", Pad("\x1b[33mGhost\x1b[0", 7, ' '))
|
||||||
|
assert.Equal(t, "\x1b[33mGhost\x1b[0.....", Pad("\x1b[33mGhost\x1b[0", 10, '.'))
|
||||||
|
assert.Equal(t, "\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\ ", Pad("\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\", 7, ' '))
|
||||||
|
assert.Equal(t, "\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\.....", Pad("\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\", 10, '.'))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleRepeatAndTrim() {
|
||||||
|
fmt.Printf("RepeatAndTrim(\"\", 5): %#v\n", RepeatAndTrim("", 5))
|
||||||
|
fmt.Printf("RepeatAndTrim(\"Ghost\", 0): %#v\n", RepeatAndTrim("Ghost", 0))
|
||||||
|
fmt.Printf("RepeatAndTrim(\"Ghost\", 3): %#v\n", RepeatAndTrim("Ghost", 3))
|
||||||
|
fmt.Printf("RepeatAndTrim(\"Ghost\", 5): %#v\n", RepeatAndTrim("Ghost", 5))
|
||||||
|
fmt.Printf("RepeatAndTrim(\"Ghost\", 7): %#v\n", RepeatAndTrim("Ghost", 7))
|
||||||
|
fmt.Printf("RepeatAndTrim(\"Ghost\", 10): %#v\n", RepeatAndTrim("Ghost", 10))
|
||||||
|
|
||||||
|
// Output: RepeatAndTrim("", 5): ""
|
||||||
|
// RepeatAndTrim("Ghost", 0): ""
|
||||||
|
// RepeatAndTrim("Ghost", 3): "Gho"
|
||||||
|
// RepeatAndTrim("Ghost", 5): "Ghost"
|
||||||
|
// RepeatAndTrim("Ghost", 7): "GhostGh"
|
||||||
|
// RepeatAndTrim("Ghost", 10): "GhostGhost"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepeatAndTrim(t *testing.T) {
|
||||||
|
assert.Equal(t, "", RepeatAndTrim("", 5))
|
||||||
|
assert.Equal(t, "", RepeatAndTrim("Ghost", 0))
|
||||||
|
assert.Equal(t, "Gho", RepeatAndTrim("Ghost", 3))
|
||||||
|
assert.Equal(t, "Ghost", RepeatAndTrim("Ghost", 5))
|
||||||
|
assert.Equal(t, "GhostGh", RepeatAndTrim("Ghost", 7))
|
||||||
|
assert.Equal(t, "GhostGhost", RepeatAndTrim("Ghost", 10))
|
||||||
|
assert.Equal(t, "───", RepeatAndTrim("─", 3))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleRuneCount() {
|
||||||
|
fmt.Printf("RuneCount(\"\"): %d\n", RuneCount(""))
|
||||||
|
fmt.Printf("RuneCount(\"Ghost\"): %d\n", RuneCount("Ghost"))
|
||||||
|
fmt.Printf("RuneCount(\"Ghostツ\"): %d\n", RuneCount("Ghostツ"))
|
||||||
|
fmt.Printf("RuneCount(\"\\x1b[33mGhost\\x1b[0m\"): %d\n", RuneCount("\x1b[33mGhost\x1b[0m"))
|
||||||
|
fmt.Printf("RuneCount(\"\\x1b[33mGhost\\x1b[0\"): %d\n", RuneCount("\x1b[33mGhost\x1b[0"))
|
||||||
|
|
||||||
|
// Output: RuneCount(""): 0
|
||||||
|
// RuneCount("Ghost"): 5
|
||||||
|
// RuneCount("Ghostツ"): 7
|
||||||
|
// RuneCount("\x1b[33mGhost\x1b[0m"): 5
|
||||||
|
// RuneCount("\x1b[33mGhost\x1b[0"): 5
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRuneCount(t *testing.T) {
|
||||||
|
assert.Equal(t, 0, RuneCount(""))
|
||||||
|
assert.Equal(t, 5, RuneCount("Ghost"))
|
||||||
|
assert.Equal(t, 7, RuneCount("Ghostツ"))
|
||||||
|
assert.Equal(t, 5, RuneCount("\x1b[33mGhost\x1b[0m"))
|
||||||
|
assert.Equal(t, 5, RuneCount("\x1b[33mGhost\x1b[0"))
|
||||||
|
assert.Equal(t, 5, RuneCount("\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleRuneWidth() {
|
||||||
|
fmt.Printf("RuneWidth('A'): %d\n", RuneWidth('A'))
|
||||||
|
fmt.Printf("RuneWidth('ツ'): %d\n", RuneWidth('ツ'))
|
||||||
|
fmt.Printf("RuneWidth('⊙'): %d\n", RuneWidth('⊙'))
|
||||||
|
fmt.Printf("RuneWidth('︿'): %d\n", RuneWidth('︿'))
|
||||||
|
fmt.Printf("RuneWidth(rune(27)): %d\n", RuneWidth(rune(27))) // ANSI escape sequence
|
||||||
|
|
||||||
|
// Output: RuneWidth('A'): 1
|
||||||
|
// RuneWidth('ツ'): 2
|
||||||
|
// RuneWidth('⊙'): 1
|
||||||
|
// RuneWidth('︿'): 2
|
||||||
|
// RuneWidth(rune(27)): 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRuneWidth(t *testing.T) {
|
||||||
|
assert.Equal(t, 1, RuneWidth('A'))
|
||||||
|
assert.Equal(t, 2, RuneWidth('ツ'))
|
||||||
|
assert.Equal(t, 1, RuneWidth('⊙'))
|
||||||
|
assert.Equal(t, 2, RuneWidth('︿'))
|
||||||
|
assert.Equal(t, 0, RuneWidth(rune(27))) // ANSI escape sequence
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleRuneWidthWithoutEscSequences() {
|
||||||
|
fmt.Printf("RuneWidthWithoutEscSequences(\"\"): %d\n", RuneWidthWithoutEscSequences(""))
|
||||||
|
fmt.Printf("RuneWidthWithoutEscSequences(\"Ghost\"): %d\n", RuneWidthWithoutEscSequences("Ghost"))
|
||||||
|
fmt.Printf("RuneWidthWithoutEscSequences(\"Ghostツ\"): %d\n", RuneWidthWithoutEscSequences("Ghostツ"))
|
||||||
|
fmt.Printf("RuneWidthWithoutEscSequences(\"\\x1b[33mGhost\\x1b[0m\"): %d\n", RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0m"))
|
||||||
|
fmt.Printf("RuneWidthWithoutEscSequences(\"\\x1b[33mGhost\\x1b[0\"): %d\n", RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0"))
|
||||||
|
|
||||||
|
// Output: RuneWidthWithoutEscSequences(""): 0
|
||||||
|
// RuneWidthWithoutEscSequences("Ghost"): 5
|
||||||
|
// RuneWidthWithoutEscSequences("Ghostツ"): 7
|
||||||
|
// RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0m"): 5
|
||||||
|
// RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0"): 5
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRuneWidthWithoutEscSequences(t *testing.T) {
|
||||||
|
assert.Equal(t, 0, RuneWidthWithoutEscSequences(""))
|
||||||
|
assert.Equal(t, 5, RuneWidthWithoutEscSequences("Ghost"))
|
||||||
|
assert.Equal(t, 7, RuneWidthWithoutEscSequences("Ghostツ"))
|
||||||
|
assert.Equal(t, 5, RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0m"))
|
||||||
|
assert.Equal(t, 5, RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0"))
|
||||||
|
assert.Equal(t, 5, RuneWidthWithoutEscSequences("\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleSnip() {
|
||||||
|
fmt.Printf("Snip(\"Ghost\", 0, \"~\"): %#v\n", Snip("Ghost", 0, "~"))
|
||||||
|
fmt.Printf("Snip(\"Ghost\", 1, \"~\"): %#v\n", Snip("Ghost", 1, "~"))
|
||||||
|
fmt.Printf("Snip(\"Ghost\", 3, \"~\"): %#v\n", Snip("Ghost", 3, "~"))
|
||||||
|
fmt.Printf("Snip(\"Ghost\", 5, \"~\"): %#v\n", Snip("Ghost", 5, "~"))
|
||||||
|
fmt.Printf("Snip(\"Ghost\", 7, \"~\"): %#v\n", Snip("Ghost", 7, "~"))
|
||||||
|
fmt.Printf("Snip(\"\\x1b[33mGhost\\x1b[0m\", 7, \"~\"): %#v\n", Snip("\x1b[33mGhost\x1b[0m", 7, "~"))
|
||||||
|
|
||||||
|
// Output: Snip("Ghost", 0, "~"): "Ghost"
|
||||||
|
// Snip("Ghost", 1, "~"): "~"
|
||||||
|
// Snip("Ghost", 3, "~"): "Gh~"
|
||||||
|
// Snip("Ghost", 5, "~"): "Ghost"
|
||||||
|
// Snip("Ghost", 7, "~"): "Ghost"
|
||||||
|
// Snip("\x1b[33mGhost\x1b[0m", 7, "~"): "\x1b[33mGhost\x1b[0m"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSnip(t *testing.T) {
|
||||||
|
assert.Equal(t, "Ghost", Snip("Ghost", 0, "~"))
|
||||||
|
assert.Equal(t, "~", Snip("Ghost", 1, "~"))
|
||||||
|
assert.Equal(t, "Gh~", Snip("Ghost", 3, "~"))
|
||||||
|
assert.Equal(t, "Ghost", Snip("Ghost", 5, "~"))
|
||||||
|
assert.Equal(t, "Ghost", Snip("Ghost", 7, "~"))
|
||||||
|
assert.Equal(t, "\x1b[33mGhost\x1b[0m", Snip("\x1b[33mGhost\x1b[0m", 7, "~"))
|
||||||
|
assert.Equal(t, "\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\", Snip("\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\", 7, "~"))
|
||||||
|
assert.Equal(t, "\x1b]8;;http://example.com\x1b\\Gh\x1b]8;;\x1b\\~", Snip("\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\", 3, "~"))
|
||||||
|
assert.Equal(t, "\x1b[33m\x1b]8;;http://example.com\x1b\\Gh\x1b]8;;\x1b\\\x1b[0m~", Snip("\x1b[33m\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\\x1b[0m", 3, "~"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleTrim() {
|
||||||
|
fmt.Printf("Trim(\"Ghost\", 0): %#v\n", Trim("Ghost", 0))
|
||||||
|
fmt.Printf("Trim(\"Ghost\", 3): %#v\n", Trim("Ghost", 3))
|
||||||
|
fmt.Printf("Trim(\"Ghost\", 6): %#v\n", Trim("Ghost", 6))
|
||||||
|
fmt.Printf("Trim(\"\\x1b[33mGhost\\x1b[0m\", 0): %#v\n", Trim("\x1b[33mGhost\x1b[0m", 0))
|
||||||
|
fmt.Printf("Trim(\"\\x1b[33mGhost\\x1b[0m\", 3): %#v\n", Trim("\x1b[33mGhost\x1b[0m", 3))
|
||||||
|
fmt.Printf("Trim(\"\\x1b[33mGhost\\x1b[0m\", 6): %#v\n", Trim("\x1b[33mGhost\x1b[0m", 6))
|
||||||
|
|
||||||
|
// Output: Trim("Ghost", 0): ""
|
||||||
|
// Trim("Ghost", 3): "Gho"
|
||||||
|
// Trim("Ghost", 6): "Ghost"
|
||||||
|
// Trim("\x1b[33mGhost\x1b[0m", 0): ""
|
||||||
|
// Trim("\x1b[33mGhost\x1b[0m", 3): "\x1b[33mGho\x1b[0m"
|
||||||
|
// Trim("\x1b[33mGhost\x1b[0m", 6): "\x1b[33mGhost\x1b[0m"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrim(t *testing.T) {
|
||||||
|
assert.Equal(t, "", Trim("Ghost", 0))
|
||||||
|
assert.Equal(t, "Gho", Trim("Ghost", 3))
|
||||||
|
assert.Equal(t, "Ghost", Trim("Ghost", 6))
|
||||||
|
assert.Equal(t, "\x1b[33mGho\x1b[0m", Trim("\x1b[33mGhost\x1b[0m", 3))
|
||||||
|
assert.Equal(t, "\x1b[33mGhost\x1b[0m", Trim("\x1b[33mGhost\x1b[0m", 6))
|
||||||
|
assert.Equal(t, "\x1b]8;;http://example.com\x1b\\Gho\x1b]8;;\x1b\\", Trim("\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\", 3))
|
||||||
|
}
|
228
helper/text/transformer.go
Normal file
228
helper/text/transformer.go
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Transformer related constants
|
||||||
|
const (
|
||||||
|
unixTimeMinMilliseconds = int64(10000000000)
|
||||||
|
unixTimeMinMicroseconds = unixTimeMinMilliseconds * 1000
|
||||||
|
unixTimeMinNanoSeconds = unixTimeMinMicroseconds * 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
// Transformer related variables
|
||||||
|
var (
|
||||||
|
colorsNumberPositive = Colors{FgHiGreen}
|
||||||
|
colorsNumberNegative = Colors{FgHiRed}
|
||||||
|
colorsNumberZero = Colors{}
|
||||||
|
colorsURL = Colors{Underline, FgBlue}
|
||||||
|
rfc3339Milli = "2006-01-02T15:04:05.000Z07:00"
|
||||||
|
rfc3339Micro = "2006-01-02T15:04:05.000000Z07:00"
|
||||||
|
|
||||||
|
possibleTimeLayouts = []string{
|
||||||
|
time.RFC3339,
|
||||||
|
rfc3339Milli, // strfmt.DateTime.String()'s default layout
|
||||||
|
rfc3339Micro,
|
||||||
|
time.RFC3339Nano,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Transformer helps format the contents of an object to the user's liking.
|
||||||
|
type Transformer func(val interface{}) string
|
||||||
|
|
||||||
|
// NewNumberTransformer returns a number Transformer that:
|
||||||
|
// * transforms the number as directed by 'format' (ex.: %.2f)
|
||||||
|
// * colors negative values Red
|
||||||
|
// * colors positive values Green
|
||||||
|
func NewNumberTransformer(format string) Transformer {
|
||||||
|
return func(val interface{}) string {
|
||||||
|
if valStr := transformInt(format, val); valStr != "" {
|
||||||
|
return valStr
|
||||||
|
}
|
||||||
|
if valStr := transformUint(format, val); valStr != "" {
|
||||||
|
return valStr
|
||||||
|
}
|
||||||
|
if valStr := transformFloat(format, val); valStr != "" {
|
||||||
|
return valStr
|
||||||
|
}
|
||||||
|
return fmt.Sprint(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func transformInt(format string, val interface{}) string {
|
||||||
|
transform := func(val int64) string {
|
||||||
|
if val < 0 {
|
||||||
|
return colorsNumberNegative.Sprintf("-"+format, -val)
|
||||||
|
}
|
||||||
|
if val > 0 {
|
||||||
|
return colorsNumberPositive.Sprintf(format, val)
|
||||||
|
}
|
||||||
|
return colorsNumberZero.Sprintf(format, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
if number, ok := val.(int); ok {
|
||||||
|
return transform(int64(number))
|
||||||
|
}
|
||||||
|
if number, ok := val.(int8); ok {
|
||||||
|
return transform(int64(number))
|
||||||
|
}
|
||||||
|
if number, ok := val.(int16); ok {
|
||||||
|
return transform(int64(number))
|
||||||
|
}
|
||||||
|
if number, ok := val.(int32); ok {
|
||||||
|
return transform(int64(number))
|
||||||
|
}
|
||||||
|
if number, ok := val.(int64); ok {
|
||||||
|
return transform(int64(number))
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func transformUint(format string, val interface{}) string {
|
||||||
|
transform := func(val uint64) string {
|
||||||
|
if val > 0 {
|
||||||
|
return colorsNumberPositive.Sprintf(format, val)
|
||||||
|
}
|
||||||
|
return colorsNumberZero.Sprintf(format, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
if number, ok := val.(uint); ok {
|
||||||
|
return transform(uint64(number))
|
||||||
|
}
|
||||||
|
if number, ok := val.(uint8); ok {
|
||||||
|
return transform(uint64(number))
|
||||||
|
}
|
||||||
|
if number, ok := val.(uint16); ok {
|
||||||
|
return transform(uint64(number))
|
||||||
|
}
|
||||||
|
if number, ok := val.(uint32); ok {
|
||||||
|
return transform(uint64(number))
|
||||||
|
}
|
||||||
|
if number, ok := val.(uint64); ok {
|
||||||
|
return transform(uint64(number))
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func transformFloat(format string, val interface{}) string {
|
||||||
|
transform := func(val float64) string {
|
||||||
|
if val < 0 {
|
||||||
|
return colorsNumberNegative.Sprintf("-"+format, -val)
|
||||||
|
}
|
||||||
|
if val > 0 {
|
||||||
|
return colorsNumberPositive.Sprintf(format, val)
|
||||||
|
}
|
||||||
|
return colorsNumberZero.Sprintf(format, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
if number, ok := val.(float32); ok {
|
||||||
|
return transform(float64(number))
|
||||||
|
}
|
||||||
|
if number, ok := val.(float64); ok {
|
||||||
|
return transform(float64(number))
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJSONTransformer returns a Transformer that can format a JSON string or an
|
||||||
|
// object into pretty-indented JSON-strings.
|
||||||
|
func NewJSONTransformer(prefix string, indent string) Transformer {
|
||||||
|
return func(val interface{}) string {
|
||||||
|
if valStr, ok := val.(string); ok {
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := json.Indent(&b, []byte(strings.TrimSpace(valStr)), prefix, indent); err == nil {
|
||||||
|
return string(b.Bytes())
|
||||||
|
}
|
||||||
|
} else if b, err := json.MarshalIndent(val, prefix, indent); err == nil {
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%#v", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTimeTransformer returns a Transformer that can format a timestamp (a
|
||||||
|
// time.Time) into a well-defined time format defined using the provided layout
|
||||||
|
// (ex.: time.RFC3339).
|
||||||
|
//
|
||||||
|
// If a non-nil location value is provided, the time will be localized to that
|
||||||
|
// location (use time.Local to get localized timestamps).
|
||||||
|
func NewTimeTransformer(layout string, location *time.Location) Transformer {
|
||||||
|
return func(val interface{}) string {
|
||||||
|
rsp := fmt.Sprint(val)
|
||||||
|
if valTime, ok := val.(time.Time); ok {
|
||||||
|
rsp = formatTime(valTime, layout, location)
|
||||||
|
} else {
|
||||||
|
// cycle through some supported layouts to see if the string form
|
||||||
|
// of the object matches any of these layouts
|
||||||
|
for _, possibleTimeLayout := range possibleTimeLayouts {
|
||||||
|
if valTime, err := time.Parse(possibleTimeLayout, rsp); err == nil {
|
||||||
|
rsp = formatTime(valTime, layout, location)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rsp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUnixTimeTransformer returns a Transformer that can format a unix-timestamp
|
||||||
|
// into a well-defined time format as defined by 'layout'. This can handle
|
||||||
|
// unix-time in Seconds, MilliSeconds, Microseconds and Nanoseconds.
|
||||||
|
//
|
||||||
|
// If a non-nil location value is provided, the time will be localized to that
|
||||||
|
// location (use time.Local to get localized timestamps).
|
||||||
|
func NewUnixTimeTransformer(layout string, location *time.Location) Transformer {
|
||||||
|
transformer := NewTimeTransformer(layout, location)
|
||||||
|
|
||||||
|
return func(val interface{}) string {
|
||||||
|
if unixTime, ok := val.(int64); ok {
|
||||||
|
return formatTimeUnix(unixTime, transformer)
|
||||||
|
} else if unixTimeStr, ok := val.(string); ok {
|
||||||
|
if unixTime, err := strconv.ParseInt(unixTimeStr, 10, 64); err == nil {
|
||||||
|
return formatTimeUnix(unixTime, transformer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Sprint(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewURLTransformer returns a Transformer that can format and pretty print a string
|
||||||
|
// that contains a URL (the text is underlined and colored Blue).
|
||||||
|
func NewURLTransformer(colors ...Color) Transformer {
|
||||||
|
colorsToUse := colorsURL
|
||||||
|
if len(colors) > 0 {
|
||||||
|
colorsToUse = colors
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(val interface{}) string {
|
||||||
|
return colorsToUse.Sprint(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatTime(t time.Time, layout string, location *time.Location) string {
|
||||||
|
rsp := ""
|
||||||
|
if t.Unix() > 0 {
|
||||||
|
if location != nil {
|
||||||
|
t = t.In(location)
|
||||||
|
}
|
||||||
|
rsp = t.Format(layout)
|
||||||
|
}
|
||||||
|
return rsp
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatTimeUnix(unixTime int64, timeTransformer Transformer) string {
|
||||||
|
if unixTime >= unixTimeMinNanoSeconds {
|
||||||
|
unixTime = unixTime / time.Second.Nanoseconds()
|
||||||
|
} else if unixTime >= unixTimeMinMicroseconds {
|
||||||
|
unixTime = unixTime / (time.Second.Nanoseconds() / 1000)
|
||||||
|
} else if unixTime >= unixTimeMinMilliseconds {
|
||||||
|
unixTime = unixTime / (time.Second.Nanoseconds() / 1000000)
|
||||||
|
}
|
||||||
|
return timeTransformer(time.Unix(unixTime, 0))
|
||||||
|
}
|
233
helper/text/transformer_test.go
Normal file
233
helper/text/transformer_test.go
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewNumberTransformer(t *testing.T) {
|
||||||
|
signColorsMap := map[string]Colors{
|
||||||
|
"negative": colorsNumberNegative,
|
||||||
|
"positive": colorsNumberPositive,
|
||||||
|
"zero": nil,
|
||||||
|
"nil": nil,
|
||||||
|
}
|
||||||
|
colorValuesMap := map[string]map[interface{}]string{
|
||||||
|
"negative": {
|
||||||
|
int(-5): "%05d",
|
||||||
|
int8(-5): "%05d",
|
||||||
|
int16(-5): "%05d",
|
||||||
|
int32(-5): "%05d",
|
||||||
|
int64(-5): "%05d",
|
||||||
|
float32(-5.55555): "%08.2f",
|
||||||
|
float64(-5.55555): "%08.2f",
|
||||||
|
},
|
||||||
|
"positive": {
|
||||||
|
int(5): "%05d",
|
||||||
|
int8(5): "%05d",
|
||||||
|
int16(5): "%05d",
|
||||||
|
int32(5): "%05d",
|
||||||
|
int64(5): "%05d",
|
||||||
|
uint(5): "%05d",
|
||||||
|
uint8(5): "%05d",
|
||||||
|
uint16(5): "%05d",
|
||||||
|
uint32(5): "%05d",
|
||||||
|
uint64(5): "%05d",
|
||||||
|
float32(5.55555): "%08.2f",
|
||||||
|
float64(5.55555): "%08.2f",
|
||||||
|
},
|
||||||
|
"zero": {
|
||||||
|
int(0): "%05d",
|
||||||
|
int8(0): "%05d",
|
||||||
|
int16(0): "%05d",
|
||||||
|
int32(0): "%05d",
|
||||||
|
int64(0): "%05d",
|
||||||
|
uint(0): "%05d",
|
||||||
|
uint8(0): "%05d",
|
||||||
|
uint16(0): "%05d",
|
||||||
|
uint32(0): "%05d",
|
||||||
|
uint64(0): "%05d",
|
||||||
|
float32(0.00000): "%08.2f",
|
||||||
|
float64(0.00000): "%08.2f",
|
||||||
|
},
|
||||||
|
"nil": {
|
||||||
|
nil: "%v",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for sign, valuesFormatMap := range colorValuesMap {
|
||||||
|
for value, format := range valuesFormatMap {
|
||||||
|
transformer := NewNumberTransformer(format)
|
||||||
|
expected := signColorsMap[sign].Sprintf(format, value)
|
||||||
|
if sign == "negative" {
|
||||||
|
expected = strings.Replace(expected, "-0", "-00", 1)
|
||||||
|
}
|
||||||
|
actual := transformer(value)
|
||||||
|
var kind reflect.Kind
|
||||||
|
if value != nil {
|
||||||
|
kind = reflect.TypeOf(value).Kind()
|
||||||
|
}
|
||||||
|
message := fmt.Sprintf("%s.%s: expected=%v, actual=%v; format=%#v",
|
||||||
|
sign, kind, expected, actual, format)
|
||||||
|
|
||||||
|
assert.Equal(t, expected, actual, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// invalid input
|
||||||
|
assert.Equal(t, "foo", NewNumberTransformer("%05d")("foo"))
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonTest struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
Bar int32 `json:"bar"`
|
||||||
|
Baz float64 `json:"baz"`
|
||||||
|
Nan jsonNestTest `json:"nan"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonNestTest struct {
|
||||||
|
A string
|
||||||
|
B int32
|
||||||
|
C float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewJSONTransformer(t *testing.T) {
|
||||||
|
transformer := NewJSONTransformer("", " ")
|
||||||
|
|
||||||
|
// instance of a struct
|
||||||
|
inputObj := jsonTest{
|
||||||
|
Foo: "fooooooo",
|
||||||
|
Bar: 13,
|
||||||
|
Baz: 3.14,
|
||||||
|
Nan: jsonNestTest{
|
||||||
|
A: "a",
|
||||||
|
B: 2,
|
||||||
|
C: 3.0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expectedOutput := `{
|
||||||
|
"foo": "fooooooo",
|
||||||
|
"bar": 13,
|
||||||
|
"baz": 3.14,
|
||||||
|
"nan": {
|
||||||
|
"A": "a",
|
||||||
|
"B": 2,
|
||||||
|
"C": 3
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
assert.Equal(t, expectedOutput, transformer(inputObj))
|
||||||
|
|
||||||
|
// numbers
|
||||||
|
assert.Equal(t, "1", transformer(int(1)))
|
||||||
|
assert.Equal(t, "1.2345", transformer(float32(1.2345)))
|
||||||
|
|
||||||
|
// slices
|
||||||
|
assert.Equal(t, "[\n 1,\n 2,\n 3\n]", transformer([]uint{1, 2, 3}))
|
||||||
|
|
||||||
|
// strings
|
||||||
|
assert.Equal(t, "\"foo\"", transformer("foo"))
|
||||||
|
assert.Equal(t, "\"{foo...\"", transformer("{foo...")) // malformed JSON
|
||||||
|
|
||||||
|
// strings with valid JSON
|
||||||
|
input := "{\"foo\":\"bar\",\"baz\":[1,2,3]}"
|
||||||
|
expectedOutput = `{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
assert.Equal(t, expectedOutput, transformer(input))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewTimeTransformer(t *testing.T) {
|
||||||
|
inStr := "2010-11-12T13:14:15-07:00"
|
||||||
|
inTime, err := time.Parse(time.RFC3339, inStr)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
location, err := time.LoadLocation("America/Los_Angeles")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
transformer := NewTimeTransformer(time.RFC3339, location)
|
||||||
|
expected := "2010-11-12T12:14:15-08:00"
|
||||||
|
assert.Equal(t, expected, transformer(inStr))
|
||||||
|
assert.Equal(t, expected, transformer(inTime))
|
||||||
|
for _, possibleTimeLayout := range possibleTimeLayouts {
|
||||||
|
assert.Equal(t, expected, transformer(inTime.Format(possibleTimeLayout)), possibleTimeLayout)
|
||||||
|
}
|
||||||
|
|
||||||
|
location, err = time.LoadLocation("Asia/Singapore")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
transformer = NewTimeTransformer(time.UnixDate, location)
|
||||||
|
expected = "Sat Nov 13 04:14:15 +08 2010"
|
||||||
|
assert.Equal(t, expected, transformer(inStr))
|
||||||
|
assert.Equal(t, expected, transformer(inTime))
|
||||||
|
for _, possibleTimeLayout := range possibleTimeLayouts {
|
||||||
|
assert.Equal(t, expected, transformer(inTime.Format(possibleTimeLayout)), possibleTimeLayout)
|
||||||
|
}
|
||||||
|
|
||||||
|
location, err = time.LoadLocation("Europe/London")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
transformer = NewTimeTransformer(time.RFC3339, location)
|
||||||
|
expected = "2010-11-12T20:14:15Z"
|
||||||
|
assert.Equal(t, expected, transformer(inStr))
|
||||||
|
assert.Equal(t, expected, transformer(inTime))
|
||||||
|
for _, possibleTimeLayout := range possibleTimeLayouts {
|
||||||
|
assert.Equal(t, expected, transformer(inTime.Format(possibleTimeLayout)), possibleTimeLayout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewUnixTimeTransformer(t *testing.T) {
|
||||||
|
inStr := "2010-11-12T13:14:15-07:00"
|
||||||
|
inTime, err := time.Parse(time.RFC3339, inStr)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
inUnixTime := inTime.Unix()
|
||||||
|
|
||||||
|
location, err := time.LoadLocation("America/Los_Angeles")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
transformer := NewUnixTimeTransformer(time.RFC3339, location)
|
||||||
|
expected := "2010-11-12T12:14:15-08:00"
|
||||||
|
assert.Equal(t, expected, transformer(fmt.Sprint(inUnixTime)), "seconds in string")
|
||||||
|
assert.Equal(t, expected, transformer(inUnixTime), "seconds")
|
||||||
|
assert.Equal(t, expected, transformer(inUnixTime*1000), "milliseconds")
|
||||||
|
assert.Equal(t, expected, transformer(inUnixTime*1000000), "microseconds")
|
||||||
|
assert.Equal(t, expected, transformer(inUnixTime*1000000000), "nanoseconds")
|
||||||
|
|
||||||
|
location, err = time.LoadLocation("Asia/Singapore")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
transformer = NewUnixTimeTransformer(time.UnixDate, location)
|
||||||
|
expected = "Sat Nov 13 04:14:15 +08 2010"
|
||||||
|
assert.Equal(t, expected, transformer(fmt.Sprint(inUnixTime)), "seconds in string")
|
||||||
|
assert.Equal(t, expected, transformer(inUnixTime), "seconds")
|
||||||
|
assert.Equal(t, expected, transformer(inUnixTime*1000), "milliseconds")
|
||||||
|
assert.Equal(t, expected, transformer(inUnixTime*1000000), "microseconds")
|
||||||
|
assert.Equal(t, expected, transformer(inUnixTime*1000000000), "nanoseconds")
|
||||||
|
|
||||||
|
location, err = time.LoadLocation("Europe/London")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
transformer = NewUnixTimeTransformer(time.RFC3339, location)
|
||||||
|
expected = "2010-11-12T20:14:15Z"
|
||||||
|
assert.Equal(t, expected, transformer(fmt.Sprint(inUnixTime)), "seconds in string")
|
||||||
|
assert.Equal(t, expected, transformer(inUnixTime), "seconds")
|
||||||
|
assert.Equal(t, expected, transformer(inUnixTime*1000), "milliseconds")
|
||||||
|
assert.Equal(t, expected, transformer(inUnixTime*1000000), "microseconds")
|
||||||
|
assert.Equal(t, expected, transformer(inUnixTime*1000000000), "nanoseconds")
|
||||||
|
|
||||||
|
assert.Equal(t, "0.123456", transformer(float32(0.123456)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewURLTransformer(t *testing.T) {
|
||||||
|
url := "https://winter.is.coming"
|
||||||
|
|
||||||
|
transformer := NewURLTransformer()
|
||||||
|
assert.Equal(t, colorsURL.Sprint(url), transformer(url))
|
||||||
|
|
||||||
|
transformer2 := NewURLTransformer(FgRed, BgWhite, Bold)
|
||||||
|
assert.Equal(t, Colors{FgRed, BgWhite, Bold}.Sprint(url), transformer2(url))
|
||||||
|
assert.Equal(t, colorsURL.Sprint(url), transformer(url))
|
||||||
|
}
|
67
helper/text/valign.go
Normal file
67
helper/text/valign.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// VAlign denotes how text is to be aligned vertically.
|
||||||
|
type VAlign int
|
||||||
|
|
||||||
|
// VAlign enumerations
|
||||||
|
const (
|
||||||
|
VAlignDefault VAlign = iota // same as VAlignTop
|
||||||
|
VAlignTop // "top\n\n"
|
||||||
|
VAlignMiddle // "\nmiddle\n"
|
||||||
|
VAlignBottom // "\n\nbottom"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Apply aligns the lines vertically. For ex.:
|
||||||
|
// * VAlignTop.Apply({"Game", "Of", "Thrones"}, 5)
|
||||||
|
// returns {"Game", "Of", "Thrones", "", ""}
|
||||||
|
// * VAlignMiddle.Apply({"Game", "Of", "Thrones"}, 5)
|
||||||
|
// returns {"", "Game", "Of", "Thrones", ""}
|
||||||
|
// * VAlignBottom.Apply({"Game", "Of", "Thrones"}, 5)
|
||||||
|
// returns {"", "", "Game", "Of", "Thrones"}
|
||||||
|
func (va VAlign) Apply(lines []string, maxLines int) []string {
|
||||||
|
if len(lines) == maxLines {
|
||||||
|
return lines
|
||||||
|
} else if len(lines) > maxLines {
|
||||||
|
maxLines = len(lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
insertIdx := 0
|
||||||
|
if va == VAlignMiddle {
|
||||||
|
insertIdx = int(maxLines-len(lines)) / 2
|
||||||
|
} else if va == VAlignBottom {
|
||||||
|
insertIdx = maxLines - len(lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
linesOut := strings.Split(strings.Repeat("\n", maxLines-1), "\n")
|
||||||
|
for idx, line := range lines {
|
||||||
|
linesOut[idx+insertIdx] = line
|
||||||
|
}
|
||||||
|
return linesOut
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyStr aligns the string (of 1 or more lines) vertically. For ex.:
|
||||||
|
// * VAlignTop.ApplyStr("Game\nOf\nThrones", 5)
|
||||||
|
// returns {"Game", "Of", "Thrones", "", ""}
|
||||||
|
// * VAlignMiddle.ApplyStr("Game\nOf\nThrones", 5)
|
||||||
|
// returns {"", "Game", "Of", "Thrones", ""}
|
||||||
|
// * VAlignBottom.ApplyStr("Game\nOf\nThrones", 5)
|
||||||
|
// returns {"", "", "Game", "Of", "Thrones"}
|
||||||
|
func (va VAlign) ApplyStr(text string, maxLines int) []string {
|
||||||
|
return va.Apply(strings.Split(text, "\n"), maxLines)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTMLProperty returns the equivalent HTML vertical-align tag property.
|
||||||
|
func (va VAlign) HTMLProperty() string {
|
||||||
|
switch va {
|
||||||
|
case VAlignTop:
|
||||||
|
return "valign=\"top\""
|
||||||
|
case VAlignMiddle:
|
||||||
|
return "valign=\"middle\""
|
||||||
|
case VAlignBottom:
|
||||||
|
return "valign=\"bottom\""
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
101
helper/text/valign_test.go
Normal file
101
helper/text/valign_test.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleVAlign_Apply() {
|
||||||
|
lines := []string{"Game", "Of", "Thrones"}
|
||||||
|
maxLines := 5
|
||||||
|
fmt.Printf("VAlignDefault: %#v\n", VAlignDefault.Apply(lines, maxLines))
|
||||||
|
fmt.Printf("VAlignTop : %#v\n", VAlignTop.Apply(lines, maxLines))
|
||||||
|
fmt.Printf("VAlignMiddle : %#v\n", VAlignMiddle.Apply(lines, maxLines))
|
||||||
|
fmt.Printf("VAlignBottom : %#v\n", VAlignBottom.Apply(lines, maxLines))
|
||||||
|
|
||||||
|
// Output: VAlignDefault: []string{"Game", "Of", "Thrones", "", ""}
|
||||||
|
// VAlignTop : []string{"Game", "Of", "Thrones", "", ""}
|
||||||
|
// VAlignMiddle : []string{"", "Game", "Of", "Thrones", ""}
|
||||||
|
// VAlignBottom : []string{"", "", "Game", "Of", "Thrones"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVAlign_Apply(t *testing.T) {
|
||||||
|
assert.Equal(t, []string{"Game", "Of", "Thrones"},
|
||||||
|
VAlignDefault.Apply([]string{"Game", "Of", "Thrones"}, 1))
|
||||||
|
assert.Equal(t, []string{"Game", "Of", "Thrones"},
|
||||||
|
VAlignDefault.Apply([]string{"Game", "Of", "Thrones"}, 3))
|
||||||
|
assert.Equal(t, []string{"Game", "Of", "Thrones", "", ""},
|
||||||
|
VAlignDefault.Apply([]string{"Game", "Of", "Thrones"}, 5))
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"Game", "Of", "Thrones"},
|
||||||
|
VAlignTop.Apply([]string{"Game", "Of", "Thrones"}, 1))
|
||||||
|
assert.Equal(t, []string{"Game", "Of", "Thrones"},
|
||||||
|
VAlignTop.Apply([]string{"Game", "Of", "Thrones"}, 3))
|
||||||
|
assert.Equal(t, []string{"Game", "Of", "Thrones", "", ""},
|
||||||
|
VAlignTop.Apply([]string{"Game", "Of", "Thrones"}, 5))
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"Game", "Of", "Thrones"},
|
||||||
|
VAlignMiddle.Apply([]string{"Game", "Of", "Thrones"}, 1))
|
||||||
|
assert.Equal(t, []string{"Game", "Of", "Thrones"},
|
||||||
|
VAlignMiddle.Apply([]string{"Game", "Of", "Thrones"}, 3))
|
||||||
|
assert.Equal(t, []string{"", "Game", "Of", "Thrones", ""},
|
||||||
|
VAlignMiddle.Apply([]string{"Game", "Of", "Thrones"}, 5))
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"Game", "Of", "Thrones"},
|
||||||
|
VAlignBottom.Apply([]string{"Game", "Of", "Thrones"}, 1))
|
||||||
|
assert.Equal(t, []string{"Game", "Of", "Thrones"},
|
||||||
|
VAlignBottom.Apply([]string{"Game", "Of", "Thrones"}, 3))
|
||||||
|
assert.Equal(t, []string{"", "", "Game", "Of", "Thrones"},
|
||||||
|
VAlignBottom.Apply([]string{"Game", "Of", "Thrones"}, 5))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleVAlign_ApplyStr() {
|
||||||
|
str := "Game\nOf\nThrones"
|
||||||
|
maxLines := 5
|
||||||
|
fmt.Printf("VAlignDefault: %#v\n", VAlignDefault.ApplyStr(str, maxLines))
|
||||||
|
fmt.Printf("VAlignTop : %#v\n", VAlignTop.ApplyStr(str, maxLines))
|
||||||
|
fmt.Printf("VAlignMiddle : %#v\n", VAlignMiddle.ApplyStr(str, maxLines))
|
||||||
|
fmt.Printf("VAlignBottom : %#v\n", VAlignBottom.ApplyStr(str, maxLines))
|
||||||
|
|
||||||
|
// Output: VAlignDefault: []string{"Game", "Of", "Thrones", "", ""}
|
||||||
|
// VAlignTop : []string{"Game", "Of", "Thrones", "", ""}
|
||||||
|
// VAlignMiddle : []string{"", "Game", "Of", "Thrones", ""}
|
||||||
|
// VAlignBottom : []string{"", "", "Game", "Of", "Thrones"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVAlign_ApplyStr(t *testing.T) {
|
||||||
|
assert.Equal(t, []string{"Game", "Of", "Thrones", "", ""},
|
||||||
|
VAlignDefault.ApplyStr("Game\nOf\nThrones", 5))
|
||||||
|
assert.Equal(t, []string{"Game", "Of", "Thrones", "", ""},
|
||||||
|
VAlignTop.ApplyStr("Game\nOf\nThrones", 5))
|
||||||
|
assert.Equal(t, []string{"", "Game", "Of", "Thrones", ""},
|
||||||
|
VAlignMiddle.ApplyStr("Game\nOf\nThrones", 5))
|
||||||
|
assert.Equal(t, []string{"", "", "Game", "Of", "Thrones"},
|
||||||
|
VAlignBottom.ApplyStr("Game\nOf\nThrones", 5))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleVAlign_HTMLProperty() {
|
||||||
|
fmt.Printf("VAlignDefault: '%s'\n", VAlignDefault.HTMLProperty())
|
||||||
|
fmt.Printf("VAlignTop : '%s'\n", VAlignTop.HTMLProperty())
|
||||||
|
fmt.Printf("VAlignMiddle : '%s'\n", VAlignMiddle.HTMLProperty())
|
||||||
|
fmt.Printf("VAlignBottom : '%s'\n", VAlignBottom.HTMLProperty())
|
||||||
|
|
||||||
|
// Output: VAlignDefault: ''
|
||||||
|
// VAlignTop : 'valign="top"'
|
||||||
|
// VAlignMiddle : 'valign="middle"'
|
||||||
|
// VAlignBottom : 'valign="bottom"'
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVAlign_HTMLProperty(t *testing.T) {
|
||||||
|
vAligns := map[VAlign]string{
|
||||||
|
VAlignDefault: "",
|
||||||
|
VAlignTop: "top",
|
||||||
|
VAlignMiddle: "middle",
|
||||||
|
VAlignBottom: "bottom",
|
||||||
|
}
|
||||||
|
for vAlign, htmlStyle := range vAligns {
|
||||||
|
assert.Contains(t, vAlign.HTMLProperty(), htmlStyle)
|
||||||
|
}
|
||||||
|
}
|
266
helper/text/wrap.go
Normal file
266
helper/text/wrap.go
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WrapHard wraps a string to the given length using a newline. Handles strings
|
||||||
|
// with ANSI escape sequences (such as text color) without breaking the text
|
||||||
|
// formatting. Breaks all words that go beyond the line boundary.
|
||||||
|
//
|
||||||
|
// For examples, refer to the unit-tests or GoDoc examples.
|
||||||
|
func WrapHard(str string, wrapLen int) string {
|
||||||
|
if wrapLen <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
str = strings.Replace(str, "\t", " ", -1)
|
||||||
|
sLen := utf8.RuneCountInString(str)
|
||||||
|
if sLen <= wrapLen {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
out := &strings.Builder{}
|
||||||
|
out.Grow(sLen + (sLen / wrapLen))
|
||||||
|
for idx, paragraph := range strings.Split(str, "\n\n") {
|
||||||
|
if idx > 0 {
|
||||||
|
out.WriteString("\n\n")
|
||||||
|
}
|
||||||
|
wrapHard(paragraph, wrapLen, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapSoft wraps a string to the given length using a newline. Handles strings
|
||||||
|
// with ANSI escape sequences (such as text color) without breaking the text
|
||||||
|
// formatting. Tries to move words that go beyond the line boundary to the next
|
||||||
|
// line.
|
||||||
|
//
|
||||||
|
// For examples, refer to the unit-tests or GoDoc examples.
|
||||||
|
func WrapSoft(str string, wrapLen int) string {
|
||||||
|
if wrapLen <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
str = strings.Replace(str, "\t", " ", -1)
|
||||||
|
sLen := utf8.RuneCountInString(str)
|
||||||
|
if sLen <= wrapLen {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
out := &strings.Builder{}
|
||||||
|
out.Grow(sLen + (sLen / wrapLen))
|
||||||
|
for idx, paragraph := range strings.Split(str, "\n\n") {
|
||||||
|
if idx > 0 {
|
||||||
|
out.WriteString("\n\n")
|
||||||
|
}
|
||||||
|
wrapSoft(paragraph, wrapLen, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapText is very similar to WrapHard except for one minor difference. Unlike
|
||||||
|
// WrapHard which discards line-breaks and respects only paragraph-breaks, this
|
||||||
|
// function respects line-breaks too.
|
||||||
|
//
|
||||||
|
// For examples, refer to the unit-tests or GoDoc examples.
|
||||||
|
func WrapText(str string, wrapLen int) string {
|
||||||
|
if wrapLen <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var out strings.Builder
|
||||||
|
sLen := utf8.RuneCountInString(str)
|
||||||
|
out.Grow(sLen + (sLen / wrapLen))
|
||||||
|
lineIdx, isEscSeq, lastEscSeq := 0, false, ""
|
||||||
|
for _, char := range str {
|
||||||
|
if char == EscapeStartRune {
|
||||||
|
isEscSeq = true
|
||||||
|
lastEscSeq = ""
|
||||||
|
}
|
||||||
|
if isEscSeq {
|
||||||
|
lastEscSeq += string(char)
|
||||||
|
}
|
||||||
|
|
||||||
|
appendChar(char, wrapLen, &lineIdx, isEscSeq, lastEscSeq, &out)
|
||||||
|
|
||||||
|
if isEscSeq && char == EscapeStopRune {
|
||||||
|
isEscSeq = false
|
||||||
|
}
|
||||||
|
if lastEscSeq == EscapeReset {
|
||||||
|
lastEscSeq = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lastEscSeq != "" && lastEscSeq != EscapeReset {
|
||||||
|
out.WriteString(EscapeReset)
|
||||||
|
}
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendChar(char rune, wrapLen int, lineLen *int, inEscSeq bool, lastSeenEscSeq string, out *strings.Builder) {
|
||||||
|
// handle reaching the end of the line as dictated by wrapLen or by finding
|
||||||
|
// a newline character
|
||||||
|
if (*lineLen == wrapLen && !inEscSeq && char != '\n') || (char == '\n') {
|
||||||
|
if lastSeenEscSeq != "" {
|
||||||
|
// terminate escape sequence and the line; and restart the escape
|
||||||
|
// sequence in the next line
|
||||||
|
out.WriteString(EscapeReset)
|
||||||
|
out.WriteRune('\n')
|
||||||
|
out.WriteString(lastSeenEscSeq)
|
||||||
|
} else {
|
||||||
|
// just start a new line
|
||||||
|
out.WriteRune('\n')
|
||||||
|
}
|
||||||
|
// reset line index to 0th character
|
||||||
|
*lineLen = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the rune is not a new line, output it
|
||||||
|
if char != '\n' {
|
||||||
|
out.WriteRune(char)
|
||||||
|
|
||||||
|
// increment the line index if not in the middle of an escape sequence
|
||||||
|
if !inEscSeq {
|
||||||
|
*lineLen++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendWord(word string, lineIdx *int, lastSeenEscSeq string, wrapLen int, out *strings.Builder) {
|
||||||
|
inEscSeq := false
|
||||||
|
for _, char := range word {
|
||||||
|
if char == EscapeStartRune {
|
||||||
|
inEscSeq = true
|
||||||
|
lastSeenEscSeq = ""
|
||||||
|
}
|
||||||
|
if inEscSeq {
|
||||||
|
lastSeenEscSeq += string(char)
|
||||||
|
}
|
||||||
|
|
||||||
|
appendChar(char, wrapLen, lineIdx, inEscSeq, lastSeenEscSeq, out)
|
||||||
|
|
||||||
|
if inEscSeq && char == EscapeStopRune {
|
||||||
|
inEscSeq = false
|
||||||
|
}
|
||||||
|
if lastSeenEscSeq == EscapeReset {
|
||||||
|
lastSeenEscSeq = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractOpenEscapeSeq(str string) string {
|
||||||
|
escapeSeq, inEscSeq := "", false
|
||||||
|
for _, char := range str {
|
||||||
|
if char == EscapeStartRune {
|
||||||
|
inEscSeq = true
|
||||||
|
escapeSeq = ""
|
||||||
|
}
|
||||||
|
if inEscSeq {
|
||||||
|
escapeSeq += string(char)
|
||||||
|
}
|
||||||
|
if char == EscapeStopRune {
|
||||||
|
inEscSeq = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if escapeSeq == EscapeReset {
|
||||||
|
escapeSeq = ""
|
||||||
|
}
|
||||||
|
return escapeSeq
|
||||||
|
}
|
||||||
|
|
||||||
|
func terminateLine(wrapLen int, lineLen *int, lastSeenEscSeq string, out *strings.Builder) {
|
||||||
|
if *lineLen < wrapLen {
|
||||||
|
out.WriteString(strings.Repeat(" ", wrapLen-*lineLen))
|
||||||
|
}
|
||||||
|
// something is already on the line; terminate it
|
||||||
|
if lastSeenEscSeq != "" {
|
||||||
|
out.WriteString(EscapeReset)
|
||||||
|
}
|
||||||
|
out.WriteRune('\n')
|
||||||
|
out.WriteString(lastSeenEscSeq)
|
||||||
|
*lineLen = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func terminateOutput(lastSeenEscSeq string, out *strings.Builder) {
|
||||||
|
if lastSeenEscSeq != "" && lastSeenEscSeq != EscapeReset && !strings.HasSuffix(out.String(), EscapeReset) {
|
||||||
|
out.WriteString(EscapeReset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapHard(paragraph string, wrapLen int, out *strings.Builder) {
|
||||||
|
lineLen, lastSeenEscSeq := 0, ""
|
||||||
|
words := strings.Fields(paragraph)
|
||||||
|
for wordIdx, word := range words {
|
||||||
|
escSeq := extractOpenEscapeSeq(word)
|
||||||
|
if escSeq != "" {
|
||||||
|
lastSeenEscSeq = escSeq
|
||||||
|
}
|
||||||
|
if lineLen > 0 {
|
||||||
|
out.WriteRune(' ')
|
||||||
|
lineLen++
|
||||||
|
}
|
||||||
|
|
||||||
|
wordLen := RuneWidthWithoutEscSequences(word)
|
||||||
|
if lineLen+wordLen <= wrapLen { // word fits within the line
|
||||||
|
out.WriteString(word)
|
||||||
|
lineLen += wordLen
|
||||||
|
} else { // word doesn't fit within the line; hard-wrap
|
||||||
|
appendWord(word, &lineLen, lastSeenEscSeq, wrapLen, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// end of line; but more words incoming
|
||||||
|
if lineLen == wrapLen && wordIdx < len(words)-1 {
|
||||||
|
terminateLine(wrapLen, &lineLen, lastSeenEscSeq, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
terminateOutput(lastSeenEscSeq, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapSoft(paragraph string, wrapLen int, out *strings.Builder) {
|
||||||
|
lineLen, lastSeenEscSeq := 0, ""
|
||||||
|
words := strings.Fields(paragraph)
|
||||||
|
for wordIdx, word := range words {
|
||||||
|
escSeq := extractOpenEscapeSeq(word)
|
||||||
|
if escSeq != "" {
|
||||||
|
lastSeenEscSeq = escSeq
|
||||||
|
}
|
||||||
|
|
||||||
|
spacing, spacingLen := wrapSoftSpacing(lineLen)
|
||||||
|
wordLen := RuneWidthWithoutEscSequences(word)
|
||||||
|
if lineLen+spacingLen+wordLen <= wrapLen { // word fits within the line
|
||||||
|
out.WriteString(spacing)
|
||||||
|
out.WriteString(word)
|
||||||
|
lineLen += spacingLen + wordLen
|
||||||
|
} else { // word doesn't fit within the line
|
||||||
|
lineLen = wrapSoftLastWordInLine(wrapLen, lineLen, lastSeenEscSeq, wordLen, word, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// end of line; but more words incoming
|
||||||
|
if lineLen == wrapLen && wordIdx < len(words)-1 {
|
||||||
|
terminateLine(wrapLen, &lineLen, lastSeenEscSeq, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
terminateOutput(lastSeenEscSeq, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapSoftLastWordInLine(wrapLen int, lineLen int, lastSeenEscSeq string, wordLen int, word string, out *strings.Builder) int {
|
||||||
|
if lineLen > 0 { // something is already on the line; terminate it
|
||||||
|
terminateLine(wrapLen, &lineLen, lastSeenEscSeq, out)
|
||||||
|
}
|
||||||
|
if wordLen <= wrapLen { // word fits within a single line
|
||||||
|
out.WriteString(word)
|
||||||
|
lineLen = wordLen
|
||||||
|
} else { // word doesn't fit within a single line; hard-wrap
|
||||||
|
appendWord(word, &lineLen, lastSeenEscSeq, wrapLen, out)
|
||||||
|
}
|
||||||
|
return lineLen
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapSoftSpacing(lineLen int) (string, int) {
|
||||||
|
spacing, spacingLen := "", 0
|
||||||
|
if lineLen > 0 {
|
||||||
|
spacing, spacingLen = " ", 1
|
||||||
|
}
|
||||||
|
return spacing, spacingLen
|
||||||
|
}
|
147
helper/text/wrap_test.go
Normal file
147
helper/text/wrap_test.go
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleWrapHard() {
|
||||||
|
str := `The quick brown fox jumped over the lazy dog.
|
||||||
|
|
||||||
|
A big crocodile died empty-fanged, gulping horribly in jerking kicking little
|
||||||
|
motions. Nonchalant old Peter Quinn ruthlessly shot the under-water vermin with
|
||||||
|
Xavier yelling Zap!`
|
||||||
|
strWrapped := WrapHard(str, 30)
|
||||||
|
for idx, line := range strings.Split(strWrapped, "\n") {
|
||||||
|
fmt.Printf("Line #%02d: '%s'\n", idx+1, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output: Line #01: 'The quick brown fox jumped ove'
|
||||||
|
// Line #02: 'r the lazy dog.'
|
||||||
|
// Line #03: ''
|
||||||
|
// Line #04: 'A big crocodile died empty-fan'
|
||||||
|
// Line #05: 'ged, gulping horribly in jerki'
|
||||||
|
// Line #06: 'ng kicking little motions. Non'
|
||||||
|
// Line #07: 'chalant old Peter Quinn ruthle'
|
||||||
|
// Line #08: 'ssly shot the under-water verm'
|
||||||
|
// Line #09: 'in with Xavier yelling Zap!'
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrapHard(t *testing.T) {
|
||||||
|
assert.Equal(t, "", WrapHard("Ghost", 0))
|
||||||
|
assert.Equal(t, "G\nh\no\ns\nt", WrapHard("Ghost", 1))
|
||||||
|
assert.Equal(t, "Gh\nos\nt", WrapHard("Ghost", 2))
|
||||||
|
assert.Equal(t, "Gho\nst", WrapHard("Ghost", 3))
|
||||||
|
assert.Equal(t, "Ghos\nt", WrapHard("Ghost", 4))
|
||||||
|
assert.Equal(t, "Ghost", WrapHard("Ghost", 5))
|
||||||
|
assert.Equal(t, "Ghost", WrapHard("Ghost", 6))
|
||||||
|
assert.Equal(t, "Jo\nn \nSn\now", WrapHard("Jon\nSnow", 2))
|
||||||
|
assert.Equal(t, "Jo\nn \nSn\now", WrapHard("Jon\nSnow\n", 2))
|
||||||
|
assert.Equal(t, "Jon\nSno\nw", WrapHard("Jon\nSnow\n", 3))
|
||||||
|
assert.Equal(t, "Jon i\ns a S\nnow", WrapHard("Jon is a Snow", 5))
|
||||||
|
assert.Equal(t, "\x1b[33mJon\x1b[0m\nSno\nw", WrapHard("\x1b[33mJon\x1b[0m\nSnow", 3))
|
||||||
|
assert.Equal(t, "\x1b[33mJon\x1b[0m\nSno\nw", WrapHard("\x1b[33mJon\x1b[0m\nSnow\n", 3))
|
||||||
|
assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33mSno\x1b[0m\n\x1b[33mw\x1b[0m", WrapHard("\x1b[33mJon Snow\x1b[0m", 3))
|
||||||
|
assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33mSno\x1b[0m\n\x1b[33mw\x1b[0m", WrapHard("\x1b[33mJon Snow\n", 3))
|
||||||
|
assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33mSno\x1b[0m\n\x1b[33mw \x1b[0m", WrapHard("\x1b[33mJon Snow\n\x1b[0m", 3))
|
||||||
|
|
||||||
|
complexIn := "+---+------+-------+------+\n| 1 | Arya | Stark | 3000 |\n+---+------+-------+------+"
|
||||||
|
assert.Equal(t, complexIn, WrapHard(complexIn, 27))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleWrapSoft() {
|
||||||
|
str := `The quick brown fox jumped over the lazy dog.
|
||||||
|
|
||||||
|
A big crocodile died empty-fanged, gulping horribly in jerking kicking little
|
||||||
|
motions. Nonchalant old Peter Quinn ruthlessly shot the under-water vermin with
|
||||||
|
Xavier yelling Zap!`
|
||||||
|
strWrapped := WrapSoft(str, 30)
|
||||||
|
for idx, line := range strings.Split(strWrapped, "\n") {
|
||||||
|
fmt.Printf("Line #%02d: '%s'\n", idx+1, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output: Line #01: 'The quick brown fox jumped '
|
||||||
|
// Line #02: 'over the lazy dog.'
|
||||||
|
// Line #03: ''
|
||||||
|
// Line #04: 'A big crocodile died '
|
||||||
|
// Line #05: 'empty-fanged, gulping horribly'
|
||||||
|
// Line #06: 'in jerking kicking little '
|
||||||
|
// Line #07: 'motions. Nonchalant old Peter '
|
||||||
|
// Line #08: 'Quinn ruthlessly shot the '
|
||||||
|
// Line #09: 'under-water vermin with Xavier'
|
||||||
|
// Line #10: 'yelling Zap!'
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrapSoft(t *testing.T) {
|
||||||
|
assert.Equal(t, "", WrapSoft("Ghost", 0))
|
||||||
|
assert.Equal(t, "G\nh\no\ns\nt", WrapSoft("Ghost", 1))
|
||||||
|
assert.Equal(t, "Gh\nos\nt", WrapSoft("Ghost", 2))
|
||||||
|
assert.Equal(t, "Gho\nst", WrapSoft("Ghost", 3))
|
||||||
|
assert.Equal(t, "Ghos\nt", WrapSoft("Ghost", 4))
|
||||||
|
assert.Equal(t, "Ghost", WrapSoft("Ghost", 5))
|
||||||
|
assert.Equal(t, "Ghost", WrapSoft("Ghost", 6))
|
||||||
|
assert.Equal(t, "Jo\nn \nSn\now", WrapSoft("Jon\nSnow", 2))
|
||||||
|
assert.Equal(t, "Jo\nn \nSn\now", WrapSoft("Jon\nSnow\n", 2))
|
||||||
|
assert.Equal(t, "Jon\nSno\nw", WrapSoft("Jon\nSnow\n", 3))
|
||||||
|
assert.Equal(t, "Jon \nSnow", WrapSoft("Jon\nSnow", 4))
|
||||||
|
assert.Equal(t, "Jon \nSnow", WrapSoft("Jon\nSnow\n", 4))
|
||||||
|
assert.Equal(t, "Jon \nis a \nSnow", WrapSoft("Jon is a Snow", 5))
|
||||||
|
assert.Equal(t, "\x1b[33mJon\x1b[0m\nSno\nw", WrapSoft("\x1b[33mJon\x1b[0m\nSnow", 3))
|
||||||
|
assert.Equal(t, "\x1b[33mJon\x1b[0m\nSno\nw", WrapSoft("\x1b[33mJon\x1b[0m\nSnow\n", 3))
|
||||||
|
assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33mSno\x1b[0m\n\x1b[33mw\x1b[0m", WrapSoft("\x1b[33mJon Snow\x1b[0m", 3))
|
||||||
|
assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33mSno\x1b[0m\n\x1b[33mw\x1b[0m", WrapSoft("\x1b[33mJon Snow\n", 3))
|
||||||
|
assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33mSno\x1b[0m\n\x1b[33mw \x1b[0m", WrapSoft("\x1b[33mJon Snow\n\x1b[0m", 3))
|
||||||
|
|
||||||
|
complexIn := "+---+------+-------+------+\n| 1 | Arya | Stark | 3000 |\n+---+------+-------+------+"
|
||||||
|
assert.Equal(t, complexIn, WrapSoft(complexIn, 27))
|
||||||
|
|
||||||
|
assert.Equal(t, "\x1b[33mJon \x1b[0m\n\x1b[33mSnow\x1b[0m", WrapSoft("\x1b[33mJon Snow\x1b[0m", 4))
|
||||||
|
assert.Equal(t, "\x1b[33mJon \x1b[0m\n\x1b[33mSnow\x1b[0m\n\x1b[33m???\x1b[0m", WrapSoft("\x1b[33mJon Snow???\x1b[0m", 4))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleWrapText() {
|
||||||
|
str := `The quick brown fox jumped over the lazy dog.
|
||||||
|
|
||||||
|
A big crocodile died empty-fanged, gulping horribly in jerking kicking little
|
||||||
|
motions. Nonchalant old Peter Quinn ruthlessly shot the under-water vermin with
|
||||||
|
Xavier yelling Zap!`
|
||||||
|
strWrapped := WrapText(str, 30)
|
||||||
|
for idx, line := range strings.Split(strWrapped, "\n") {
|
||||||
|
fmt.Printf("Line #%02d: '%s'\n", idx+1, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output: Line #01: 'The quick brown fox jumped ove'
|
||||||
|
// Line #02: 'r the lazy dog.'
|
||||||
|
// Line #03: ''
|
||||||
|
// Line #04: 'A big crocodile died empty-fan'
|
||||||
|
// Line #05: 'ged, gulping horribly in jerki'
|
||||||
|
// Line #06: 'ng kicking little'
|
||||||
|
// Line #07: 'motions. Nonchalant old Peter '
|
||||||
|
// Line #08: 'Quinn ruthlessly shot the unde'
|
||||||
|
// Line #09: 'r-water vermin with'
|
||||||
|
// Line #10: 'Xavier yelling Zap!'
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrapText(t *testing.T) {
|
||||||
|
assert.Equal(t, "", WrapText("Ghost", 0))
|
||||||
|
assert.Equal(t, "G\nh\no\ns\nt", WrapText("Ghost", 1))
|
||||||
|
assert.Equal(t, "Gh\nos\nt", WrapText("Ghost", 2))
|
||||||
|
assert.Equal(t, "Gho\nst", WrapText("Ghost", 3))
|
||||||
|
assert.Equal(t, "Ghos\nt", WrapText("Ghost", 4))
|
||||||
|
assert.Equal(t, "Ghost", WrapText("Ghost", 5))
|
||||||
|
assert.Equal(t, "Ghost", WrapText("Ghost", 6))
|
||||||
|
assert.Equal(t, "Jo\nn\nSn\now", WrapText("Jon\nSnow", 2))
|
||||||
|
assert.Equal(t, "Jo\nn\nSn\now\n", WrapText("Jon\nSnow\n", 2))
|
||||||
|
assert.Equal(t, "Jon\nSno\nw\n", WrapText("Jon\nSnow\n", 3))
|
||||||
|
assert.Equal(t, "\x1b[33mJon\x1b[0m\nSno\nw", WrapText("\x1b[33mJon\x1b[0m\nSnow", 3))
|
||||||
|
assert.Equal(t, "\x1b[33mJon\x1b[0m\nSno\nw\n", WrapText("\x1b[33mJon\x1b[0m\nSnow\n", 3))
|
||||||
|
assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33m Sn\x1b[0m\n\x1b[33mow\x1b[0m", WrapText("\x1b[33mJon Snow\x1b[0m", 3))
|
||||||
|
assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33m Sn\x1b[0m\n\x1b[33mow\x1b[0m\n\x1b[33m\x1b[0m", WrapText("\x1b[33mJon Snow\n", 3))
|
||||||
|
assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33m Sn\x1b[0m\n\x1b[33mow\x1b[0m\n\x1b[33m\x1b[0m", WrapText("\x1b[33mJon Snow\n\x1b[0m", 3))
|
||||||
|
|
||||||
|
complexIn := "+---+------+-------+------+\n| 1 | Arya | Stark | 3000 |\n+---+------+-------+------+"
|
||||||
|
assert.Equal(t, complexIn, WrapText(complexIn, 27))
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user