package tablewriter
import (
"bytes"
"fmt"
"io"
"regexp"
"strings"
)
const (
MAX_ROW_WIDTH = 30
)
const (
CENTER = "+"
ROW = "-"
COLUMN = "|"
SPACE = " "
NEWLINE = "\n"
)
const (
ALIGN_DEFAULT = iota
ALIGN_CENTER
ALIGN_RIGHT
ALIGN_LEFT
)
var (
decimal = regexp .MustCompile (`^-?(?:\d{1,3}(?:,\d{3})*|\d+)(?:\.\d+)?$` )
percent = regexp .MustCompile (`^-?\d+\.?\d*$%$` )
)
type Border struct {
Left bool
Right bool
Top bool
Bottom bool
}
type Table struct {
out io .Writer
rows [][]string
lines [][][]string
cs map [int ]int
rs map [int ]int
headers [][]string
footers [][]string
caption bool
captionText string
autoFmt bool
autoWrap bool
reflowText bool
mW int
pCenter string
pRow string
pColumn string
tColumn int
tRow int
hAlign int
fAlign int
align int
newLine string
rowLine bool
autoMergeCells bool
columnsToAutoMergeCells map [int ]bool
noWhiteSpace bool
tablePadding string
hdrLine bool
borders Border
colSize int
headerParams []string
columnsParams []string
footerParams []string
columnsAlign []int
}
func NewWriter (writer io .Writer ) *Table {
t := &Table {
out : writer ,
rows : [][]string {},
lines : [][][]string {},
cs : make (map [int ]int ),
rs : make (map [int ]int ),
headers : [][]string {},
footers : [][]string {},
caption : false ,
captionText : "Table caption." ,
autoFmt : true ,
autoWrap : true ,
reflowText : true ,
mW : MAX_ROW_WIDTH ,
pCenter : CENTER ,
pRow : ROW ,
pColumn : COLUMN ,
tColumn : -1 ,
tRow : -1 ,
hAlign : ALIGN_DEFAULT ,
fAlign : ALIGN_DEFAULT ,
align : ALIGN_DEFAULT ,
newLine : NEWLINE ,
rowLine : false ,
hdrLine : true ,
borders : Border {Left : true , Right : true , Bottom : true , Top : true },
colSize : -1 ,
headerParams : []string {},
columnsParams : []string {},
footerParams : []string {},
columnsAlign : []int {}}
return t
}
func (t *Table ) Render () {
if t .borders .Top {
t .printLine (true )
}
t .printHeading ()
if t .autoMergeCells {
t .printRowsMergeCells ()
} else {
t .printRows ()
}
if !t .rowLine && t .borders .Bottom {
t .printLine (true )
}
t .printFooter ()
if t .caption {
t .printCaption ()
}
}
const (
headerRowIdx = -1
footerRowIdx = -2
)
func (t *Table ) SetHeader (keys []string ) {
t .colSize = len (keys )
for i , v := range keys {
lines := t .parseDimension (v , i , headerRowIdx )
t .headers = append (t .headers , lines )
}
}
func (t *Table ) SetFooter (keys []string ) {
for i , v := range keys {
lines := t .parseDimension (v , i , footerRowIdx )
t .footers = append (t .footers , lines )
}
}
func (t *Table ) SetCaption (caption bool , captionText ...string ) {
t .caption = caption
if len (captionText ) == 1 {
t .captionText = captionText [0 ]
}
}
func (t *Table ) SetAutoFormatHeaders (auto bool ) {
t .autoFmt = auto
}
func (t *Table ) SetAutoWrapText (auto bool ) {
t .autoWrap = auto
}
func (t *Table ) SetReflowDuringAutoWrap (auto bool ) {
t .reflowText = auto
}
func (t *Table ) SetColWidth (width int ) {
t .mW = width
}
func (t *Table ) SetColMinWidth (column int , width int ) {
t .cs [column ] = width
}
func (t *Table ) SetColumnSeparator (sep string ) {
t .pColumn = sep
}
func (t *Table ) SetRowSeparator (sep string ) {
t .pRow = sep
}
func (t *Table ) SetCenterSeparator (sep string ) {
t .pCenter = sep
}
func (t *Table ) SetHeaderAlignment (hAlign int ) {
t .hAlign = hAlign
}
func (t *Table ) SetFooterAlignment (fAlign int ) {
t .fAlign = fAlign
}
func (t *Table ) SetAlignment (align int ) {
t .align = align
}
func (t *Table ) SetNoWhiteSpace (allow bool ) {
t .noWhiteSpace = allow
}
func (t *Table ) SetTablePadding (padding string ) {
t .tablePadding = padding
}
func (t *Table ) SetColumnAlignment (keys []int ) {
for _ , v := range keys {
switch v {
case ALIGN_CENTER :
break
case ALIGN_LEFT :
break
case ALIGN_RIGHT :
break
default :
v = ALIGN_DEFAULT
}
t .columnsAlign = append (t .columnsAlign , v )
}
}
func (t *Table ) SetNewLine (nl string ) {
t .newLine = nl
}
func (t *Table ) SetHeaderLine (line bool ) {
t .hdrLine = line
}
func (t *Table ) SetRowLine (line bool ) {
t .rowLine = line
}
func (t *Table ) SetAutoMergeCells (auto bool ) {
t .autoMergeCells = auto
}
func (t *Table ) SetAutoMergeCellsByColumnIndex (cols []int ) {
t .autoMergeCells = true
if len (cols ) > 0 {
m := make (map [int ]bool )
for _ , col := range cols {
m [col ] = true
}
t .columnsToAutoMergeCells = m
}
}
func (t *Table ) SetBorder (border bool ) {
t .SetBorders (Border {border , border , border , border })
}
func (t *Table ) SetBorders (border Border ) {
t .borders = border
}
func (t *Table ) Append (row []string ) {
rowSize := len (t .headers )
if rowSize > t .colSize {
t .colSize = rowSize
}
n := len (t .lines )
line := [][]string {}
for i , v := range row {
out := t .parseDimension (v , i , n )
line = append (line , out )
}
t .lines = append (t .lines , line )
}
func (t *Table ) Rich (row []string , colors []Colors ) {
rowSize := len (t .headers )
if rowSize > t .colSize {
t .colSize = rowSize
}
n := len (t .lines )
line := [][]string {}
for i , v := range row {
out := t .parseDimension (v , i , n )
if len (colors ) > i {
color := colors [i ]
out [0 ] = format (out [0 ], color )
}
line = append (line , out )
}
t .lines = append (t .lines , line )
}
func (t *Table ) AppendBulk (rows [][]string ) {
for _ , row := range rows {
t .Append (row )
}
}
func (t *Table ) NumLines () int {
return len (t .lines )
}
func (t *Table ) ClearRows () {
t .lines = [][][]string {}
}
func (t *Table ) ClearFooter () {
t .footers = [][]string {}
}
func (t *Table ) center (i int ) string {
if i == -1 && !t .borders .Left {
return t .pRow
}
if i == len (t .cs )-1 && !t .borders .Right {
return t .pRow
}
return t .pCenter
}
func (t *Table ) printLine (nl bool ) {
fmt .Fprint (t .out , t .center (-1 ))
for i := 0 ; i < len (t .cs ); i ++ {
v := t .cs [i ]
fmt .Fprintf (t .out , "%s%s%s%s" ,
t .pRow ,
strings .Repeat (string (t .pRow ), v ),
t .pRow ,
t .center (i ))
}
if nl {
fmt .Fprint (t .out , t .newLine )
}
}
func (t *Table ) printLineOptionalCellSeparators (nl bool , displayCellSeparator []bool ) {
fmt .Fprint (t .out , t .pCenter )
for i := 0 ; i < len (t .cs ); i ++ {
v := t .cs [i ]
if i > len (displayCellSeparator ) || displayCellSeparator [i ] {
fmt .Fprintf (t .out , "%s%s%s%s" ,
t .pRow ,
strings .Repeat (string (t .pRow ), v ),
t .pRow ,
t .pCenter )
} else {
fmt .Fprintf (t .out , "%s%s" ,
strings .Repeat (" " , v +2 ),
t .pCenter )
}
}
if nl {
fmt .Fprint (t .out , t .newLine )
}
}
func pad(align int ) func (string , string , int ) string {
padFunc := Pad
switch align {
case ALIGN_LEFT :
padFunc = PadRight
case ALIGN_RIGHT :
padFunc = PadLeft
}
return padFunc
}
func (t *Table ) printHeading () {
if len (t .headers ) < 1 {
return
}
end := len (t .cs ) - 1
padFunc := pad (t .hAlign )
is_esc_seq := false
if len (t .headerParams ) > 0 {
is_esc_seq = true
}
max := t .rs [headerRowIdx ]
for x := 0 ; x < max ; x ++ {
if !t .noWhiteSpace {
fmt .Fprint (t .out , ConditionString (t .borders .Left , t .pColumn , SPACE ))
}
for y := 0 ; y <= end ; y ++ {
v := t .cs [y ]
h := ""
if y < len (t .headers ) && x < len (t .headers [y ]) {
h = t .headers [y ][x ]
}
if t .autoFmt {
h = Title (h )
}
pad := ConditionString ((y == end && !t .borders .Left ), SPACE , t .pColumn )
if t .noWhiteSpace {
pad = ConditionString ((y == end && !t .borders .Left ), SPACE , t .tablePadding )
}
if is_esc_seq {
if !t .noWhiteSpace {
fmt .Fprintf (t .out , " %s %s" ,
format (padFunc (h , SPACE , v ),
t .headerParams [y ]), pad )
} else {
fmt .Fprintf (t .out , "%s %s" ,
format (padFunc (h , SPACE , v ),
t .headerParams [y ]), pad )
}
} else {
if !t .noWhiteSpace {
fmt .Fprintf (t .out , " %s %s" ,
padFunc (h , SPACE , v ),
pad )
} else {
fmt .Fprintf (t .out , "%s%s" ,
padFunc (h , SPACE , v ),
pad )
}
}
}
fmt .Fprint (t .out , t .newLine )
}
if t .hdrLine {
t .printLine (true )
}
}
func (t *Table ) printFooter () {
if len (t .footers ) < 1 {
return
}
if !t .borders .Bottom {
t .printLine (true )
}
end := len (t .cs ) - 1
padFunc := pad (t .fAlign )
is_esc_seq := false
if len (t .footerParams ) > 0 {
is_esc_seq = true
}
max := t .rs [footerRowIdx ]
erasePad := make ([]bool , len (t .footers ))
for x := 0 ; x < max ; x ++ {
fmt .Fprint (t .out , ConditionString (t .borders .Bottom , t .pColumn , SPACE ))
for y := 0 ; y <= end ; y ++ {
v := t .cs [y ]
f := ""
if y < len (t .footers ) && x < len (t .footers [y ]) {
f = t .footers [y ][x ]
}
if t .autoFmt {
f = Title (f )
}
pad := ConditionString ((y == end && !t .borders .Top ), SPACE , t .pColumn )
if erasePad [y ] || (x == 0 && len (f ) == 0 ) {
pad = SPACE
erasePad [y ] = true
}
if is_esc_seq {
fmt .Fprintf (t .out , " %s %s" ,
format (padFunc (f , SPACE , v ),
t .footerParams [y ]), pad )
} else {
fmt .Fprintf (t .out , " %s %s" ,
padFunc (f , SPACE , v ),
pad )
}
}
fmt .Fprint (t .out , t .newLine )
}
hasPrinted := false
for i := 0 ; i <= end ; i ++ {
v := t .cs [i ]
pad := t .pRow
center := t .pCenter
length := len (t .footers [i ][0 ])
if length > 0 {
hasPrinted = true
}
if length == 0 && !t .borders .Right {
center = SPACE
}
if i == 0 {
if length > 0 && !t .borders .Left {
center = t .pRow
}
fmt .Fprint (t .out , center )
}
if length == 0 {
pad = SPACE
}
if hasPrinted || t .borders .Left {
pad = t .pRow
center = t .pCenter
}
if center != SPACE {
if i == end && !t .borders .Right {
center = t .pRow
}
}
if center == SPACE {
if i < end && len (t .footers [i +1 ][0 ]) != 0 {
if !t .borders .Left {
center = t .pRow
} else {
center = t .pCenter
}
}
}
fmt .Fprintf (t .out , "%s%s%s%s" ,
pad ,
strings .Repeat (string (pad ), v ),
pad ,
center )
}
fmt .Fprint (t .out , t .newLine )
}
func (t Table ) printCaption () {
width := t .getTableWidth ()
paragraph , _ := WrapString (t .captionText , width )
for linecount := 0 ; linecount < len (paragraph ); linecount ++ {
fmt .Fprintln (t .out , paragraph [linecount ])
}
}
func (t Table ) getTableWidth () int {
var chars int
for _ , v := range t .cs {
chars += v
}
return (chars + (3 * t .colSize ) + 2 )
}
func (t Table ) printRows () {
for i , lines := range t .lines {
t .printRow (lines , i )
}
}
func (t *Table ) fillAlignment (num int ) {
if len (t .columnsAlign ) < num {
t .columnsAlign = make ([]int , num )
for i := range t .columnsAlign {
t .columnsAlign [i ] = t .align
}
}
}
func (t *Table ) printRow (columns [][]string , rowIdx int ) {
max := t .rs [rowIdx ]
total := len (columns )
pads := []int {}
is_esc_seq := false
if len (t .columnsParams ) > 0 {
is_esc_seq = true
}
t .fillAlignment (total )
for i , line := range columns {
length := len (line )
pad := max - length
pads = append (pads , pad )
for n := 0 ; n < pad ; n ++ {
columns [i ] = append (columns [i ], " " )
}
}
for x := 0 ; x < max ; x ++ {
for y := 0 ; y < total ; y ++ {
if !t .noWhiteSpace {
fmt .Fprint (t .out , ConditionString ((!t .borders .Left && y == 0 ), SPACE , t .pColumn ))
fmt .Fprintf (t .out , SPACE )
}
str := columns [y ][x ]
if is_esc_seq {
str = format (str , t .columnsParams [y ])
}
switch t .columnsAlign [y ] {
case ALIGN_CENTER :
fmt .Fprintf (t .out , "%s" , Pad (str , SPACE , t .cs [y ]))
case ALIGN_RIGHT :
fmt .Fprintf (t .out , "%s" , PadLeft (str , SPACE , t .cs [y ]))
case ALIGN_LEFT :
fmt .Fprintf (t .out , "%s" , PadRight (str , SPACE , t .cs [y ]))
default :
if decimal .MatchString (strings .TrimSpace (str )) || percent .MatchString (strings .TrimSpace (str )) {
fmt .Fprintf (t .out , "%s" , PadLeft (str , SPACE , t .cs [y ]))
} else {
fmt .Fprintf (t .out , "%s" , PadRight (str , SPACE , t .cs [y ]))
}
}
if !t .noWhiteSpace {
fmt .Fprintf (t .out , SPACE )
} else {
fmt .Fprintf (t .out , t .tablePadding )
}
}
if !t .noWhiteSpace {
fmt .Fprint (t .out , ConditionString (t .borders .Left , t .pColumn , SPACE ))
}
fmt .Fprint (t .out , t .newLine )
}
if t .rowLine {
t .printLine (true )
}
}
func (t *Table ) printRowsMergeCells () {
var previousLine []string
var displayCellBorder []bool
var tmpWriter bytes .Buffer
for i , lines := range t .lines {
previousLine , displayCellBorder = t .printRowMergeCells (&tmpWriter , lines , i , previousLine )
if i > 0 {
if t .rowLine {
t .printLineOptionalCellSeparators (true , displayCellBorder )
}
}
tmpWriter .WriteTo (t .out )
}
if t .rowLine {
t .printLine (true )
}
}
func (t *Table ) printRowMergeCells (writer io .Writer , columns [][]string , rowIdx int , previousLine []string ) ([]string , []bool ) {
max := t .rs [rowIdx ]
total := len (columns )
pads := []int {}
is_esc_seq := false
if len (t .columnsParams ) > 0 {
is_esc_seq = true
}
for i , line := range columns {
length := len (line )
pad := max - length
pads = append (pads , pad )
for n := 0 ; n < pad ; n ++ {
columns [i ] = append (columns [i ], " " )
}
}
var displayCellBorder []bool
t .fillAlignment (total )
for x := 0 ; x < max ; x ++ {
for y := 0 ; y < total ; y ++ {
fmt .Fprint (writer , ConditionString ((!t .borders .Left && y == 0 ), SPACE , t .pColumn ))
fmt .Fprintf (writer , SPACE )
str := columns [y ][x ]
if is_esc_seq {
str = format (str , t .columnsParams [y ])
}
if t .autoMergeCells {
var mergeCell bool
if t .columnsToAutoMergeCells != nil {
if t .columnsToAutoMergeCells [y ] {
mergeCell = true
}
} else {
mergeCell = true
}
fullLine := strings .TrimRight (strings .Join (columns [y ], " " ), " " )
if len (previousLine ) > y && fullLine == previousLine [y ] && fullLine != "" && mergeCell {
displayCellBorder = append (displayCellBorder , false )
str = ""
} else {
displayCellBorder = append (displayCellBorder , true )
}
}
switch t .columnsAlign [y ] {
case ALIGN_CENTER :
fmt .Fprintf (writer , "%s" , Pad (str , SPACE , t .cs [y ]))
case ALIGN_RIGHT :
fmt .Fprintf (writer , "%s" , PadLeft (str , SPACE , t .cs [y ]))
case ALIGN_LEFT :
fmt .Fprintf (writer , "%s" , PadRight (str , SPACE , t .cs [y ]))
default :
if decimal .MatchString (strings .TrimSpace (str )) || percent .MatchString (strings .TrimSpace (str )) {
fmt .Fprintf (writer , "%s" , PadLeft (str , SPACE , t .cs [y ]))
} else {
fmt .Fprintf (writer , "%s" , PadRight (str , SPACE , t .cs [y ]))
}
}
fmt .Fprintf (writer , SPACE )
}
fmt .Fprint (writer , ConditionString (t .borders .Left , t .pColumn , SPACE ))
fmt .Fprint (writer , t .newLine )
}
previousLine = make ([]string , total )
for y := 0 ; y < total ; y ++ {
previousLine [y ] = strings .TrimRight (strings .Join (columns [y ], " " ), " " )
}
return previousLine , displayCellBorder
}
func (t *Table ) parseDimension (str string , colKey , rowKey int ) []string {
var (
raw []string
maxWidth int
)
raw = getLines (str )
maxWidth = 0
for _ , line := range raw {
if w := DisplayWidth (line ); w > maxWidth {
maxWidth = w
}
}
if t .autoWrap {
if maxWidth > t .mW {
maxWidth = t .mW
}
newMaxWidth := maxWidth
newRaw := make ([]string , 0 , len (raw ))
if t .reflowText {
raw = []string {strings .Join (raw , " " )}
}
for i , para := range raw {
paraLines , _ := WrapString (para , maxWidth )
for _ , line := range paraLines {
if w := DisplayWidth (line ); w > newMaxWidth {
newMaxWidth = w
}
}
if i > 0 {
newRaw = append (newRaw , " " )
}
newRaw = append (newRaw , paraLines ...)
}
raw = newRaw
maxWidth = newMaxWidth
}
v , ok := t .cs [colKey ]
if !ok || v < maxWidth || v == 0 {
t .cs [colKey ] = maxWidth
}
h := len (raw )
v , ok = t .rs [rowKey ]
if !ok || v < h || v == 0 {
t .rs [rowKey ] = h
}
return raw
}
The pages are generated with Golds v0.8.2 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .