Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 24 additions & 24 deletions packages/layout-engine/layout-bridge/src/incrementalLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,7 @@
// Dirty region computation
const dirtyStart = performance.now();
const dirty = computeDirtyRegions(previousBlocks, nextBlocks);
const dirtyTime = performance.now() - dirtyStart;

Check warning on line 742 in packages/layout-engine/layout-bridge/src/incrementalLayout.ts

View workflow job for this annotation

GitHub Actions / validate

'dirtyTime' is assigned a value but never used. Allowed unused vars must match /^_/u

if (dirty.deletedBlockIds.length > 0) {
measureCache.invalidate(dirty.deletedBlockIds);
Expand Down Expand Up @@ -1668,32 +1668,32 @@
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this to module level -- other layout limits in the pipeline are defined at the top of the file. also, if this loop maxes out without stabilizing, a console.warn would help debugging.


// 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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same layoutDocument(...) call with same options appears 3x here. a small relayout(reserves) helper would clean this up.

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 =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no test hits the multi-pass path. one where footnotes shift pages between passes would catch regressions.

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;
Comment on lines +1693 to +1694

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Re-layout after updating reserves on the last pass

When the for loop exits because MAX_FOOTNOTE_LAYOUT_PASSES is reached (without hitting reservesStable), reserves is updated to nextReserves but layout still reflects the previous reserve vector. The subsequent plan/injection phase then runs against a layout that may not match reserves, and because reservesDiffer compares against reserves (not the reserve vector actually used to produce layout), this mismatch can slip through and place footnotes in the wrong reserved band. In long/oscillating footnote pagination cases this can reintroduce overlap/truncation near the footer.

Useful? React with 👍 / 👎.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@VladaHarbour worth double checking and adding a comment if relevant or not

}

// 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),
Expand Down
6 changes: 3 additions & 3 deletions packages/layout-engine/layout-bridge/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 ?? {};
Expand Down
4 changes: 2 additions & 2 deletions packages/layout-engine/layout-engine/src/layout-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
32 changes: 14 additions & 18 deletions packages/layout-engine/measuring/dom/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+ 0 on the next line is leftover from replacing + 4 -- just drop it. same for this comment: if padding is 0, drop the mention.

const expectedCellHeight = para0Height + 10 + para1Height + 20 + 0;
expect(cellMeasure.height).toBe(expectedCellHeight);
});

Expand Down Expand Up @@ -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);
});

Expand Down Expand Up @@ -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);
});

Expand Down Expand Up @@ -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);
});

Expand Down Expand Up @@ -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);
});

Expand Down Expand Up @@ -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);
});
});
Expand Down
7 changes: 3 additions & 4 deletions packages/layout-engine/measuring/dom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2454,7 +2454,6 @@ function resolveTableWidth(attrs: TableBlock['attrs'], maxWidth: number): number

async function measureTableBlock(block: TableBlock, constraints: MeasureConstraints): Promise<TableMeasure> {
const maxWidth = typeof constraints === 'number' ? constraints : constraints.maxWidth;

// Resolve percentage or explicit pixel table width
const resolvedTableWidth = resolveTableWidth(block.attrs, maxWidth);

Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading