fix(parser): grow truncated rows to host inherited merge values (#21044)

This commit is contained in:
jay77721 authored and GitHub committed 2026-10-16 12:29:39 +08:00
1 parent 150977db52
commit a2f22bebb5
2 files changed
+44 -7

No files matched your search

+12 -7
View File
@@ -594,11 +594,14 @@ func inheritMergedHeader(records [][]string, headerRowIdx int, mm map[[2]int][2]
// inheritMergedDataCells fills empty cells of non-header rows with the value
// of their merge master, so a vertically merged group label (e.g. A2:A4) is
// visible on every data row instead of only the first. Unlike the header-row
// path above, data rows are never widened: only cells already present in the
// row slice are filled, so a far merge cannot allocate a sea of empty <td>
// cells (see padRowToWidth for the blow-up this avoids). Call with absolute
// sheet row numbers, before records are reordered around the header row.
// visible on every data row instead of only the first. excelize truncates
// each row at its last valued cell, so a slave cell past that point is absent
// from the slice: the row is then extended exactly up to the slave column to
// host the real inherited value. The extension is bounded by the master row's
// own width (the master cell must exist and be non-empty), so unlike header
// padding it cannot chase a far merge across the sheet (see padRowToWidth).
// Call with absolute sheet row numbers, before records are reordered around
// the header row.
func inheritMergedDataCells(rows [][]string, headerRow int, ranges []mergeRange) {
for _, r := range ranges {
// Vertical merges only: a horizontally merged data cell is already
@@ -629,8 +632,10 @@ func inheritMergedDataCells(rows [][]string, headerRow int, ranges []mergeRange)
if row == headerRow {
continue
}
if r.sc > len(rows[row-1]) {
continue
// Grow a truncated row exactly up to the slave column; the
// appended cells stay empty except the inherited value below.
for len(rows[row-1]) < r.sc {
rows[row-1] = append(rows[row-1], "")
}
if strings.TrimSpace(rows[row-1][r.sc-1]) == "" {
rows[row-1][r.sc-1] = master
@@ -566,6 +566,38 @@ func TestXLSXParser_MergedDataCellHorizontalUntouched(t *testing.T) {
}
}
// TestXLSXParser_MergedDataCellBeyondTruncation asserts a merged slave cell
// past its row's truncation point (excelize drops trailing empties) is still
// filled: the row grows exactly up to the slave column to host the value.
func TestXLSXParser_MergedDataCellBeyondTruncation(t *testing.T) {
data := newTestXLSX(t, func(f *excelize.File) {
mustSetCell(t, f, "Sheet1", "A1", "name")
mustSetCell(t, f, "Sheet1", "B1", "age")
mustSetCell(t, f, "Sheet1", "C1", "department")
mustSetCell(t, f, "Sheet1", "D1", "notes")
mustSetCell(t, f, "Sheet1", "A2", "Mike")
mustSetCell(t, f, "Sheet1", "B2", 28)
mustSetCell(t, f, "Sheet1", "C2", "Sales")
mustSetCell(t, f, "Sheet1", "D2", "Important customer")
mustMergeCell(t, f, "Sheet1", "A2", "A3")
mustMergeCell(t, f, "Sheet1", "B2", "B3")
mustMergeCell(t, f, "Sheet1", "D2", "D3")
// Row 3's own value sits before the merged columns, so GetRows
// truncates the row short of the D3 slave.
mustSetCell(t, f, "Sheet1", "C3", "IT")
})
p, _ := NewXLSXParser("")
res := p.ParseWithResult(t.Context(), "t.xlsx", data)
if res.Err != nil {
t.Fatalf("ParseWithResult: %v", res.Err)
}
text := spreadsheetText(res)
want := "<tr><td>Mike</td><td>28</td><td>IT</td><td>Important customer</td></tr>"
if !strings.Contains(text, want) {
t.Fatalf("truncated slave cells not filled, got:\n%s", text)
}
}
// TestXLSXParser_MergedHeaderDownIntoDataUntouched asserts a vertical merge
// mastered on the header row does not leak the header label into data rows.
func TestXLSXParser_MergedHeaderDownIntoDataUntouched(t *testing.T) {