diff --git a/table/config.go b/table/config.go
index fb96d70..c36124a 100644
--- a/table/config.go
+++ b/table/config.go
@@ -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
@@ -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)
diff --git a/table/render.go b/table/render.go
index 2fd759e..4ef68f9 100644
--- a/table/render.go
+++ b/table/render.go
@@ -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
diff --git a/table/render_html.go b/table/render_html.go
index decf4fa..729f95e 100644
--- a/table/render_html.go
+++ b/table/render_html.go
@@ -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)
@@ -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 {
@@ -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(" ")
diff --git a/table/render_html_test.go b/table/render_html_test.go
index ad2eae3..d0f8509 100644
--- a/table/render_html_test.go
+++ b/table/render_html_test.go
@@ -518,6 +518,44 @@ func TestTable_RenderHTML_Sorted(t *testing.T) {
`)
}
+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(), `
+
+
+
+ A |
+ B |
+ C |
+
+
+
+
+ Y |
+ Y |
+ 1 |
+
+
+ N |
+ 2 |
+
+
+ 3 |
+
+
+
`)
+ })
+}
+
func TestTable_RenderHTML_RowAutoMerge(t *testing.T) {
t.Run("simple", func(t *testing.T) {
rcAutoMerge := RowConfig{AutoMerge: true}
@@ -544,6 +582,7 @@ func TestTable_RenderHTML_RowAutoMerge(t *testing.T) {
`)
})
+
t.Run("merged and unmerged entries", func(t *testing.T) {
rcAutoMerge := RowConfig{AutoMerge: true}
tw := NewWriter()
diff --git a/table/render_tsv.go b/table/render_tsv.go
index bb73975..67f1e7f 100644
--- a/table/render_tsv.go
+++ b/table/render_tsv.go
@@ -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)
}
@@ -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
diff --git a/table/table.go b/table/table.go
index c15ccd7..191dd77 100644
--- a/table/table.go
+++ b/table/table.go
@@ -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
@@ -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
@@ -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)
}
@@ -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)
@@ -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] {