Skip to content

Commit db68a45

Browse files
NadeemShadanNadeem Shadanliborm85
authored
Fix dontBreakRows rowSpan ending offset across pages (#2932)
* Fix dontBreakRows rowSpan ending offset across pages (#2895) * Update CHANGELOG.md --------- Co-authored-by: Nadeem Shadan <nadeem.s@iinerds.com> Co-authored-by: Libor M. <liborm85@gmail.com>
1 parent 4301e73 commit db68a45

File tree

3 files changed

+79
-1
lines changed

3 files changed

+79
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
```
1414
- Fixed extra blank page when using headerRows, dontBreakRows and cell pageBreak together
1515
- Fixed rendering of an invalid color name - previously it used the last valid color; now it defaults to black
16+
- Fixed dontBreakRows rowSpan ending offset across pages
1617

1718
## 0.3.7 - 2026-03-17
1819

src/LayoutBuilder.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -942,11 +942,13 @@ class LayoutBuilder {
942942
// We store a reference of the ending cell in the first cell of the rowspan
943943
cell._endingCell = rowSpanRightEndingCell;
944944
cell._endingCell._startingRowSpanY = cell._startingRowSpanY;
945+
cell._endingCell._startingRowSpanPage = cell._startingRowSpanPage;
945946
}
946947
if (rowSpanLeftEndingCell) {
947948
// We store a reference of the left ending cell in the first cell of the rowspan
948949
cell._leftEndingCell = rowSpanLeftEndingCell;
949950
cell._leftEndingCell._startingRowSpanY = cell._startingRowSpanY;
951+
cell._leftEndingCell._startingRowSpanPage = cell._startingRowSpanPage;
950952
}
951953

952954
// If we are after a cell that started a rowspan
@@ -984,7 +986,17 @@ class LayoutBuilder {
984986
if (dontBreakRows) {
985987
// Calculate how many points we have to discount to Y when dontBreakRows and rowSpan are combined
986988
const ctxBeforeRowSpanLastRow = this.writer.contextStack[this.writer.contextStack.length - 1];
987-
discountY = ctxBeforeRowSpanLastRow.y - cell._startingRowSpanY;
989+
const startsOnCurrentPage = (
990+
typeof cell._startingRowSpanPage === 'number' &&
991+
cell._startingRowSpanPage === ctxBeforeRowSpanLastRow.page
992+
);
993+
994+
if (startsOnCurrentPage && typeof cell._startingRowSpanY === 'number') {
995+
discountY = ctxBeforeRowSpanLastRow.y - cell._startingRowSpanY;
996+
}
997+
998+
// Do not increase Y by applying a negative discount.
999+
discountY = Math.max(0, discountY);
9881000
}
9891001
let originalXOffset = 0;
9901002
// If context was saved from an unbreakable block and we are not in an unbreakable block anymore
@@ -1170,6 +1182,7 @@ class LayoutBuilder {
11701182
tableNode.table.body[i].forEach(cell => {
11711183
if (cell.rowSpan && cell.rowSpan > 1) {
11721184
cell._startingRowSpanY = this.writer.context().y;
1185+
cell._startingRowSpanPage = this.writer.context().page;
11731186
}
11741187
});
11751188
}

tests/integration/tables.spec.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,4 +338,68 @@ describe('Integration test: tables', function () {
338338
});
339339
});
340340

341+
it('keeps row heights stable when rowSpan crosses pages with dontBreakRows (#2895)', function () {
342+
var dd = {
343+
content: {
344+
table: {
345+
dontBreakRows: true,
346+
heights: 45,
347+
widths: [50, 100, 200, 50],
348+
body: [
349+
['1', '2', '3', '4'],
350+
[{ rowSpan: 4, text: '4span' }, null, null, null],
351+
[null, null, null, null],
352+
[{ rowSpan: 2, text: '2span' }, null, null, null],
353+
[null, null, null, null],
354+
[{ rowSpan: 2, text: null }, null, null, null],
355+
[null, null, null, null],
356+
[{ rowSpan: 2, text: null }, null, null, null],
357+
[null, null, null, null],
358+
[null, null, null, null],
359+
[{ rowSpan: 15, text: 'span 15', maxHeight: 50 }, null, null, null],
360+
[null, null, null, null],
361+
[null, null, null, null],
362+
[null, null, null, null],
363+
[null, null, null, null],
364+
[null, null, null, null],
365+
[null, null, null, null],
366+
[null, null, null, null],
367+
[null, null, null, null],
368+
[null, null, null, null],
369+
[null, null, null, null],
370+
[null, null, null, null],
371+
[null, null, null, null],
372+
[null, null, null, null],
373+
[null, null, null, null],
374+
[null, null, null, null],
375+
[null, null, null, null],
376+
[{ rowSpan: 5, text: 'span 5' }, null, null, null],
377+
[null, null, null, null],
378+
[{ rowSpan: 2, text: null }, null, null, null],
379+
[null, null, null, null],
380+
[null, null, null, null]
381+
]
382+
}
383+
}
384+
};
385+
386+
var pages = testHelper.renderPages('A4', dd);
387+
var lastPage = pages[pages.length - 1];
388+
var horizontalLineYs = [...new Set(
389+
lastPage.items
390+
.filter(node => node.type === 'vector' && node.item.type === 'line' && Math.abs(node.item.y1 - node.item.y2) < 0.001)
391+
.map(node => Number(node.item.y1.toFixed(3)))
392+
)].sort((a, b) => a - b);
393+
394+
var maxGap = 0;
395+
for (var i = 1; i < horizontalLineYs.length; i++) {
396+
maxGap = Math.max(maxGap, horizontalLineYs[i] - horizontalLineYs[i - 1]);
397+
}
398+
399+
// Each row is 45pt tall. A gap above ~90pt would indicate a blown-out row caused
400+
// by a negative discountY when a rowspan started on a previous page. Allow up to
401+
// 2x row height (90pt) as a safe upper bound; anything beyond that is the bug.
402+
assert.ok(maxGap < 90, 'max gap between horizontal lines was ' + maxGap + 'pt, expected < 90pt');
403+
});
404+
341405
});

0 commit comments

Comments
 (0)