diff --git a/src/lib/import/CLAUDE.md b/src/lib/import/CLAUDE.md index 9272a85..6f00a07 100644 --- a/src/lib/import/CLAUDE.md +++ b/src/lib/import/CLAUDE.md @@ -59,7 +59,10 @@ which is where an assigned style's regions otherwise come from. An area covers a its `top`/`bottom`/`left`/`right` apply where the cell sits on that box's edge, its `insideH`/`insideV` anywhere within. A **band's box is the whole banded region**, not the one row it shades — that is what makes `insideH` the line *between* two band rows, which -is how a style with no inner rules (Word's Medium Shading) comes out borderless. Areas +is how a style with no inner rules (Word's Medium Shading) comes out borderless. A cell's +own box spans the rows its `w:vMerge` covers — counted ahead of the covered rows +(`vMergeRows`) — or a merge reaching the last row loses its bottom edge to that same rule. +Areas layer in Word's ascending precedence, over the table's borders and under the cell's own `w:tcPr`; banding counts from the first *body* row, so the row under a header row is band 1. `w:tblLook` decides which areas apply at all — named flags where the file has them, the diff --git a/src/lib/import/docx.ts b/src/lib/import/docx.ts index 88a3661..9e8ed20 100644 --- a/src/lib/import/docx.ts +++ b/src/lib/import/docx.ts @@ -1704,6 +1704,28 @@ function condAreasFor( return out; } +// How many rows a vertically merged cell covers. Its box has to say so before the covered +// rows are read, or a merge reaching the table's last row reads as an inside line there — +// which a style whose band declares insideH nil then drops. +function vMergeRows(trs: Element[], from: number, col: number): number { + let n = 1; + for (let ri = from + 1; ri < trs.length; ri++) { + let c = 0; + let covered = false; + for (const tc of fcAll(trs[ri], 'tc')) { + if (c >= col) { + const el = fc(fc(tc, 'tcPr'), 'vMerge'); + covered = c === col && !!el && (wVal(el) ?? 'continue') !== 'restart'; + break; + } + c += intAttr(fc(fc(tc, 'tcPr'), 'gridSpan'), W, 'val') ?? 1; + } + if (!covered) break; + n++; + } + return n; +} + // Which side of an area a cell's edge is: its own outer side where the cell sits on the // area's edge, the area's inside line otherwise. function areaSide(area: GridBox, cell: GridBox, side: 'top' | 'bottom' | 'left' | 'right'): string { @@ -1825,7 +1847,8 @@ function buildTable(tbl: Element, ctx: Ctx): Node | null { col += colspan; continue; // covered cell — dropped, span folded into its origin } - const box: GridBox = { row: ri, col, rowEnd: ri + 1, colEnd: col + colspan }; + const rowEnd = ri + (vMerge === 'restart' ? vMergeRows(trs, ri, col) : 1); + const box: GridBox = { row: ri, col, rowEnd, colEnd: col + colspan }; const areas = conds.size ? condAreasFor(conds, flags, band, box, trs.length, gridCols) : []; const paint = condPaint(areas); const fill = fc(tcPr, 'shd')?.getAttributeNS(W, 'fill'); diff --git a/tests/unit/docx-table-style.test.ts b/tests/unit/docx-table-style.test.ts index f204636..dbbb802 100644 --- a/tests/unit/docx-table-style.test.ts +++ b/tests/unit/docx-table-style.test.ts @@ -40,6 +40,17 @@ const table = (look = '${[0, 1].map((c) => `r${r}c${c}`).join('')}`, ).join('') + ''; +// The same grid with its first column merged from the first body row down to the last. +const mergedTable = () => + ` + ` + + [0, 1, 2, 3].map((r) => { + const merge = r === 1 ? '' : ''; + const first = r === 0 ? 'h' + : `${merge}m`; + return `${first}r${r}`; + }).join('') + ''; + const docxWith = (body: string) => zipSync({ 'word/document.xml': strToU8(`${body}`), @@ -77,6 +88,12 @@ describe('a Word table style paints its conditional areas', () => { expect(rows[3][0].attrs.borderBottom).toBe('1pt solid #4F81BD'); // the table's own edge }); + it('gives a merged cell the edges of every row it covers', () => { + const merged = cellsOf(docxWith(mergedTable())); + expect(merged[1][0].attrs.rowspan).toBe(3); + expect(merged[1][0].attrs.borderBottom).toBe('1pt solid #4F81BD'); // the table's own edge + }); + it('paints nothing the table’s look switches off', () => { const off = cellsOf(docxWith(table(''))); expect(off[0][0].attrs.backgroundColor).toBeUndefined();