Skip to content

Commit

Permalink
table: support vertical merge in HTML rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
jedib0t committed Mar 1, 2025
1 parent 5a8fa5f commit 343d631
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 13 deletions.
4 changes: 2 additions & 2 deletions table/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ type ColumnConfig struct {
// 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)
// * Does not work in CSV/Markdown render modes
//
// Works best when:
// * Style().Options.SeparateRows == true
Expand Down Expand Up @@ -87,8 +87,8 @@ type RowConfig struct {
// 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)
// * Does not work in CSV/Markdown render modes
AutoMerge bool

// Alignment to use on a merge (defaults to text.AlignCenter)
Expand Down
2 changes: 1 addition & 1 deletion table/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func (t *Table) renderColumn(out *strings.Builder, row rowStr, colIdx int, maxCo
}

// extract the text, convert-case if not-empty and align horizontally
mergeVertically := t.shouldMergeCellsVertically(colIdx, hint)
mergeVertically := t.shouldMergeCellsVerticallyAbove(colIdx, hint)
var colStr string
if mergeVertically {
// leave colStr empty; align will expand the column as necessary
Expand Down
8 changes: 8 additions & 0 deletions table/render_html.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ func (t *Table) htmlRenderRow(out *strings.Builder, row rowStr, hint renderHint)
if colIdx == 0 && t.autoIndex {
t.htmlRenderColumnAutoIndex(out, hint)
}
// auto-merged columns should be skipped
if t.shouldMergeCellsVerticallyAbove(colIdx, hint) {
continue
}

align := t.getAlign(colIdx, hint)
rowConfig := t.getRowConfig(hint)
Expand All @@ -184,6 +188,9 @@ func (t *Table) htmlRenderRow(out *strings.Builder, row rowStr, hint renderHint)
if extraColumnsRendered > 0 {
out.WriteString(" colspan=")
out.WriteString(fmt.Sprint(extraColumnsRendered + 1))
} else if rowSpan := t.shouldMergeCellsVerticallyBelow(colIdx, hint); rowSpan > 1 {
out.WriteString(" rowspan=")
out.WriteString(fmt.Sprint(rowSpan))
}
out.WriteString(">")
if len(colStr) == 0 {
Expand Down Expand Up @@ -222,6 +229,7 @@ func (t *Table) htmlRenderRows(out *strings.Builder, rows []rowStr, hint renderH
t.htmlRenderRow(out, row, hint)
shouldRenderTagClose = true
}
t.firstRowOfPage = false
}
if shouldRenderTagClose {
out.WriteString(" </")
Expand Down
39 changes: 39 additions & 0 deletions table/render_html_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,44 @@ func TestTable_RenderHTML_Sorted(t *testing.T) {
</table>`)
}

func TestTable_RenderHTML_ColAutoMerge(t *testing.T) {
t.Run("simple", func(t *testing.T) {
tw := NewWriter()
tw.AppendHeader(Row{"A", "B", "C"})
tw.AppendRow(Row{"Y", "Y", 1})
tw.AppendRow(Row{"Y", "N", 2})
tw.AppendRow(Row{"Y", "N", 3})
tw.SetColumnConfigs([]ColumnConfig{
{Name: "A", AutoMerge: true},
{Name: "B", AutoMerge: true},
})
compareOutput(t, tw.RenderHTML(), `
<table class="go-pretty-table">
<thead>
<tr>
<th>A</th>
<th>B</th>
<th align="right">C</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan=3>Y</td>
<td>Y</td>
<td align="right">1</td>
</tr>
<tr>
<td rowspan=2>N</td>
<td align="right">2</td>
</tr>
<tr>
<td align="right">3</td>
</tr>
</tbody>
</table>`)
})
}

func TestTable_RenderHTML_RowAutoMerge(t *testing.T) {
t.Run("simple", func(t *testing.T) {
rcAutoMerge := RowConfig{AutoMerge: true}
Expand All @@ -544,6 +582,7 @@ func TestTable_RenderHTML_RowAutoMerge(t *testing.T) {
</tbody>
</table>`)
})

t.Run("merged and unmerged entries", func(t *testing.T) {
rcAutoMerge := RowConfig{AutoMerge: true}
tw := NewWriter()
Expand Down
7 changes: 2 additions & 5 deletions table/render_tsv.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ func (t *Table) tsvRenderRow(out *strings.Builder, row rowStr, hint renderHint)
}

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

func (t *Table) tsvFixDoubleQuotes(str string) string {
return strings.Replace(str, "\"", "\"\"", -1)
}

func (t *Table) tsvRenderRows(out *strings.Builder, rows []rowStr, hint renderHint) {
for idx, row := range rows {
hint.rowNumber = idx + 1
Expand Down
27 changes: 22 additions & 5 deletions table/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ func (t *Table) getBorderLeft(hint renderHint) string {
} else if hint.isSeparatorRow {
if t.autoIndex && hint.isHeaderOrFooterSeparator() {
border = t.style.Box.Left
} else if !t.autoIndex && t.shouldMergeCellsVertically(0, hint) {
} else if !t.autoIndex && t.shouldMergeCellsVerticallyAbove(0, hint) {
border = t.style.Box.Left
} else {
border = t.style.Box.LeftSeparator
Expand All @@ -454,7 +454,7 @@ func (t *Table) getBorderRight(hint renderHint) string {
} else if hint.isBorderBottom {
border = t.style.Box.BottomRight
} else if hint.isSeparatorRow {
if t.shouldMergeCellsVertically(t.numColumns-1, hint) {
if t.shouldMergeCellsVerticallyAbove(t.numColumns-1, hint) {
border = t.style.Box.Right
} else {
border = t.style.Box.RightSeparator
Expand Down Expand Up @@ -525,12 +525,12 @@ func (t *Table) getColumnSeparator(row rowStr, colIdx int, hint renderHint) stri
}

func (t *Table) getColumnSeparatorNonBorder(mergeCellsAbove bool, mergeCellsBelow bool, colIdx int, hint renderHint) string {
mergeNextCol := t.shouldMergeCellsVertically(colIdx, hint)
mergeNextCol := t.shouldMergeCellsVerticallyAbove(colIdx, hint)
if hint.isAutoIndexColumn {
return t.getColumnSeparatorNonBorderAutoIndex(mergeNextCol, hint)
}

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

Expand Down Expand Up @@ -839,7 +839,7 @@ func (t *Table) shouldMergeCellsHorizontallyBelow(row rowStr, colIdx int, hint r
return false
}

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

func (t *Table) shouldMergeCellsVerticallyBelow(colIdx int, hint renderHint) int {
numRowsToMerge := 0
if t.columnConfigMap[colIdx].AutoMerge && colIdx < t.numColumns {
numRowsToMerge = 1
rowCurr := t.getRow(hint.rowNumber-1, hint)
for rowIdx := hint.rowNumber; rowIdx < len(t.rows); rowIdx++ {
rowNext := t.getRow(rowIdx, hint)
if colIdx < len(rowCurr) && colIdx < len(rowNext) && rowNext[colIdx] == rowCurr[colIdx] {
numRowsToMerge++
} else {
break
}
}
}
return numRowsToMerge
}

func (t *Table) shouldSeparateRows(rowIdx int, numRows int) bool {
// not asked to separate rows and no manually added separator
if !t.style.Options.SeparateRows && !t.separators[rowIdx] {
Expand Down

0 comments on commit 343d631

Please # to comment.