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(" `) } +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(), ` + + + + + + + + + + + + + + + + + + + + + + +
ABC
YY1
N2
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] {