fix: a vertically merged cell keeps the edges of the rows it covers
A cell's grid box claimed one row, so a w:vMerge reaching the table's last row sat inside the banded region instead of on its bottom edge — and a band declaring insideH nil then dropped the line. The merge extent is counted before the covered rows are read. Measured against LibreOffice: the bottom rule runs the full width under a merged first column, which the editor now draws too.
This commit is contained in:
1 parent
04d1fed5f0
commit
9d494b2966
3 files changed
+45
-2
No files matched your search
@@ -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
|
||||
|
||||
+24
-1
@@ -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');
|
||||
|
||||
@@ -40,6 +40,17 @@ const table = (look = '<w:tblLook w:firstRow="1" w:firstColumn="1" w:noHBand="0"
|
||||
`<w:tr>${[0, 1].map((c) => `<w:tc><w:p><w:r><w:t>r${r}c${c}</w:t></w:r></w:p></w:tc>`).join('')}</w:tr>`,
|
||||
).join('') + '</w:tbl>';
|
||||
|
||||
// The same grid with its first column merged from the first body row down to the last.
|
||||
const mergedTable = () =>
|
||||
`<w:tbl><w:tblPr><w:tblStyle w:val="Banded"/><w:tblLook w:firstRow="1" w:firstColumn="1" w:noHBand="0" w:noVBand="1"/></w:tblPr>
|
||||
<w:tblGrid><w:gridCol w:w="4000"/><w:gridCol w:w="4000"/></w:tblGrid>` +
|
||||
[0, 1, 2, 3].map((r) => {
|
||||
const merge = r === 1 ? '<w:vMerge w:val="restart"/>' : '<w:vMerge/>';
|
||||
const first = r === 0 ? '<w:tc><w:p><w:r><w:t>h</w:t></w:r></w:p></w:tc>'
|
||||
: `<w:tc><w:tcPr>${merge}</w:tcPr><w:p><w:r><w:t>m</w:t></w:r></w:p></w:tc>`;
|
||||
return `<w:tr>${first}<w:tc><w:p><w:r><w:t>r${r}</w:t></w:r></w:p></w:tc></w:tr>`;
|
||||
}).join('') + '</w:tbl>';
|
||||
|
||||
const docxWith = (body: string) =>
|
||||
zipSync({
|
||||
'word/document.xml': strToU8(`<?xml version="1.0"?><w:document xmlns:w="${W}"><w:body>${body}</w:body></w:document>`),
|
||||
@@ -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('<w:tblLook w:firstRow="0" w:firstColumn="0" w:noHBand="1" w:noVBand="1"/>')));
|
||||
expect(off[0][0].attrs.backgroundColor).toBeUndefined();
|
||||
|
||||
Reference in new issue
Block a user