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 Jan 4, 2025
1 parent bfe1b7c commit 0de5488
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 7 deletions.
2 changes: 1 addition & 1 deletion 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
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
27 changes: 22 additions & 5 deletions table/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,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 @@ -451,7 +451,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 @@ -522,12 +522,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 @@ -836,7 +836,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 @@ -855,6 +855,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 0de5488

Please sign in to comment.