Skip to content

Commit 39375b4

Browse files
authored
table: horizontal separator customization by context (#386)
1 parent 3b69e0f commit 39375b4

File tree

14 files changed

+1484
-776
lines changed

14 files changed

+1484
-776
lines changed

table/EXAMPLES.md

Lines changed: 579 additions & 0 deletions
Large diffs are not rendered by default.

table/README.md

Lines changed: 2 additions & 518 deletions
Large diffs are not rendered by default.

table/render.go

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
// │ │ │ TOTAL │ 10000 │ │
2121
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
2222
func (t *Table) Render() string {
23-
t.initForRender()
23+
t.initForRender(renderModeDefault)
2424

2525
var out strings.Builder
2626
if t.numColumns > 0 {
@@ -50,6 +50,7 @@ func (t *Table) Render() string {
5050
return t.render(&out)
5151
}
5252

53+
//gocyclo:ignore
5354
func (t *Table) renderColumn(out *strings.Builder, row rowStr, colIdx int, maxColumnLength int, hint renderHint) int {
5455
numColumnsRendered := 1
5556

@@ -93,11 +94,10 @@ func (t *Table) renderColumn(out *strings.Builder, row rowStr, colIdx int, maxCo
9394
numColumnsRendered++
9495
}
9596
}
96-
colStr = align.Apply(colStr, maxColumnLength)
9797

9898
// pad both sides of the column
9999
if !hint.isSeparatorRow || (hint.isSeparatorRow && mergeVertically) {
100-
colStr = t.style.Box.PaddingLeft + colStr + t.style.Box.PaddingRight
100+
colStr = t.style.Box.PaddingLeft + align.Apply(colStr, maxColumnLength) + t.style.Box.PaddingRight
101101
}
102102

103103
t.renderColumnColorized(out, colIdx, colStr, hint)
@@ -112,9 +112,9 @@ func (t *Table) renderColumnAutoIndex(out *strings.Builder, hint renderHint) {
112112
if hint.isSeparatorRow {
113113
numChars := t.autoIndexVIndexMaxLength + utf8.RuneCountInString(t.style.Box.PaddingLeft) +
114114
utf8.RuneCountInString(t.style.Box.PaddingRight)
115-
chars := t.style.Box.MiddleHorizontal
115+
chars := t.style.Box.middleHorizontal(hint.separatorType)
116116
if hint.isAutoIndexColumn && hint.isHeaderOrFooterSeparator() {
117-
chars = text.RepeatAndTrim(" ", len(t.style.Box.MiddleHorizontal))
117+
chars = text.RepeatAndTrim(" ", len(chars))
118118
}
119119
outAutoIndex.WriteString(text.RepeatAndTrim(chars, numChars))
120120
} else {
@@ -304,8 +304,10 @@ func (t *Table) renderRowSeparator(out *strings.Builder, hint renderHint) {
304304
} else if hint.isFooterRow && !t.style.Options.SeparateFooter {
305305
return
306306
}
307+
307308
hint.isSeparatorRow = true
308-
t.renderLine(out, t.rowSeparator, hint)
309+
separator := t.rowSeparatorStrings[hint.separatorType]
310+
t.renderLine(out, t.rowSeparators[separator], hint)
309311
}
310312

311313
func (t *Table) renderRows(out *strings.Builder, rows []rowStr, hint renderHint) {
@@ -316,8 +318,17 @@ func (t *Table) renderRows(out *strings.Builder, rows []rowStr, hint renderHint)
316318
t.renderRow(out, row, hint)
317319

318320
if t.shouldSeparateRows(rowIdx, len(rows)) {
319-
hint.isFirstRow = false
320-
t.renderRowSeparator(out, hint)
321+
hintSep := hint
322+
hintSep.isFirstRow = false
323+
hintSep.isSeparatorRow = true
324+
if hintSep.isHeaderRow {
325+
hintSep.separatorType = separatorTypeHeaderMiddle
326+
} else if hintSep.isFooterRow {
327+
hintSep.separatorType = separatorTypeFooterMiddle
328+
} else {
329+
hintSep.separatorType = separatorTypeRowMiddle
330+
}
331+
t.renderRowSeparator(out, hintSep)
321332
}
322333
}
323334
}
@@ -328,28 +339,41 @@ func (t *Table) renderRowsBorderBottom(out *strings.Builder) {
328339
isBorderBottom: true,
329340
isFooterRow: true,
330341
rowNumber: len(t.rowsFooter),
342+
separatorType: separatorTypeFooterBottom,
331343
})
332344
} else {
333345
t.renderRowSeparator(out, renderHint{
334346
isBorderBottom: true,
335347
isFooterRow: false,
336348
rowNumber: len(t.rows),
349+
separatorType: separatorTypeRowBottom,
337350
})
338351
}
339352
}
340353

341354
func (t *Table) renderRowsBorderTop(out *strings.Builder) {
355+
st := separatorTypeHeaderTop
356+
if t.title != "" {
357+
st = separatorTypeTitleBottom
358+
} else if len(t.rowsHeader) == 0 && !t.autoIndex {
359+
st = separatorTypeRowTop
360+
}
361+
342362
if len(t.rowsHeader) > 0 || t.autoIndex {
343363
t.renderRowSeparator(out, renderHint{
344-
isBorderTop: true,
345-
isHeaderRow: true,
346-
rowNumber: 0,
364+
isBorderTop: true,
365+
isHeaderRow: true,
366+
isSeparatorRow: true,
367+
rowNumber: 0,
368+
separatorType: st,
347369
})
348370
} else {
349371
t.renderRowSeparator(out, renderHint{
350-
isBorderTop: true,
351-
isHeaderRow: false,
352-
rowNumber: 0,
372+
isBorderTop: true,
373+
isHeaderRow: false,
374+
isSeparatorRow: true,
375+
rowNumber: 0,
376+
separatorType: st,
353377
})
354378
}
355379
}
@@ -360,14 +384,20 @@ func (t *Table) renderRowsFooter(out *strings.Builder) {
360384
isFooterRow: true,
361385
isFirstRow: true,
362386
isSeparatorRow: true,
387+
separatorType: separatorTypeFooterTop,
363388
})
364389
t.renderRows(out, t.rowsFooter, renderHint{isFooterRow: true})
365390
}
366391
}
367392

368393
func (t *Table) renderRowsHeader(out *strings.Builder) {
369394
if len(t.rowsHeader) > 0 || t.autoIndex {
370-
hintSeparator := renderHint{isHeaderRow: true, isLastRow: true, isSeparatorRow: true}
395+
hintSeparator := renderHint{
396+
isHeaderRow: true,
397+
isLastRow: true,
398+
isSeparatorRow: true,
399+
separatorType: separatorTypeHeaderMiddle,
400+
}
371401

372402
if len(t.rowsHeader) > 0 {
373403
t.renderRows(out, t.rowsHeader, renderHint{isHeaderRow: true})
@@ -376,6 +406,8 @@ func (t *Table) renderRowsHeader(out *strings.Builder) {
376406
t.renderRow(out, t.getAutoIndexColumnIDs(), renderHint{isAutoIndexRow: true, isHeaderRow: true})
377407
hintSeparator.rowNumber = 1
378408
}
409+
410+
hintSeparator.separatorType = separatorTypeHeaderBottom
379411
t.renderRowSeparator(out, hintSeparator)
380412
}
381413
}
@@ -393,8 +425,9 @@ func (t *Table) renderTitle(out *strings.Builder) {
393425
}
394426
if t.style.Options.DrawBorder {
395427
lenBorder := rowLength - text.StringWidthWithoutEscSequences(t.style.Box.TopLeft+t.style.Box.TopRight)
428+
middleHorizontal := t.style.Box.middleHorizontal(separatorTypeTitleTop)
396429
out.WriteString(colorsBorder.Sprint(t.style.Box.TopLeft))
397-
out.WriteString(colorsBorder.Sprint(text.RepeatAndTrim(t.style.Box.MiddleHorizontal, lenBorder)))
430+
out.WriteString(colorsBorder.Sprint(text.RepeatAndTrim(middleHorizontal, lenBorder)))
398431
out.WriteString(colorsBorder.Sprint(t.style.Box.TopRight))
399432
}
400433

table/render_csv.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
// 300,Tyrion,Lannister,5000,
1515
// ,,Total,10000,
1616
func (t *Table) RenderCSV() string {
17-
t.initForRender()
17+
t.initForRender(renderModeCSV)
1818

1919
var out strings.Builder
2020
if t.numColumns > 0 {

table/render_hint.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type renderHint struct {
1515
isTitleRow bool // title row?
1616
rowLineNumber int // the line number for a multi-line row
1717
rowNumber int // the row number/index
18+
separatorType separatorType
1819
}
1920

2021
func (h *renderHint) isBorderOrSeparator() bool {
@@ -37,3 +38,13 @@ func (h *renderHint) isHeaderOrFooterSeparator() bool {
3738
func (h *renderHint) isLastLineOfLastRow() bool {
3839
return h.isLastLineOfRow && h.isLastRow
3940
}
41+
42+
type renderMode string
43+
44+
const (
45+
renderModeDefault renderMode = "default"
46+
renderModeCSV renderMode = "csv"
47+
renderModeMarkdown renderMode = "markdown"
48+
renderModeTSV renderMode = "tsv"
49+
renderModeHTML renderMode = "html"
50+
)

table/render_html.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ const (
6060
// </tfoot>
6161
// </table>
6262
func (t *Table) RenderHTML() string {
63-
t.initForRender()
63+
t.initForRender(renderModeHTML)
6464

6565
var out strings.Builder
6666
if t.numColumns > 0 {

table/render_init.go

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,9 @@ func (t *Table) reBalanceMaxMergedColumnLengths() {
153153
}
154154
}
155155

156-
func (t *Table) initForRender() {
156+
func (t *Table) initForRender(mode renderMode) {
157+
t.renderMode = mode
158+
157159
// pick a default style if none was set until now
158160
t.Style()
159161

@@ -348,10 +350,57 @@ func (t *Table) initForRenderRowPainterColors() {
348350
}
349351

350352
func (t *Table) initForRenderRowSeparator() {
351-
t.rowSeparator = make(rowStr, t.numColumns)
352-
for colIdx, maxColumnLength := range t.maxColumnLengths {
353-
maxColumnLength += text.StringWidthWithoutEscSequences(t.style.Box.PaddingLeft + t.style.Box.PaddingRight)
354-
t.rowSeparator[colIdx] = text.RepeatAndTrim(t.style.Box.MiddleHorizontal, maxColumnLength)
353+
// this is needed only for default render mode
354+
if t.renderMode != renderModeDefault {
355+
return
356+
}
357+
358+
// init the separatorType -> separator-string map
359+
t.initForRenderRowSeparatorStrings()
360+
361+
// init the separator-string -> separator-row map
362+
t.rowSeparators = make(map[string]rowStr, len(t.rowSeparatorStrings))
363+
paddingLength := text.StringWidthWithoutEscSequences(t.style.Box.PaddingLeft + t.style.Box.PaddingRight)
364+
for _, separator := range t.rowSeparatorStrings {
365+
t.rowSeparators[separator] = make(rowStr, t.numColumns)
366+
for colIdx, maxColumnLength := range t.maxColumnLengths {
367+
t.rowSeparators[separator][colIdx] = text.RepeatAndTrim(separator, maxColumnLength+paddingLength)
368+
}
369+
}
370+
}
371+
372+
func (t *Table) initForRenderRowSeparatorStrings() {
373+
// allocate and init only the separators that are needed
374+
t.rowSeparatorStrings = make(map[separatorType]string)
375+
addSeparatorType := func(st separatorType) {
376+
t.rowSeparatorStrings[st] = t.style.Box.middleHorizontal(st)
377+
}
378+
379+
// for other render modes, we need all the separators
380+
if t.title != "" {
381+
addSeparatorType(separatorTypeTitleTop)
382+
addSeparatorType(separatorTypeTitleBottom)
383+
}
384+
if len(t.rowsHeader) > 0 || t.autoIndex {
385+
addSeparatorType(separatorTypeHeaderTop)
386+
addSeparatorType(separatorTypeHeaderBottom)
387+
if len(t.rowsHeader) > 1 {
388+
addSeparatorType(separatorTypeHeaderMiddle)
389+
}
390+
}
391+
if len(t.rows) > 0 {
392+
addSeparatorType(separatorTypeRowTop)
393+
addSeparatorType(separatorTypeRowBottom)
394+
if len(t.rows) > 1 {
395+
addSeparatorType(separatorTypeRowMiddle)
396+
}
397+
}
398+
if len(t.rowsFooter) > 0 || t.autoIndex {
399+
addSeparatorType(separatorTypeFooterTop)
400+
addSeparatorType(separatorTypeFooterBottom)
401+
if len(t.rowsFooter) > 1 {
402+
addSeparatorType(separatorTypeFooterMiddle)
403+
}
355404
}
356405
}
357406

@@ -409,7 +458,7 @@ func (t *Table) reset() {
409458
t.maxRowLength = 0
410459
t.numColumns = 0
411460
t.numLinesRendered = 0
412-
t.rowSeparator = nil
461+
t.rowSeparators = nil
413462
t.rows = nil
414463
t.rowsColors = nil
415464
t.rowsFooter = nil

table/render_markdown.go

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
// | 300 | Tyrion | Lannister | 5000 | |
1515
// | | | Total | 10000 | |
1616
func (t *Table) RenderMarkdown() string {
17-
t.initForRender()
17+
t.initForRender(renderModeMarkdown)
1818

1919
var out strings.Builder
2020
if t.numColumns > 0 {
@@ -47,19 +47,15 @@ func (t *Table) markdownRenderRow(out *strings.Builder, row rowStr, hint renderH
4747
for colIdx := 0; colIdx < t.numColumns; colIdx++ {
4848
t.markdownRenderRowAutoIndex(out, colIdx, hint)
4949

50-
if hint.isSeparatorRow {
51-
out.WriteString(t.getAlign(colIdx, hint).MarkdownProperty())
52-
} else {
53-
var colStr string
54-
if colIdx < len(row) {
55-
colStr = row[colIdx]
56-
}
57-
out.WriteRune(' ')
58-
colStr = strings.ReplaceAll(colStr, "|", "\\|")
59-
colStr = strings.ReplaceAll(colStr, "\n", "<br/>")
60-
out.WriteString(colStr)
61-
out.WriteRune(' ')
50+
var colStr string
51+
if colIdx < len(row) {
52+
colStr = row[colIdx]
6253
}
54+
out.WriteRune(' ')
55+
colStr = strings.ReplaceAll(colStr, "|", "\\|")
56+
colStr = strings.ReplaceAll(colStr, "\n", "<br/>")
57+
out.WriteString(colStr)
58+
out.WriteRune(' ')
6359
out.WriteRune('|')
6460
}
6561
}
@@ -83,7 +79,7 @@ func (t *Table) markdownRenderRows(out *strings.Builder, rows []rowStr, hint ren
8379
t.markdownRenderRow(out, row, hint)
8480

8581
if idx == len(rows)-1 && hint.isHeaderRow {
86-
t.markdownRenderRow(out, t.rowSeparator, renderHint{isSeparatorRow: true})
82+
t.markdownRenderSeparator(out, renderHint{isSeparatorRow: true})
8783
}
8884
}
8985
}
@@ -101,6 +97,21 @@ func (t *Table) markdownRenderRowsHeader(out *strings.Builder) {
10197
}
10298
}
10399

100+
func (t *Table) markdownRenderSeparator(out *strings.Builder, hint renderHint) {
101+
// when working on line number 2 or more, insert a newline first
102+
if out.Len() > 0 {
103+
out.WriteRune('\n')
104+
}
105+
106+
out.WriteRune('|')
107+
for colIdx := 0; colIdx < t.numColumns; colIdx++ {
108+
t.markdownRenderRowAutoIndex(out, colIdx, hint)
109+
110+
out.WriteString(t.getAlign(colIdx, hint).MarkdownProperty())
111+
out.WriteRune('|')
112+
}
113+
}
114+
104115
func (t *Table) markdownRenderTitle(out *strings.Builder) {
105116
if t.title != "" {
106117
out.WriteString("# ")

0 commit comments

Comments
 (0)