From f5b5328b32c4bb2e4c790abe9f20dafc7d977921 Mon Sep 17 00:00:00 2001 From: VladaHarbour Date: Wed, 11 Feb 2026 17:58:17 +0200 Subject: [PATCH 1/2] fix: change footnotes layout calculation --- .../layout-bridge/src/incrementalLayout.ts | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/layout-engine/layout-bridge/src/incrementalLayout.ts b/packages/layout-engine/layout-bridge/src/incrementalLayout.ts index 82433951d4..47bb0030af 100644 --- a/packages/layout-engine/layout-bridge/src/incrementalLayout.ts +++ b/packages/layout-engine/layout-bridge/src/incrementalLayout.ts @@ -1668,32 +1668,32 @@ export async function incrementalLayout( let plan = computeFootnoteLayoutPlan(layout, idsByColumn, measuresById, [], pageColumns); let reserves = plan.reserves; - // If any reserves, relayout once, then re-assign and inject. + const MAX_FOOTNOTE_LAYOUT_PASSES = 4; + + // Relayout with footnote reserves and iterate until reserves and page count stabilize, + // so each page gets the correct reserve (avoids "too much" on one page and "not enough" on another). if (reserves.some((h) => h > 0)) { - layout = layoutDocument(currentBlocks, currentMeasures, { - ...options, - footnoteReservedByPageIndex: reserves, - headerContentHeights, - footerContentHeights, - remeasureParagraph: (block: FlowBlock, maxWidth: number, firstLineIndent?: number) => - remeasureParagraph(block as ParagraphBlock, maxWidth, firstLineIndent), - }); + for (let pass = 0; pass < MAX_FOOTNOTE_LAYOUT_PASSES; pass += 1) { + layout = layoutDocument(currentBlocks, currentMeasures, { + ...options, + footnoteReservedByPageIndex: reserves, + headerContentHeights, + footerContentHeights, + remeasureParagraph: (block: FlowBlock, maxWidth: number, firstLineIndent?: number) => + remeasureParagraph(block as ParagraphBlock, maxWidth, firstLineIndent), + }); + ({ columns: pageColumns, idsByColumn } = resolveFootnoteAssignments(layout)); + ({ measuresById } = await measureFootnoteBlocks(collectFootnoteIdsByColumn(idsByColumn))); + plan = computeFootnoteLayoutPlan(layout, idsByColumn, measuresById, reserves, pageColumns); + const nextReserves = plan.reserves; + const reservesStable = + nextReserves.length === reserves.length && + nextReserves.every((h, i) => (reserves[i] ?? 0) === h) && + reserves.every((h, i) => (nextReserves[i] ?? 0) === h); + reserves = nextReserves; + if (reservesStable) break; + } - // Pass 2: recompute assignment and reserves for the updated pagination. - ({ columns: pageColumns, idsByColumn } = resolveFootnoteAssignments(layout)); - ({ measuresById } = await measureFootnoteBlocks(collectFootnoteIdsByColumn(idsByColumn))); - plan = computeFootnoteLayoutPlan(layout, idsByColumn, measuresById, reserves, pageColumns); - reserves = plan.reserves; - - // Apply final reserves (best-effort second relayout) then inject fragments. - layout = layoutDocument(currentBlocks, currentMeasures, { - ...options, - footnoteReservedByPageIndex: reserves, - headerContentHeights, - footerContentHeights, - remeasureParagraph: (block: FlowBlock, maxWidth: number, firstLineIndent?: number) => - remeasureParagraph(block as ParagraphBlock, maxWidth, firstLineIndent), - }); let { columns: finalPageColumns, idsByColumn: finalIdsByColumn } = resolveFootnoteAssignments(layout); let { blocks: finalBlocks, measuresById: finalMeasuresById } = await measureFootnoteBlocks( collectFootnoteIdsByColumn(finalIdsByColumn), From 7e093b892f75540dfc88dfefd9f6f3f9b0a06726 Mon Sep 17 00:00:00 2001 From: VladaHarbour Date: Thu, 12 Feb 2026 17:41:01 +0200 Subject: [PATCH 2/2] fix: remove default top/bottom table padding --- .../layout-engine/layout-bridge/src/index.ts | 6 ++-- .../layout-engine/src/layout-table.ts | 4 +-- .../measuring/dom/src/index.test.ts | 32 ++++++++----------- .../layout-engine/measuring/dom/src/index.ts | 7 ++-- .../dom/src/table/renderTableCell.test.ts | 6 ++-- .../painters/dom/src/table/renderTableCell.ts | 6 ++-- 6 files changed, 28 insertions(+), 33 deletions(-) diff --git a/packages/layout-engine/layout-bridge/src/index.ts b/packages/layout-engine/layout-bridge/src/index.ts index f9c681381f..8323118ba6 100644 --- a/packages/layout-engine/layout-bridge/src/index.ts +++ b/packages/layout-engine/layout-bridge/src/index.ts @@ -702,9 +702,9 @@ export const hitTestTableFragment = ( const blockEndY = blockStartY + blockHeight; // Calculate position within the cell (accounting for cell padding) - const padding = cell.attrs?.padding ?? { top: 2, left: 4, right: 4, bottom: 2 }; + const padding = cell.attrs?.padding ?? { top: 0, left: 4, right: 4, bottom: 0 }; const cellLocalX = localX - colX - (padding.left ?? 4); - const cellLocalY = localY - rowY - (padding.top ?? 2); + const cellLocalY = localY - rowY - (padding.top ?? 0); const paragraphBlock = cellBlock as ParagraphBlock; const paragraphMeasure = cellBlockMeasure as ParagraphMeasure; @@ -1339,7 +1339,7 @@ type TableRowBlock = TableBlock['rows'][number]; type TableCellBlock = TableRowBlock['cells'][number]; type TableCellMeasure = TableMeasure['rows'][number]['cells'][number]; -const DEFAULT_CELL_PADDING = { top: 2, bottom: 2, left: 4, right: 4 }; +const DEFAULT_CELL_PADDING = { top: 0, bottom: 0, left: 4, right: 4 }; const getCellPaddingFromRow = (cellIdx: number, row?: TableRowBlock) => { const padding = row?.cells?.[cellIdx]?.attrs?.padding ?? {}; diff --git a/packages/layout-engine/layout-engine/src/layout-table.ts b/packages/layout-engine/layout-engine/src/layout-table.ts index b02c5d9fbe..1c5fc27000 100644 --- a/packages/layout-engine/layout-engine/src/layout-table.ts +++ b/packages/layout-engine/layout-engine/src/layout-table.ts @@ -376,8 +376,8 @@ type CellPadding = { top: number; bottom: number; left: number; right: number }; function getCellPadding(cellIdx: number, blockRow?: TableRow): CellPadding { const padding = blockRow?.cells?.[cellIdx]?.attrs?.padding ?? {}; return { - top: padding.top ?? 2, - bottom: padding.bottom ?? 2, + top: padding.top ?? 0, + bottom: padding.bottom ?? 0, left: padding.left ?? 4, right: padding.right ?? 4, }; diff --git a/packages/layout-engine/measuring/dom/src/index.test.ts b/packages/layout-engine/measuring/dom/src/index.test.ts index dd35aa5b11..2be57f989b 100644 --- a/packages/layout-engine/measuring/dom/src/index.test.ts +++ b/packages/layout-engine/measuring/dom/src/index.test.ts @@ -3255,14 +3255,13 @@ describe('measureBlock', () => { expect(cellMeasure.blocks[1].kind).toBe('paragraph'); expect(cellMeasure.blocks[2].kind).toBe('paragraph'); - // Heights should accumulate (3 paragraphs + padding) + // Heights should accumulate (3 paragraphs) const para1Height = cellMeasure.blocks[0].totalHeight; const para2Height = cellMeasure.blocks[1].totalHeight; const para3Height = cellMeasure.blocks[2].totalHeight; const totalContentHeight = para1Height + para2Height + para3Height; - const padding = 4; // Default top (2) + bottom (2) - expect(cellMeasure.height).toBe(totalContentHeight + padding); + expect(cellMeasure.height).toBe(totalContentHeight); }); it('measures cell with empty blocks array', async () => { @@ -3290,10 +3289,7 @@ describe('measureBlock', () => { const cellMeasure = measure.rows[0].cells[0]; expect(cellMeasure.blocks).toHaveLength(0); - - // Height should be just padding - const padding = 4; // Default top (2) + bottom (2) - expect(cellMeasure.height).toBe(padding); + expect(cellMeasure.height).toBe(0); }); it('maintains backward compatibility with legacy paragraph field', async () => { @@ -4579,8 +4575,8 @@ describe('measureBlock', () => { const para0Height = block0Measure.kind === 'paragraph' ? block0Measure.totalHeight : 0; const para1Height = block1Measure.kind === 'paragraph' ? block1Measure.totalHeight : 0; - // Cell height includes: para0Height + 10 + para1Height + 20 + padding (default 2 top + 2 bottom) - const expectedCellHeight = para0Height + 10 + para1Height + 20 + 4; + // Cell height includes: para0Height + 10 + para1Height + 20 + padding (default 0 top + 0 bottom) + const expectedCellHeight = para0Height + 10 + para1Height + 20 + 0; expect(cellMeasure.height).toBe(expectedCellHeight); }); @@ -4637,8 +4633,8 @@ describe('measureBlock', () => { // Only positive spacing should be added // Zero and negative spacing should not be added - // Cell height = para0 + para1 + para2 + 15 (positive spacing) + 4 (padding) - const expectedCellHeight = para0Height + para1Height + para2Height + 15 + 4; + // Cell height = para0 + para1 + para2 + 15 (positive spacing) + const expectedCellHeight = para0Height + para1Height + para2Height + 15; expect(cellMeasure.height).toBe(expectedCellHeight); }); @@ -4677,8 +4673,8 @@ describe('measureBlock', () => { const paraHeight = block0.kind === 'paragraph' ? block0.totalHeight : 0; - // Cell height should just be paragraph height + padding (no spacing.after) - const expectedCellHeight = paraHeight + 4; + // Cell height should just be paragraph height (no spacing.after) + const expectedCellHeight = paraHeight; expect(cellMeasure.height).toBe(expectedCellHeight); }); @@ -4724,7 +4720,7 @@ describe('measureBlock', () => { const paraHeight = paraMeasure.kind === 'paragraph' ? paraMeasure.totalHeight : 0; // Anchored image is out-of-flow: it should not increase cell height. - const expectedCellHeight = paraHeight + 4; // default top+bottom padding + const expectedCellHeight = paraHeight; expect(cellMeasure.height).toBe(expectedCellHeight); }); @@ -4780,8 +4776,8 @@ describe('measureBlock', () => { const para2Height = block2.kind === 'paragraph' ? block2.totalHeight : 0; // Only the valid number should add spacing - // Cell height = para0 + 10 (valid spacing) + para1 + para2 + 4 (padding) - const expectedCellHeight = para0Height + 10 + para1Height + para2Height + 4; + // Cell height = para0 + 10 (valid spacing) + para1 + para2 + const expectedCellHeight = para0Height + 10 + para1Height + para2Height; expect(cellMeasure.height).toBe(expectedCellHeight); }); @@ -4845,8 +4841,8 @@ describe('measureBlock', () => { const imageHeight = block1.kind === 'image' ? block1.height : 0; const para1Height = block2.kind === 'paragraph' ? block2.totalHeight : 0; - // Cell height = para0 + 10 + image + para1 + 5 + 4 (padding) - const expectedCellHeight = para0Height + 10 + imageHeight + para1Height + 5 + 4; + // Cell height = para0 + 10 + image + para1 + 5 + const expectedCellHeight = para0Height + 10 + imageHeight + para1Height + 5; expect(cellMeasure.height).toBe(expectedCellHeight); }); }); diff --git a/packages/layout-engine/measuring/dom/src/index.ts b/packages/layout-engine/measuring/dom/src/index.ts index 20f39c3daf..3010fb0964 100644 --- a/packages/layout-engine/measuring/dom/src/index.ts +++ b/packages/layout-engine/measuring/dom/src/index.ts @@ -2454,7 +2454,6 @@ function resolveTableWidth(attrs: TableBlock['attrs'], maxWidth: number): number async function measureTableBlock(block: TableBlock, constraints: MeasureConstraints): Promise { const maxWidth = typeof constraints === 'number' ? constraints : constraints.maxWidth; - // Resolve percentage or explicit pixel table width const resolvedTableWidth = resolveTableWidth(block.attrs, maxWidth); @@ -2645,9 +2644,9 @@ async function measureTableBlock(block: TableBlock, constraints: MeasureConstrai } // Get cell padding for height calculation - const cellPadding = cell.attrs?.padding ?? { top: 2, left: 4, right: 4, bottom: 2 }; - const paddingTop = cellPadding.top ?? 2; - const paddingBottom = cellPadding.bottom ?? 2; + const cellPadding = cell.attrs?.padding ?? { top: 0, left: 4, right: 4, bottom: 0 }; + const paddingTop = cellPadding.top ?? 0; + const paddingBottom = cellPadding.bottom ?? 0; const paddingLeft = cellPadding.left ?? 4; const paddingRight = cellPadding.right ?? 4; diff --git a/packages/layout-engine/painters/dom/src/table/renderTableCell.test.ts b/packages/layout-engine/painters/dom/src/table/renderTableCell.test.ts index 644c0f0083..0fb32548f8 100644 --- a/packages/layout-engine/painters/dom/src/table/renderTableCell.test.ts +++ b/packages/layout-engine/painters/dom/src/table/renderTableCell.test.ts @@ -102,11 +102,11 @@ describe('renderTableCell', () => { cell: baseCell, }); - // Default padding is top: 2, left: 4, right: 4, bottom: 2 - expect(cellElement.style.paddingTop).toBe('2px'); + // Default padding is top: 0, left: 4, right: 4, bottom: 0 + expect(cellElement.style.paddingTop).toBe('0px'); expect(cellElement.style.paddingLeft).toBe('4px'); expect(cellElement.style.paddingRight).toBe('4px'); - expect(cellElement.style.paddingBottom).toBe('2px'); + expect(cellElement.style.paddingBottom).toBe('0px'); }); it('content fills cell with 100% width and height', () => { diff --git a/packages/layout-engine/painters/dom/src/table/renderTableCell.ts b/packages/layout-engine/painters/dom/src/table/renderTableCell.ts index c694c4d7cf..a71fe42610 100644 --- a/packages/layout-engine/painters/dom/src/table/renderTableCell.ts +++ b/packages/layout-engine/painters/dom/src/table/renderTableCell.ts @@ -599,11 +599,11 @@ export const renderTableCell = (deps: TableCellRenderDependencies): TableCellRen } = deps; const attrs = cell?.attrs; - const padding = attrs?.padding || { top: 2, left: 4, right: 4, bottom: 2 }; + const padding = attrs?.padding || { top: 0, left: 4, right: 4, bottom: 0 }; const paddingLeft = padding.left ?? 4; - const paddingTop = padding.top ?? 2; + const paddingTop = padding.top ?? 0; const paddingRight = padding.right ?? 4; - const paddingBottom = padding.bottom ?? 2; + const paddingBottom = padding.bottom ?? 0; const cellEl = doc.createElement('div'); cellEl.style.position = 'absolute';