Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 343d631

Browse files
committedMar 1, 2025·
table: support vertical merge in HTML rendering
1 parent 5a8fa5f commit 343d631

File tree

6 files changed

+74
-13
lines changed

6 files changed

+74
-13
lines changed
 

‎table/config.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ type ColumnConfig struct {
2525
// AutoMerge merges cells with similar values and prevents separators from
2626
// being drawn. Caveats:
2727
// * VAlign is applied on the individual cell and not on the merged cell
28-
// * Does not work in CSV/HTML/Markdown render modes
2928
// * Does not work well with horizontal auto-merge (RowConfig.AutoMerge)
29+
// * Does not work in CSV/Markdown render modes
3030
//
3131
// Works best when:
3232
// * Style().Options.SeparateRows == true
@@ -87,8 +87,8 @@ type RowConfig struct {
8787
// being drawn. Caveats:
8888
// * Align is overridden to text.AlignCenter on the merged cell (unless set
8989
// by AutoMergeAlign value below)
90-
// * Does not work in CSV/HTML/Markdown render modes
9190
// * Does not work well with vertical auto-merge (ColumnConfig.AutoMerge)
91+
// * Does not work in CSV/Markdown render modes
9292
AutoMerge bool
9393

9494
// Alignment to use on a merge (defaults to text.AlignCenter)

‎table/render.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func (t *Table) renderColumn(out *strings.Builder, row rowStr, colIdx int, maxCo
6767
}
6868

6969
// extract the text, convert-case if not-empty and align horizontally
70-
mergeVertically := t.shouldMergeCellsVertically(colIdx, hint)
70+
mergeVertically := t.shouldMergeCellsVerticallyAbove(colIdx, hint)
7171
var colStr string
7272
if mergeVertically {
7373
// leave colStr empty; align will expand the column as necessary

‎table/render_html.go

+8
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,10 @@ func (t *Table) htmlRenderRow(out *strings.Builder, row rowStr, hint renderHint)
159159
if colIdx == 0 && t.autoIndex {
160160
t.htmlRenderColumnAutoIndex(out, hint)
161161
}
162+
// auto-merged columns should be skipped
163+
if t.shouldMergeCellsVerticallyAbove(colIdx, hint) {
164+
continue
165+
}
162166

163167
align := t.getAlign(colIdx, hint)
164168
rowConfig := t.getRowConfig(hint)
@@ -184,6 +188,9 @@ func (t *Table) htmlRenderRow(out *strings.Builder, row rowStr, hint renderHint)
184188
if extraColumnsRendered > 0 {
185189
out.WriteString(" colspan=")
186190
out.WriteString(fmt.Sprint(extraColumnsRendered + 1))
191+
} else if rowSpan := t.shouldMergeCellsVerticallyBelow(colIdx, hint); rowSpan > 1 {
192+
out.WriteString(" rowspan=")
193+
out.WriteString(fmt.Sprint(rowSpan))
187194
}
188195
out.WriteString(">")
189196
if len(colStr) == 0 {
@@ -222,6 +229,7 @@ func (t *Table) htmlRenderRows(out *strings.Builder, rows []rowStr, hint renderH
222229
t.htmlRenderRow(out, row, hint)
223230
shouldRenderTagClose = true
224231
}
232+
t.firstRowOfPage = false
225233
}
226234
if shouldRenderTagClose {
227235
out.WriteString(" </")

‎table/render_html_test.go

+39
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,44 @@ func TestTable_RenderHTML_Sorted(t *testing.T) {
518518
</table>`)
519519
}
520520

521+
func TestTable_RenderHTML_ColAutoMerge(t *testing.T) {
522+
t.Run("simple", func(t *testing.T) {
523+
tw := NewWriter()
524+
tw.AppendHeader(Row{"A", "B", "C"})
525+
tw.AppendRow(Row{"Y", "Y", 1})
526+
tw.AppendRow(Row{"Y", "N", 2})
527+
tw.AppendRow(Row{"Y", "N", 3})
528+
tw.SetColumnConfigs([]ColumnConfig{
529+
{Name: "A", AutoMerge: true},
530+
{Name: "B", AutoMerge: true},
531+
})
532+
compareOutput(t, tw.RenderHTML(), `
533+
<table class="go-pretty-table">
534+
<thead>
535+
<tr>
536+
<th>A</th>
537+
<th>B</th>
538+
<th align="right">C</th>
539+
</tr>
540+
</thead>
541+
<tbody>
542+
<tr>
543+
<td rowspan=3>Y</td>
544+
<td>Y</td>
545+
<td align="right">1</td>
546+
</tr>
547+
<tr>
548+
<td rowspan=2>N</td>
549+
<td align="right">2</td>
550+
</tr>
551+
<tr>
552+
<td align="right">3</td>
553+
</tr>
554+
</tbody>
555+
</table>`)
556+
})
557+
}
558+
521559
func TestTable_RenderHTML_RowAutoMerge(t *testing.T) {
522560
t.Run("simple", func(t *testing.T) {
523561
rcAutoMerge := RowConfig{AutoMerge: true}
@@ -544,6 +582,7 @@ func TestTable_RenderHTML_RowAutoMerge(t *testing.T) {
544582
</tbody>
545583
</table>`)
546584
})
585+
547586
t.Run("merged and unmerged entries", func(t *testing.T) {
548587
rcAutoMerge := RowConfig{AutoMerge: true}
549588
tw := NewWriter()

‎table/render_tsv.go

+2-5
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ func (t *Table) tsvRenderRow(out *strings.Builder, row rowStr, hint renderHint)
5050
}
5151

5252
if strings.ContainsAny(col, "\t\n\"") || strings.Contains(col, " ") {
53-
out.WriteString(fmt.Sprintf("\"%s\"", t.tsvFixDoubleQuotes(col)))
53+
col = strings.ReplaceAll(col, "\"", "\"\"") // fix double-quotes
54+
out.WriteString(fmt.Sprintf("\"%s\"", col))
5455
} else {
5556
out.WriteString(col)
5657
}
@@ -61,10 +62,6 @@ func (t *Table) tsvRenderRow(out *strings.Builder, row rowStr, hint renderHint)
6162
}
6263
}
6364

64-
func (t *Table) tsvFixDoubleQuotes(str string) string {
65-
return strings.Replace(str, "\"", "\"\"", -1)
66-
}
67-
6865
func (t *Table) tsvRenderRows(out *strings.Builder, rows []rowStr, hint renderHint) {
6966
for idx, row := range rows {
7067
hint.rowNumber = idx + 1

‎table/table.go

+22-5
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ func (t *Table) getBorderLeft(hint renderHint) string {
434434
} else if hint.isSeparatorRow {
435435
if t.autoIndex && hint.isHeaderOrFooterSeparator() {
436436
border = t.style.Box.Left
437-
} else if !t.autoIndex && t.shouldMergeCellsVertically(0, hint) {
437+
} else if !t.autoIndex && t.shouldMergeCellsVerticallyAbove(0, hint) {
438438
border = t.style.Box.Left
439439
} else {
440440
border = t.style.Box.LeftSeparator
@@ -454,7 +454,7 @@ func (t *Table) getBorderRight(hint renderHint) string {
454454
} else if hint.isBorderBottom {
455455
border = t.style.Box.BottomRight
456456
} else if hint.isSeparatorRow {
457-
if t.shouldMergeCellsVertically(t.numColumns-1, hint) {
457+
if t.shouldMergeCellsVerticallyAbove(t.numColumns-1, hint) {
458458
border = t.style.Box.Right
459459
} else {
460460
border = t.style.Box.RightSeparator
@@ -525,12 +525,12 @@ func (t *Table) getColumnSeparator(row rowStr, colIdx int, hint renderHint) stri
525525
}
526526

527527
func (t *Table) getColumnSeparatorNonBorder(mergeCellsAbove bool, mergeCellsBelow bool, colIdx int, hint renderHint) string {
528-
mergeNextCol := t.shouldMergeCellsVertically(colIdx, hint)
528+
mergeNextCol := t.shouldMergeCellsVerticallyAbove(colIdx, hint)
529529
if hint.isAutoIndexColumn {
530530
return t.getColumnSeparatorNonBorderAutoIndex(mergeNextCol, hint)
531531
}
532532

533-
mergeCurrCol := t.shouldMergeCellsVertically(colIdx-1, hint)
533+
mergeCurrCol := t.shouldMergeCellsVerticallyAbove(colIdx-1, hint)
534534
return t.getColumnSeparatorNonBorderNonAutoIndex(mergeCellsAbove, mergeCellsBelow, mergeCurrCol, mergeNextCol)
535535
}
536536

@@ -839,7 +839,7 @@ func (t *Table) shouldMergeCellsHorizontallyBelow(row rowStr, colIdx int, hint r
839839
return false
840840
}
841841

842-
func (t *Table) shouldMergeCellsVertically(colIdx int, hint renderHint) bool {
842+
func (t *Table) shouldMergeCellsVerticallyAbove(colIdx int, hint renderHint) bool {
843843
if !t.firstRowOfPage && t.columnConfigMap[colIdx].AutoMerge && colIdx < t.numColumns {
844844
if hint.isSeparatorRow {
845845
rowPrev := t.getRow(hint.rowNumber-1, hint)
@@ -858,6 +858,23 @@ func (t *Table) shouldMergeCellsVertically(colIdx int, hint renderHint) bool {
858858
return false
859859
}
860860

861+
func (t *Table) shouldMergeCellsVerticallyBelow(colIdx int, hint renderHint) int {
862+
numRowsToMerge := 0
863+
if t.columnConfigMap[colIdx].AutoMerge && colIdx < t.numColumns {
864+
numRowsToMerge = 1
865+
rowCurr := t.getRow(hint.rowNumber-1, hint)
866+
for rowIdx := hint.rowNumber; rowIdx < len(t.rows); rowIdx++ {
867+
rowNext := t.getRow(rowIdx, hint)
868+
if colIdx < len(rowCurr) && colIdx < len(rowNext) && rowNext[colIdx] == rowCurr[colIdx] {
869+
numRowsToMerge++
870+
} else {
871+
break
872+
}
873+
}
874+
}
875+
return numRowsToMerge
876+
}
877+
861878
func (t *Table) shouldSeparateRows(rowIdx int, numRows int) bool {
862879
// not asked to separate rows and no manually added separator
863880
if !t.style.Options.SeparateRows && !t.separators[rowIdx] {

0 commit comments

Comments
 (0)
Please sign in to comment.