From 241780a7d133882a97bcaf8fa3c97d3eaf35fb59 Mon Sep 17 00:00:00 2001 From: Naveen Mahalingam Date: Fri, 27 Oct 2023 19:29:02 -0700 Subject: [PATCH] table: debug logging --- go.mod | 1 + table/out.go | 61 +++++++ table/render.go | 116 ++++++------- table/render_csv.go | 40 ++--- table/render_debug_test.go | 340 +++++++++++++++++++++++++++++++++++++ table/render_html.go | 126 +++++++------- table/render_init.go | 13 ++ table/render_markdown.go | 62 +++---- table/render_tsv.go | 40 ++--- table/table.go | 14 +- table/table_test.go | 8 + table/writer.go | 1 + 12 files changed, 627 insertions(+), 195 deletions(-) create mode 100644 table/out.go create mode 100644 table/render_debug_test.go diff --git a/go.mod b/go.mod index 1b19eb9..a3d828a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/jedib0t/go-pretty/v6 go 1.16 require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/mattn/go-runewidth v0.0.13 github.com/pkg/profile v1.6.0 github.com/stretchr/testify v1.7.4 diff --git a/table/out.go b/table/out.go new file mode 100644 index 0000000..394dc9c --- /dev/null +++ b/table/out.go @@ -0,0 +1,61 @@ +package table + +import ( + "fmt" + "io" + "strings" +) + +type outputWriter interface { + Len() int + Grow(n int) + Reset() + String() string + WriteRune(r rune) (int, error) + WriteString(s string) (int, error) +} + +func newOutputWriter(debug io.Writer) outputWriter { + return &ow{ + debug: debug, + out: strings.Builder{}, + } +} + +type ow struct { + debug io.Writer + out strings.Builder +} + +func (o *ow) Len() int { + return o.out.Len() +} + +func (o *ow) Grow(n int) { + if o.debug != nil { + _, _ = o.debug.Write([]byte(fmt.Sprintf(">> grow buffer by %d bytes\n", n))) + } + o.out.Grow(n) +} + +func (o *ow) Reset() { + o.out.Reset() +} + +func (o *ow) String() string { + return o.out.String() +} + +func (o *ow) WriteRune(r rune) (int, error) { + if o.debug != nil { + _, _ = o.debug.Write([]byte(fmt.Sprintf("++ [%02d] %#v\n", 1, r))) + } + return o.out.WriteRune(r) +} + +func (o *ow) WriteString(s string) (int, error) { + if o.debug != nil { + _, _ = o.debug.Write([]byte(fmt.Sprintf("++ [%02d] %#v\n", len(s), s))) + } + return o.out.WriteString(s) +} diff --git a/table/render.go b/table/render.go index 2e93da5..3fbe9e4 100644 --- a/table/render.go +++ b/table/render.go @@ -22,35 +22,35 @@ import ( func (t *Table) Render() string { t.initForRender() - var out strings.Builder + out := newOutputWriter(t.debugWriter) if t.numColumns > 0 { - t.renderTitle(&out) + t.renderTitle(out) // top-most border - t.renderRowsBorderTop(&out) + t.renderRowsBorderTop(out) // header rows - t.renderRowsHeader(&out) + t.renderRowsHeader(out) // (data) rows - t.renderRows(&out, t.rows, renderHint{}) + t.renderRows(out, t.rows, renderHint{}) // footer rows - t.renderRowsFooter(&out) + t.renderRowsFooter(out) // bottom-most border - t.renderRowsBorderBottom(&out) + t.renderRowsBorderBottom(out) // caption if t.caption != "" { - out.WriteRune('\n') - out.WriteString(t.caption) + _, _ = out.WriteRune('\n') + _, _ = out.WriteString(t.caption) } } - return t.render(&out) + return t.render(out) } -func (t *Table) renderColumn(out *strings.Builder, row rowStr, colIdx int, maxColumnLength int, hint renderHint) int { +func (t *Table) renderColumn(out outputWriter, row rowStr, colIdx int, maxColumnLength int, hint renderHint) int { numColumnsRendered := 1 // when working on the first column, and autoIndex is true, insert a new @@ -105,7 +105,7 @@ func (t *Table) renderColumn(out *strings.Builder, row rowStr, colIdx int, maxCo return colIdx + numColumnsRendered } -func (t *Table) renderColumnAutoIndex(out *strings.Builder, hint renderHint) { +func (t *Table) renderColumnAutoIndex(out outputWriter, hint renderHint) { var outAutoIndex strings.Builder outAutoIndex.Grow(t.maxColumnLengths[0]) @@ -132,61 +132,61 @@ func (t *Table) renderColumnAutoIndex(out *strings.Builder, hint renderHint) { if hint.isFooterRow { colors = t.style.Color.Footer } - out.WriteString(colors.Sprint(outAutoIndex.String())) + _, _ = out.WriteString(colors.Sprint(outAutoIndex.String())) } else { - out.WriteString(outAutoIndex.String()) + _, _ = 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) { +func (t *Table) renderColumnColorized(out outputWriter, colIdx int, colStr string, hint renderHint) { colors := t.getColumnColors(colIdx, hint) if colors != nil { - out.WriteString(colors.Sprint(colStr)) + _, _ = out.WriteString(colors.Sprint(colStr)) } else if hint.isHeaderRow && t.style.Color.Header != nil { - out.WriteString(t.style.Color.Header.Sprint(colStr)) + _, _ = 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)) + _, _ = 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)) + _, _ = 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)) + _, _ = out.WriteString(t.style.Color.RowAlternate.Sprint(colStr)) } else if t.style.Color.Row != nil { - out.WriteString(t.style.Color.Row.Sprint(colStr)) + _, _ = out.WriteString(t.style.Color.Row.Sprint(colStr)) } else { - out.WriteString(colStr) + _, _ = out.WriteString(colStr) } } else { - out.WriteString(colStr) + _, _ = out.WriteString(colStr) } } -func (t *Table) renderColumnSeparator(out *strings.Builder, row rowStr, colIdx int, hint renderHint) { +func (t *Table) renderColumnSeparator(out outputWriter, 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)) + _, _ = out.WriteString(colors.Sprint(separator)) } else { - out.WriteString(separator) + _, _ = out.WriteString(separator) } } } -func (t *Table) renderLine(out *strings.Builder, row rowStr, hint renderHint) { +func (t *Table) renderLine(out outputWriter, 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') + _, _ = out.WriteRune('\n') } // use a brand-new strings.Builder if a row length limit has been set - var outLine *strings.Builder + var outLine outputWriter if t.allowedRowLength > 0 { - outLine = &strings.Builder{} + outLine = newOutputWriter(t.debugWriter) } else { outLine = out } @@ -216,52 +216,52 @@ func (t *Table) renderLine(out *strings.Builder, row rowStr, hint renderHint) { if t.pageSize > 0 && t.numLinesRendered%t.pageSize == 0 && !hint.isLastLineOfLastRow() { t.renderRowsFooter(out) t.renderRowsBorderBottom(out) - out.WriteString(t.style.Box.PageSeparator) + _, _ = out.WriteString(t.style.Box.PageSeparator) t.renderRowsBorderTop(out) t.renderRowsHeader(out) } } } -func (t *Table) renderLineMergeOutputs(out *strings.Builder, outLine *strings.Builder) { +func (t *Table) renderLineMergeOutputs(out outputWriter, outLine outputWriter) { 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) + _, _ = out.WriteString(text.Trim(outLineStr, trimLength)) + _, _ = out.WriteString(t.style.Box.UnfinishedRow) } } else { - out.WriteString(outLineStr) + _, _ = out.WriteString(outLineStr) } } -func (t *Table) renderMarginLeft(out *strings.Builder, hint renderHint) { - out.WriteString(t.style.Format.Direction.Modifier()) +func (t *Table) renderMarginLeft(out outputWriter, 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)) + _, _ = out.WriteString(colors.Sprint(border)) } else { - out.WriteString(border) + _, _ = out.WriteString(border) } } } -func (t *Table) renderMarginRight(out *strings.Builder, hint renderHint) { +func (t *Table) renderMarginRight(out outputWriter, hint renderHint) { if t.style.Options.DrawBorder { border := t.getBorderRight(hint) colors := t.getBorderColors(hint) if colors.EscapeSeq() != "" { - out.WriteString(colors.Sprint(border)) + _, _ = out.WriteString(colors.Sprint(border)) } else { - out.WriteString(border) + _, _ = out.WriteString(border) } } } -func (t *Table) renderRow(out *strings.Builder, row rowStr, hint renderHint) { +func (t *Table) renderRow(out outputWriter, 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 @@ -292,7 +292,7 @@ func (t *Table) renderRow(out *strings.Builder, row rowStr, hint renderHint) { } } -func (t *Table) renderRowSeparator(out *strings.Builder, hint renderHint) { +func (t *Table) renderRowSeparator(out outputWriter, hint renderHint) { if hint.isBorderTop || hint.isBorderBottom { if !t.style.Options.DrawBorder { return @@ -306,7 +306,7 @@ func (t *Table) renderRowSeparator(out *strings.Builder, hint renderHint) { t.renderLine(out, t.rowSeparator, hint) } -func (t *Table) renderRows(out *strings.Builder, rows []rowStr, hint renderHint) { +func (t *Table) renderRows(out outputWriter, rows []rowStr, hint renderHint) { for rowIdx, row := range rows { hint.isFirstRow = rowIdx == 0 hint.isLastRow = rowIdx == len(rows)-1 @@ -321,7 +321,7 @@ func (t *Table) renderRows(out *strings.Builder, rows []rowStr, hint renderHint) } } -func (t *Table) renderRowsBorderBottom(out *strings.Builder) { +func (t *Table) renderRowsBorderBottom(out outputWriter) { if len(t.rowsFooter) > 0 { t.renderRowSeparator(out, renderHint{isBorderBottom: true, isFooterRow: true, rowNumber: len(t.rowsFooter)}) } else { @@ -329,7 +329,7 @@ func (t *Table) renderRowsBorderBottom(out *strings.Builder) { } } -func (t *Table) renderRowsBorderTop(out *strings.Builder) { +func (t *Table) renderRowsBorderTop(out outputWriter) { if len(t.rowsHeader) > 0 || t.autoIndex { t.renderRowSeparator(out, renderHint{isBorderTop: true, isHeaderRow: true, rowNumber: 0}) } else { @@ -337,7 +337,7 @@ func (t *Table) renderRowsBorderTop(out *strings.Builder) { } } -func (t *Table) renderRowsFooter(out *strings.Builder) { +func (t *Table) renderRowsFooter(out outputWriter) { if len(t.rowsFooter) > 0 { t.renderRowSeparator(out, renderHint{ isFooterRow: true, @@ -348,7 +348,7 @@ func (t *Table) renderRowsFooter(out *strings.Builder) { } } -func (t *Table) renderRowsHeader(out *strings.Builder) { +func (t *Table) renderRowsHeader(out outputWriter) { if len(t.rowsHeader) > 0 || t.autoIndex { hintSeparator := renderHint{isHeaderRow: true, isLastRow: true, isSeparatorRow: true} @@ -363,7 +363,7 @@ func (t *Table) renderRowsHeader(out *strings.Builder) { } } -func (t *Table) renderTitle(out *strings.Builder) { +func (t *Table) renderTitle(out outputWriter) { if t.title != "" { colors := t.style.Title.Colors colorsBorder := t.getBorderColors(renderHint{isTitleRow: true}) @@ -373,9 +373,9 @@ func (t *Table) renderTitle(out *strings.Builder) { } 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)) + _, _ = 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) @@ -389,20 +389,20 @@ func (t *Table) renderTitle(out *strings.Builder) { } } -func (t *Table) renderTitleLine(out *strings.Builder, lenText int, titleLine string, colors text.Colors, colorsBorder text.Colors) { +func (t *Table) renderTitleLine(out outputWriter, 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') + _, _ = out.WriteRune('\n') } if t.style.Options.DrawBorder { - out.WriteString(colorsBorder.Sprint(t.style.Box.Left)) + _, _ = out.WriteString(colorsBorder.Sprint(t.style.Box.Left)) } - out.WriteString(colors.Sprint(titleLine)) + _, _ = out.WriteString(colors.Sprint(titleLine)) if t.style.Options.DrawBorder { - out.WriteString(colorsBorder.Sprint(t.style.Box.Right)) + _, _ = out.WriteString(colorsBorder.Sprint(t.style.Box.Right)) } } diff --git a/table/render_csv.go b/table/render_csv.go index 831194e..db86540 100644 --- a/table/render_csv.go +++ b/table/render_csv.go @@ -16,23 +16,23 @@ import ( func (t *Table) RenderCSV() string { t.initForRender() - var out strings.Builder + out := newOutputWriter(t.debugWriter) if t.numColumns > 0 { if t.title != "" { - out.WriteString(t.title) + _, _ = out.WriteString(t.title) } if t.autoIndex && len(t.rowsHeader) == 0 { - t.csvRenderRow(&out, t.getAutoIndexColumnIDs(), renderHint{isAutoIndexRow: true, isHeaderRow: true}) + 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}) + 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) + _, _ = out.WriteRune('\n') + _, _ = out.WriteString(t.caption) } } - return t.render(&out) + return t.render(out) } func (t *Table) csvFixCommas(str string) string { @@ -43,10 +43,10 @@ func (t *Table) csvFixDoubleQuotes(str string) string { return strings.Replace(str, "\"", "\\\"", -1) } -func (t *Table) csvRenderRow(out *strings.Builder, row rowStr, hint renderHint) { +func (t *Table) csvRenderRow(out outputWriter, row rowStr, hint renderHint) { // when working on line number 2 or more, insert a newline first if out.Len() > 0 { - out.WriteRune('\n') + _, _ = out.WriteRune('\n') } // generate the columns to render in CSV format and append to "out" @@ -54,27 +54,27 @@ func (t *Table) csvRenderRow(out *strings.Builder, row rowStr, hint renderHint) // auto-index column if colIdx == 0 && t.autoIndex { if hint.isRegularRow() { - out.WriteString(fmt.Sprint(hint.rowNumber)) + _, _ = out.WriteString(fmt.Sprint(hint.rowNumber)) } - out.WriteRune(',') + _, _ = out.WriteRune(',') } if colIdx > 0 { - out.WriteRune(',') + _, _ = out.WriteRune(',') } if strings.ContainsAny(colStr, "\",\n") { - out.WriteRune('"') - out.WriteString(t.csvFixCommas(t.csvFixDoubleQuotes(colStr))) - out.WriteRune('"') + _, _ = out.WriteRune('"') + _, _ = out.WriteString(t.csvFixCommas(t.csvFixDoubleQuotes(colStr))) + _, _ = out.WriteRune('"') } else if utf8.RuneCountInString(colStr) > 0 { - out.WriteString(colStr) + _, _ = out.WriteString(colStr) } } for colIdx := len(row); colIdx < t.numColumns; colIdx++ { - out.WriteRune(',') + _, _ = out.WriteRune(',') } } -func (t *Table) csvRenderRows(out *strings.Builder, rows []rowStr, hint renderHint) { +func (t *Table) csvRenderRows(out outputWriter, rows []rowStr, hint renderHint) { for rowIdx, row := range rows { hint.rowNumber = rowIdx + 1 t.csvRenderRow(out, row, hint) diff --git a/table/render_debug_test.go b/table/render_debug_test.go new file mode 100644 index 0000000..eafa0a1 --- /dev/null +++ b/table/render_debug_test.go @@ -0,0 +1,340 @@ +package table + +import ( + "strings" + "testing" +) + +func TestTable_Render_Debug(t *testing.T) { + debugData := strings.Builder{} + tw := Table{} + tw.AppendHeader(testHeader) + tw.AppendRows(testRows) + tw.AppendFooter(testFooter) + tw.SetDebugWriter(&debugData) + + compareOutput(t, tw.Render(), ` ++-----+------------+-----------+--------+-----------------------------+ +| # | FIRST NAME | LAST NAME | SALARY | | ++-----+------------+-----------+--------+-----------------------------+ +| 1 | Arya | Stark | 3000 | | +| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | +| 300 | Tyrion | Lannister | 5000 | | ++-----+------------+-----------+--------+-----------------------------+ +| | | TOTAL | 10000 | | ++-----+------------+-----------+--------+-----------------------------+`) + + compareOutput(t, debugData.String(), `(table.Table) { + allowedRowLength: (int) 0, + autoIndex: (bool) false, + autoIndexVIndexMaxLength: (int) 1, + caption: (string) "", + columnIsNonNumeric: ([]bool) (len=5) { + (bool) false, + (bool) true, + (bool) true, + (bool) false, + (bool) true + }, + columnConfigs: ([]table.ColumnConfig) , + columnConfigMap: (map[int]table.ColumnConfig) { + }, + debugWriter: (*strings.Builder)(), + htmlCSSClass: (string) "", + indexColumn: (int) 0, + maxColumnLengths: ([]int) (len=5) { + (int) 3, + (int) 10, + (int) 9, + (int) 6, + (int) 27 + }, + maxRowLength: (int) 71, + numColumns: (int) 5, + numLinesRendered: (int) 0, + outputMirror: (io.Writer) , + pageSize: (int) 0, + rows: ([]table.rowStr) (len=3) { + (table.rowStr) (len=4) { + (string) (len=1) "1", + (string) (len=4) "Arya", + (string) (len=5) "Stark", + (string) (len=4) "3000" + }, + (table.rowStr) (len=5) { + (string) (len=2) "20", + (string) (len=3) "Jon", + (string) (len=4) "Snow", + (string) (len=4) "2000", + (string) (len=27) "You know nothing, Jon Snow!" + }, + (table.rowStr) (len=4) { + (string) (len=3) "300", + (string) (len=6) "Tyrion", + (string) (len=9) "Lannister", + (string) (len=4) "5000" + } + }, + rowsColors: ([]text.Colors) , + rowsConfigMap: (map[int]table.RowConfig) , + rowsRaw: ([]table.Row) (len=3) { + (table.Row) (len=4) { + (int) 1, + (string) (len=4) "Arya", + (string) (len=5) "Stark", + (int) 3000 + }, + (table.Row) (len=5) { + (int) 20, + (string) (len=3) "Jon", + (string) (len=4) "Snow", + (int) 2000, + (string) (len=27) "You know nothing, Jon Snow!" + }, + (table.Row) (len=4) { + (int) 300, + (string) (len=6) "Tyrion", + (string) (len=9) "Lannister", + (int) 5000 + } + }, + rowsFooter: ([]table.rowStr) (len=1) { + (table.rowStr) (len=4) { + (string) "", + (string) "", + (string) (len=5) "Total", + (string) (len=5) "10000" + } + }, + rowsFooterConfigMap: (map[int]table.RowConfig) , + rowsFooterRaw: ([]table.Row) (len=1) { + (table.Row) (len=4) { + (string) "", + (string) "", + (string) (len=5) "Total", + (int) 10000 + } + }, + rowsHeader: ([]table.rowStr) (len=1) { + (table.rowStr) (len=4) { + (string) (len=1) "#", + (string) (len=10) "First Name", + (string) (len=9) "Last Name", + (string) (len=6) "Salary" + } + }, + rowsHeaderConfigMap: (map[int]table.RowConfig) , + rowsHeaderRaw: ([]table.Row) (len=1) { + (table.Row) (len=4) { + (string) (len=1) "#", + (string) (len=10) "First Name", + (string) (len=9) "Last Name", + (string) (len=6) "Salary" + } + }, + rowPainter: (table.RowPainter) , + rowSeparator: (table.rowStr) (len=5) { + (string) (len=5) "-----", + (string) (len=12) "------------", + (string) (len=11) "-----------", + (string) (len=8) "--------", + (string) (len=29) "-----------------------------" + }, + separators: (map[int]bool) , + sortBy: ([]table.SortBy) , + style: (*table.Style)({ + Name: (string) (len=12) "StyleDefault", + Box: (table.BoxStyle) { + BottomLeft: (string) (len=1) "+", + BottomRight: (string) (len=1) "+", + BottomSeparator: (string) (len=1) "+", + EmptySeparator: (string) (len=1) " ", + Left: (string) (len=1) "|", + LeftSeparator: (string) (len=1) "+", + MiddleHorizontal: (string) (len=1) "-", + MiddleSeparator: (string) (len=1) "+", + MiddleVertical: (string) (len=1) "|", + PaddingLeft: (string) (len=1) " ", + PaddingRight: (string) (len=1) " ", + PageSeparator: (string) (len=1) "\n", + Right: (string) (len=1) "|", + RightSeparator: (string) (len=1) "+", + TopLeft: (string) (len=1) "+", + TopRight: (string) (len=1) "+", + TopSeparator: (string) (len=1) "+", + UnfinishedRow: (string) (len=2) " ~" + }, + Color: (table.ColorOptions) { + Border: (text.Colors) , + Footer: (text.Colors) , + Header: (text.Colors) , + IndexColumn: (text.Colors) , + Row: (text.Colors) , + RowAlternate: (text.Colors) , + Separator: (text.Colors) + }, + Format: (table.FormatOptions) { + Direction: (text.Direction) 0, + Footer: (text.Format) 3, + Header: (text.Format) 3, + Row: (text.Format) 0 + }, + HTML: (table.HTMLOptions) { + CSSClass: (string) (len=15) "go-pretty-table", + EmptyColumn: (string) (len=6) " ", + EscapeText: (bool) true, + Newline: (string) (len=5) "
" + }, + Options: (table.Options) { + DoNotColorBordersAndSeparators: (bool) false, + DrawBorder: (bool) true, + SeparateColumns: (bool) true, + SeparateFooter: (bool) true, + SeparateHeader: (bool) true, + SeparateRows: (bool) false + }, + Title: (table.TitleOptions) { + Align: (text.Align) 0, + Colors: (text.Colors) , + Format: (text.Format) 0 + } + }), + suppressEmptyColumns: (bool) false, + title: (string) "" +} +>> grow buffer by 71 bytes +++ [00] "" +++ [01] "+" +++ [05] "-----" +++ [01] "+" +++ [12] "------------" +++ [01] "+" +++ [11] "-----------" +++ [01] "+" +++ [08] "--------" +++ [01] "+" +++ [29] "-----------------------------" +++ [01] "+" +++ [01] 10 +>> grow buffer by 71 bytes +++ [00] "" +++ [01] "|" +++ [05] " # " +++ [01] "|" +++ [12] " FIRST NAME " +++ [01] "|" +++ [11] " LAST NAME " +++ [01] "|" +++ [08] " SALARY " +++ [01] "|" +++ [29] " " +++ [01] "|" +++ [01] 10 +>> grow buffer by 71 bytes +++ [00] "" +++ [01] "+" +++ [05] "-----" +++ [01] "+" +++ [12] "------------" +++ [01] "+" +++ [11] "-----------" +++ [01] "+" +++ [08] "--------" +++ [01] "+" +++ [29] "-----------------------------" +++ [01] "+" +++ [01] 10 +>> grow buffer by 71 bytes +++ [00] "" +++ [01] "|" +++ [05] " 1 " +++ [01] "|" +++ [12] " Arya " +++ [01] "|" +++ [11] " Stark " +++ [01] "|" +++ [08] " 3000 " +++ [01] "|" +++ [29] " " +++ [01] "|" +++ [01] 10 +>> grow buffer by 71 bytes +++ [00] "" +++ [01] "|" +++ [05] " 20 " +++ [01] "|" +++ [12] " Jon " +++ [01] "|" +++ [11] " Snow " +++ [01] "|" +++ [08] " 2000 " +++ [01] "|" +++ [29] " You know nothing, Jon Snow! " +++ [01] "|" +++ [01] 10 +>> grow buffer by 71 bytes +++ [00] "" +++ [01] "|" +++ [05] " 300 " +++ [01] "|" +++ [12] " Tyrion " +++ [01] "|" +++ [11] " Lannister " +++ [01] "|" +++ [08] " 5000 " +++ [01] "|" +++ [29] " " +++ [01] "|" +++ [01] 10 +>> grow buffer by 71 bytes +++ [00] "" +++ [01] "+" +++ [05] "-----" +++ [01] "+" +++ [12] "------------" +++ [01] "+" +++ [11] "-----------" +++ [01] "+" +++ [08] "--------" +++ [01] "+" +++ [29] "-----------------------------" +++ [01] "+" +++ [01] 10 +>> grow buffer by 71 bytes +++ [00] "" +++ [01] "|" +++ [05] " " +++ [01] "|" +++ [12] " " +++ [01] "|" +++ [11] " TOTAL " +++ [01] "|" +++ [08] " 10000 " +++ [01] "|" +++ [29] " " +++ [01] "|" +++ [01] 10 +>> grow buffer by 71 bytes +++ [00] "" +++ [01] "+" +++ [05] "-----" +++ [01] "+" +++ [12] "------------" +++ [01] "+" +++ [11] "-----------" +++ [01] "+" +++ [08] "--------" +++ [01] "+" +++ [29] "-----------------------------" +++ [01] "+" +[00] "+-----+------------+-----------+--------+-----------------------------+" +[01] "| # | FIRST NAME | LAST NAME | SALARY | |" +[02] "+-----+------------+-----------+--------+-----------------------------+" +[03] "| 1 | Arya | Stark | 3000 | |" +[04] "| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |" +[05] "| 300 | Tyrion | Lannister | 5000 | |" +[06] "+-----+------------+-----------+--------+-----------------------------+" +[07] "| | | TOTAL | 10000 | |" +[08] "+-----+------------+-----------+--------+-----------------------------+" +`) +} diff --git a/table/render_html.go b/table/render_html.go index fcea86b..b46dea4 100644 --- a/table/render_html.go +++ b/table/render_html.go @@ -60,23 +60,23 @@ const ( func (t *Table) RenderHTML() string { t.initForRender() - var out strings.Builder + out := newOutputWriter(t.debugWriter) if t.numColumns > 0 { - out.WriteString("\n") - t.htmlRenderTitle(&out) - t.htmlRenderRowsHeader(&out) - t.htmlRenderRows(&out, t.rows, renderHint{}) - t.htmlRenderRowsFooter(&out) - t.htmlRenderCaption(&out) - out.WriteString("
") + _, _ = out.WriteString("\">\n") + t.htmlRenderTitle(out) + t.htmlRenderRowsHeader(out) + t.htmlRenderRows(out, t.rows, renderHint{}) + t.htmlRenderRowsFooter(out) + t.htmlRenderCaption(out) + _, _ = out.WriteString("") } - return t.render(&out) + return t.render(out) } func (t *Table) htmlGetColStrAndTag(row rowStr, colIdx int, hint renderHint) (string, string) { @@ -95,25 +95,25 @@ func (t *Table) htmlGetColStrAndTag(row rowStr, colIdx int, hint renderHint) (st return colStr, colTagName } -func (t *Table) htmlRenderCaption(out *strings.Builder) { +func (t *Table) htmlRenderCaption(out outputWriter) { if t.caption != "" { - out.WriteString(" ") - out.WriteString(t.caption) - out.WriteString("\n") + _, _ = out.WriteString(" ") + _, _ = out.WriteString(t.caption) + _, _ = out.WriteString("\n") } } -func (t *Table) htmlRenderColumn(out *strings.Builder, colStr string) { +func (t *Table) htmlRenderColumn(out outputWriter, 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) + _, _ = out.WriteString(colStr) } -func (t *Table) htmlRenderColumnAttributes(out *strings.Builder, colIdx int, hint renderHint) { +func (t *Table) htmlRenderColumnAttributes(out outputWriter, colIdx int, hint renderHint) { // determine the HTML "align"/"valign" property values align := t.getAlign(colIdx, hint).HTMLProperty() vAlign := t.getVAlign(colIdx, hint).HTMLProperty() @@ -121,37 +121,37 @@ func (t *Table) htmlRenderColumnAttributes(out *strings.Builder, colIdx int, hin class := t.getColumnColors(colIdx, hint).HTMLProperty() if align != "" { - out.WriteRune(' ') - out.WriteString(align) + _, _ = out.WriteRune(' ') + _, _ = out.WriteString(align) } if class != "" { - out.WriteRune(' ') - out.WriteString(class) + _, _ = out.WriteRune(' ') + _, _ = out.WriteString(class) } if vAlign != "" { - out.WriteRune(' ') - out.WriteString(vAlign) + _, _ = out.WriteRune(' ') + _, _ = out.WriteString(vAlign) } } -func (t *Table) htmlRenderColumnAutoIndex(out *strings.Builder, hint renderHint) { +func (t *Table) htmlRenderColumnAutoIndex(out outputWriter, hint renderHint) { if hint.isHeaderRow { - out.WriteString(" ") - out.WriteString(t.style.HTML.EmptyColumn) - out.WriteString("\n") + _, _ = out.WriteString(" ") + _, _ = out.WriteString(t.style.HTML.EmptyColumn) + _, _ = out.WriteString("\n") } else if hint.isFooterRow { - out.WriteString(" ") - out.WriteString(t.style.HTML.EmptyColumn) - out.WriteString("\n") + _, _ = out.WriteString(" ") + _, _ = out.WriteString(t.style.HTML.EmptyColumn) + _, _ = out.WriteString("\n") } else { - out.WriteString(" ") - out.WriteString(fmt.Sprint(hint.rowNumber)) - out.WriteString("\n") + _, _ = out.WriteString(" ") + _, _ = out.WriteString(fmt.Sprint(hint.rowNumber)) + _, _ = out.WriteString("\n") } } -func (t *Table) htmlRenderRow(out *strings.Builder, row rowStr, hint renderHint) { - out.WriteString(" \n") +func (t *Table) htmlRenderRow(out outputWriter, row rowStr, hint renderHint) { + _, _ = out.WriteString(" \n") for colIdx := 0; colIdx < t.numColumns; colIdx++ { // auto-index column if colIdx == 0 && t.autoIndex { @@ -160,23 +160,23 @@ func (t *Table) htmlRenderRow(out *strings.Builder, row rowStr, hint renderHint) colStr, colTagName := t.htmlGetColStrAndTag(row, colIdx, hint) // write the row - out.WriteString(" <") - out.WriteString(colTagName) + _, _ = out.WriteString(" <") + _, _ = out.WriteString(colTagName) t.htmlRenderColumnAttributes(out, colIdx, hint) - out.WriteString(">") + _, _ = out.WriteString(">") if len(colStr) == 0 { - out.WriteString(t.style.HTML.EmptyColumn) + _, _ = out.WriteString(t.style.HTML.EmptyColumn) } else { t.htmlRenderColumn(out, colStr) } - out.WriteString("\n") + _, _ = out.WriteString("\n") } - out.WriteString(" \n") + _, _ = out.WriteString(" \n") } -func (t *Table) htmlRenderRows(out *strings.Builder, rows []rowStr, hint renderHint) { +func (t *Table) htmlRenderRows(out outputWriter, rows []rowStr, hint renderHint) { if len(rows) > 0 { // determine that tag to use based on the type of the row rowsTag := "tbody" @@ -191,9 +191,9 @@ func (t *Table) htmlRenderRows(out *strings.Builder, rows []rowStr, hint renderH hint.rowNumber = idx + 1 if len(row) > 0 { if !renderedTagOpen { - out.WriteString(" <") - out.WriteString(rowsTag) - out.WriteString(">\n") + _, _ = out.WriteString(" <") + _, _ = out.WriteString(rowsTag) + _, _ = out.WriteString(">\n") renderedTagOpen = true } t.htmlRenderRow(out, row, hint) @@ -201,20 +201,20 @@ func (t *Table) htmlRenderRows(out *strings.Builder, rows []rowStr, hint renderH } } if shouldRenderTagClose { - out.WriteString(" \n") + _, _ = out.WriteString(" \n") } } } -func (t *Table) htmlRenderRowsFooter(out *strings.Builder) { +func (t *Table) htmlRenderRowsFooter(out outputWriter) { if len(t.rowsFooter) > 0 { t.htmlRenderRows(out, t.rowsFooter, renderHint{isFooterRow: true}) } } -func (t *Table) htmlRenderRowsHeader(out *strings.Builder) { +func (t *Table) htmlRenderRowsHeader(out outputWriter) { if len(t.rowsHeader) > 0 { t.htmlRenderRows(out, t.rowsHeader, renderHint{isHeaderRow: true}) } else if t.autoIndex { @@ -223,23 +223,23 @@ func (t *Table) htmlRenderRowsHeader(out *strings.Builder) { } } -func (t *Table) htmlRenderTitle(out *strings.Builder) { +func (t *Table) htmlRenderTitle(out outputWriter) { 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(" ') - out.WriteString(title) - out.WriteString("\n") + _, _ = out.WriteRune('>') + _, _ = out.WriteString(title) + _, _ = out.WriteString("\n") } } diff --git a/table/render_init.go b/table/render_init.go index 917879b..6078e23 100644 --- a/table/render_init.go +++ b/table/render_init.go @@ -4,9 +4,18 @@ import ( "fmt" "strings" + "github.com/davecgh/go-spew/spew" "github.com/jedib0t/go-pretty/v6/text" ) +func init() { + spew.Config.DisableCapacities = true + spew.Config.DisablePointerAddresses = true + spew.Config.DisablePointerMethods = true + spew.Config.Indent = " " + spew.Config.SortKeys = true +} + 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 { @@ -110,6 +119,10 @@ func (t *Table) initForRender() { // reset the counter for the number of lines rendered t.numLinesRendered = 0 + + if t.debugWriter != nil { + _, _ = t.debugWriter.Write([]byte(spew.Sdump(*t))) + } } func (t *Table) initForRenderColumnConfigs() { diff --git a/table/render_markdown.go b/table/render_markdown.go index adf573f..c2bb814 100644 --- a/table/render_markdown.go +++ b/table/render_markdown.go @@ -16,67 +16,67 @@ import ( func (t *Table) RenderMarkdown() string { t.initForRender() - var out strings.Builder + out := newOutputWriter(t.debugWriter) if t.numColumns > 0 { - t.markdownRenderTitle(&out) - t.markdownRenderRowsHeader(&out) - t.markdownRenderRows(&out, t.rows, renderHint{}) - t.markdownRenderRowsFooter(&out) - t.markdownRenderCaption(&out) + t.markdownRenderTitle(out) + t.markdownRenderRowsHeader(out) + t.markdownRenderRows(out, t.rows, renderHint{}) + t.markdownRenderRowsFooter(out) + t.markdownRenderCaption(out) } - return t.render(&out) + return t.render(out) } -func (t *Table) markdownRenderCaption(out *strings.Builder) { +func (t *Table) markdownRenderCaption(out outputWriter) { if t.caption != "" { - out.WriteRune('\n') - out.WriteRune('_') - out.WriteString(t.caption) - out.WriteRune('_') + _, _ = out.WriteRune('\n') + _, _ = out.WriteRune('_') + _, _ = out.WriteString(t.caption) + _, _ = out.WriteRune('_') } } -func (t *Table) markdownRenderRow(out *strings.Builder, row rowStr, hint renderHint) { +func (t *Table) markdownRenderRow(out outputWriter, row rowStr, hint renderHint) { // when working on line number 2 or more, insert a newline first if out.Len() > 0 { - out.WriteRune('\n') + _, _ = out.WriteRune('\n') } // render each column up to the max. columns seen in all the rows - out.WriteRune('|') + _, _ = out.WriteRune('|') for colIdx := 0; colIdx < t.numColumns; colIdx++ { t.markdownRenderRowAutoIndex(out, colIdx, hint) if hint.isSeparatorRow { - out.WriteString(t.getAlign(colIdx, hint).MarkdownProperty()) + _, _ = out.WriteString(t.getAlign(colIdx, hint).MarkdownProperty()) } else { var colStr string if colIdx < len(row) { colStr = row[colIdx] } - out.WriteRune(' ') + _, _ = out.WriteRune(' ') colStr = strings.ReplaceAll(colStr, "|", "\\|") colStr = strings.ReplaceAll(colStr, "\n", "
") - out.WriteString(colStr) - out.WriteRune(' ') + _, _ = out.WriteString(colStr) + _, _ = out.WriteRune(' ') } - out.WriteRune('|') + _, _ = out.WriteRune('|') } } -func (t *Table) markdownRenderRowAutoIndex(out *strings.Builder, colIdx int, hint renderHint) { +func (t *Table) markdownRenderRowAutoIndex(out outputWriter, colIdx int, hint renderHint) { if colIdx == 0 && t.autoIndex { - out.WriteRune(' ') + _, _ = out.WriteRune(' ') if hint.isSeparatorRow { - out.WriteString("---:") + _, _ = out.WriteString("---:") } else if hint.isRegularRow() { - out.WriteString(fmt.Sprintf("%d ", hint.rowNumber)) + _, _ = out.WriteString(fmt.Sprintf("%d ", hint.rowNumber)) } - out.WriteRune('|') + _, _ = out.WriteRune('|') } } -func (t *Table) markdownRenderRows(out *strings.Builder, rows []rowStr, hint renderHint) { +func (t *Table) markdownRenderRows(out outputWriter, rows []rowStr, hint renderHint) { if len(rows) > 0 { for idx, row := range rows { hint.rowNumber = idx + 1 @@ -89,11 +89,11 @@ func (t *Table) markdownRenderRows(out *strings.Builder, rows []rowStr, hint ren } } -func (t *Table) markdownRenderRowsFooter(out *strings.Builder) { +func (t *Table) markdownRenderRowsFooter(out outputWriter) { t.markdownRenderRows(out, t.rowsFooter, renderHint{isFooterRow: true}) } -func (t *Table) markdownRenderRowsHeader(out *strings.Builder) { +func (t *Table) markdownRenderRowsHeader(out outputWriter) { if len(t.rowsHeader) > 0 { t.markdownRenderRows(out, t.rowsHeader, renderHint{isHeaderRow: true}) } else if t.autoIndex { @@ -101,9 +101,9 @@ func (t *Table) markdownRenderRowsHeader(out *strings.Builder) { } } -func (t *Table) markdownRenderTitle(out *strings.Builder) { +func (t *Table) markdownRenderTitle(out outputWriter) { if t.title != "" { - out.WriteString("# ") - out.WriteString(t.title) + _, _ = out.WriteString("# ") + _, _ = out.WriteString(t.title) } } diff --git a/table/render_tsv.go b/table/render_tsv.go index bb73975..4f6fecd 100644 --- a/table/render_tsv.go +++ b/table/render_tsv.go @@ -8,56 +8,52 @@ import ( func (t *Table) RenderTSV() string { t.initForRender() - var out strings.Builder - + out := newOutputWriter(t.debugWriter) if t.numColumns > 0 { if t.title != "" { - out.WriteString(t.title) + _, _ = out.WriteString(t.title) } - if t.autoIndex && len(t.rowsHeader) == 0 { - t.tsvRenderRow(&out, t.getAutoIndexColumnIDs(), renderHint{isAutoIndexRow: true, isHeaderRow: true}) + t.tsvRenderRow(out, t.getAutoIndexColumnIDs(), renderHint{isAutoIndexRow: true, isHeaderRow: true}) } - - t.tsvRenderRows(&out, t.rowsHeader, renderHint{isHeaderRow: true}) - t.tsvRenderRows(&out, t.rows, renderHint{}) - t.tsvRenderRows(&out, t.rowsFooter, renderHint{isFooterRow: true}) - + t.tsvRenderRows(out, t.rowsHeader, renderHint{isHeaderRow: true}) + t.tsvRenderRows(out, t.rows, renderHint{}) + t.tsvRenderRows(out, t.rowsFooter, renderHint{isFooterRow: true}) if t.caption != "" { - out.WriteRune('\n') - out.WriteString(t.caption) + _, _ = out.WriteRune('\n') + _, _ = out.WriteString(t.caption) } } - return t.render(&out) + return t.render(out) } -func (t *Table) tsvRenderRow(out *strings.Builder, row rowStr, hint renderHint) { +func (t *Table) tsvRenderRow(out outputWriter, row rowStr, hint renderHint) { if out.Len() > 0 { - out.WriteRune('\n') + _, _ = out.WriteRune('\n') } for idx, col := range row { if idx == 0 && t.autoIndex { if hint.isRegularRow() { - out.WriteString(fmt.Sprint(hint.rowNumber)) + _, _ = out.WriteString(fmt.Sprint(hint.rowNumber)) } - out.WriteRune('\t') + _, _ = out.WriteRune('\t') } if idx > 0 { - out.WriteRune('\t') + _, _ = out.WriteRune('\t') } if strings.ContainsAny(col, "\t\n\"") || strings.Contains(col, " ") { - out.WriteString(fmt.Sprintf("\"%s\"", t.tsvFixDoubleQuotes(col))) + _, _ = out.WriteString(fmt.Sprintf("\"%s\"", t.tsvFixDoubleQuotes(col))) } else { - out.WriteString(col) + _, _ = out.WriteString(col) } } for colIdx := len(row); colIdx < t.numColumns; colIdx++ { - out.WriteRune('\t') + _, _ = out.WriteRune('\t') } } @@ -65,7 +61,7 @@ func (t *Table) tsvFixDoubleQuotes(str string) string { return strings.Replace(str, "\"", "\"\"", -1) } -func (t *Table) tsvRenderRows(out *strings.Builder, rows []rowStr, hint renderHint) { +func (t *Table) tsvRenderRows(out outputWriter, rows []rowStr, hint renderHint) { for idx, row := range rows { hint.rowNumber = idx + 1 t.tsvRenderRow(out, row, hint) diff --git a/table/table.go b/table/table.go index 6b7c906..4d7c9fc 100644 --- a/table/table.go +++ b/table/table.go @@ -50,6 +50,8 @@ type Table struct { // columnConfigMap stores the custom-configuration by column // number and is generated before rendering columnConfigMap map[int]ColumnConfig + // debugWriter is used to write debugging logs to + debugWriter io.Writer // htmlCSSClass stores the HTML CSS Class to use on the node htmlCSSClass string // indexColumn stores the number of the column considered as the "index" @@ -229,6 +231,11 @@ func (t *Table) SetColumnConfigs(configs []ColumnConfig) { t.columnConfigs = configs } +// SetDebugWriter sets the writer to send debugging logs to. +func (t *Table) SetDebugWriter(w io.Writer) { + t.debugWriter = w +} + // SetHTMLCSSClass sets the HTML CSS Class to use on the
node // when rendering the Table in HTML format. // @@ -677,12 +684,17 @@ func (t *Table) isIndexColumn(colIdx int, hint renderHint) bool { return t.indexColumn == colIdx+1 || hint.isAutoIndexColumn } -func (t *Table) render(out *strings.Builder) string { +func (t *Table) render(out outputWriter) string { outStr := out.String() if t.outputMirror != nil && len(outStr) > 0 { _, _ = t.outputMirror.Write([]byte(outStr)) _, _ = t.outputMirror.Write([]byte("\n")) } + if t.debugWriter != nil { + for idx, line := range strings.Split(outStr, "\n") { + _, _ = t.debugWriter.Write([]byte(fmt.Sprintf("[%02d] %#v\n", idx, line))) + } + } return outStr } diff --git a/table/table_test.go b/table/table_test.go index d4f9955..7d992ea 100644 --- a/table/table_test.go +++ b/table/table_test.go @@ -309,6 +309,14 @@ func TestTable_SetColumnConfigs(t *testing.T) { assert.Equal(t, 3, len(table.columnConfigs)) } +func TestTable_SetDebugWriter(t *testing.T) { + table := Table{} + assert.Nil(t, table.debugWriter) + + table.SetDebugWriter(&strings.Builder{}) + assert.NotNil(t, table.debugWriter) +} + func TestTable_SetHTMLCSSClass(t *testing.T) { table := Table{} table.AppendRow(testRows[0]) diff --git a/table/writer.go b/table/writer.go index c08195b..1392820 100644 --- a/table/writer.go +++ b/table/writer.go @@ -24,6 +24,7 @@ type Writer interface { SetAutoIndex(autoIndex bool) SetCaption(format string, a ...interface{}) SetColumnConfigs(configs []ColumnConfig) + SetDebugWriter(w io.Writer) SetIndexColumn(colNum int) SetOutputMirror(mirror io.Writer) SetPageSize(numLines int)