From 525f52e5848d90a80d6a49c2004b659852b92430 Mon Sep 17 00:00:00 2001 From: VladaHarbour Date: Mon, 26 Jan 2026 20:00:42 +0200 Subject: [PATCH 1/2] fix: before paragraph spacing inside table cells --- .../layout-engine/layout-bridge/src/index.ts | 5 + .../layout-engine/measuring/dom/src/index.ts | 6 +- .../dom/src/table/renderTableCell.test.ts | 141 ++++++++++++++++++ .../painters/dom/src/table/renderTableCell.ts | 9 +- 4 files changed, 159 insertions(+), 2 deletions(-) diff --git a/packages/layout-engine/layout-bridge/src/index.ts b/packages/layout-engine/layout-bridge/src/index.ts index 2282cc2cc..379bfee9a 100644 --- a/packages/layout-engine/layout-bridge/src/index.ts +++ b/packages/layout-engine/layout-bridge/src/index.ts @@ -1594,6 +1594,11 @@ export function selectionToRects( if (typeof totalHeight === 'number' && totalHeight > height) { height = totalHeight; } + const spacingBefore = (paraBlock.attrs as { spacing?: { before?: number } } | undefined)?.spacing + ?.before; + if (typeof spacingBefore === 'number' && spacingBefore > 0) { + height += spacingBefore; + } const spacingAfter = (paraBlock.attrs as { spacing?: { after?: number } } | undefined)?.spacing?.after; if (typeof spacingAfter === 'number' && spacingAfter > 0) { height += spacingAfter; diff --git a/packages/layout-engine/measuring/dom/src/index.ts b/packages/layout-engine/measuring/dom/src/index.ts index 83d131bac..d2628d5c6 100644 --- a/packages/layout-engine/measuring/dom/src/index.ts +++ b/packages/layout-engine/measuring/dom/src/index.ts @@ -2708,9 +2708,13 @@ async function measureTableBlock(block: TableBlock, constraints: MeasureConstrai contentHeight += blockHeight; - // Add paragraph spacing.after to content height for all paragraphs. + // Add paragraph spacing.after/spacing.before to content height for all paragraphs. // Word applies spacing.after even to the last paragraph in a cell, creating space at the bottom. if (block.kind === 'paragraph') { + const spacingBefore = (block as ParagraphBlock).attrs?.spacing?.before; + if (typeof spacingBefore === 'number' && spacingBefore > 0) { + contentHeight += spacingBefore; + } const spacingAfter = (block as ParagraphBlock).attrs?.spacing?.after; if (typeof spacingAfter === 'number' && spacingAfter > 0) { contentHeight += spacingAfter; 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 644c0f008..b45c8083b 100644 --- a/packages/layout-engine/painters/dom/src/table/renderTableCell.test.ts +++ b/packages/layout-engine/painters/dom/src/table/renderTableCell.test.ts @@ -924,6 +924,147 @@ describe('renderTableCell', () => { }); }); + describe('spacing.before margin-top rendering', () => { + const baseMeasure: ParagraphMeasure = { + kind: 'paragraph', + lines: [ + { + fromRun: 0, + fromChar: 0, + toRun: 0, + toChar: 10, + width: 100, + ascent: 12, + descent: 4, + lineHeight: 20, + }, + ], + totalHeight: 20, + }; + + it('applies margin-top only for positive spacing.before', () => { + const para1: ParagraphBlock = { + kind: 'paragraph', + id: 'para-before-zero', + runs: [{ text: 'Zero spacing', fontFamily: 'Arial', fontSize: 16 }], + attrs: { spacing: { before: 0 } }, + }; + + const para2: ParagraphBlock = { + kind: 'paragraph', + id: 'para-before-negative', + runs: [{ text: 'Negative spacing', fontFamily: 'Arial', fontSize: 16 }], + attrs: { spacing: { before: -6 } }, + }; + + const para3: ParagraphBlock = { + kind: 'paragraph', + id: 'para-before-positive', + runs: [{ text: 'Positive spacing', fontFamily: 'Arial', fontSize: 16 }], + attrs: { spacing: { before: 9 } }, + }; + + const cellMeasure: TableCellMeasure = { + blocks: [baseMeasure, baseMeasure, baseMeasure], + width: 120, + height: 80, + gridColumnStart: 0, + colSpan: 1, + rowSpan: 1, + }; + + const cell: TableCell = { + id: 'cell-spacing-before-conditional', + blocks: [para1, para2, para3], + attrs: {}, + }; + + const { cellElement } = renderTableCell({ + ...createBaseDeps(), + cellMeasure, + cell, + }); + + const contentElement = cellElement.firstElementChild as HTMLElement; + const paraWrappers = contentElement.children; + + expect((paraWrappers[0] as HTMLElement).style.marginTop).toBe(''); + expect((paraWrappers[1] as HTMLElement).style.marginTop).toBe(''); + expect((paraWrappers[2] as HTMLElement).style.marginTop).toBe('9px'); + }); + + it('skips spacing.before for partial renders', () => { + const para: ParagraphBlock = { + kind: 'paragraph', + id: 'para-before-partial', + runs: [{ text: 'Partial render test', fontFamily: 'Arial', fontSize: 16 }], + attrs: { spacing: { before: 11 } }, + }; + + const measure: ParagraphMeasure = { + kind: 'paragraph', + lines: [ + { + fromRun: 0, + fromChar: 0, + toRun: 0, + toChar: 10, + width: 100, + ascent: 12, + descent: 4, + lineHeight: 20, + }, + { + fromRun: 0, + fromChar: 10, + toRun: 0, + toChar: 19, + width: 100, + ascent: 12, + descent: 4, + lineHeight: 20, + }, + ], + totalHeight: 40, + }; + + const cellMeasure: TableCellMeasure = { + blocks: [measure], + width: 120, + height: 60, + gridColumnStart: 0, + colSpan: 1, + rowSpan: 1, + }; + + const cell: TableCell = { + id: 'cell-before-partial', + blocks: [para], + attrs: {}, + }; + + const { cellElement: partialCell } = renderTableCell({ + ...createBaseDeps(), + cellMeasure, + cell, + fromLine: 1, + toLine: 2, + }); + + const partialWrapper = (partialCell.firstElementChild as HTMLElement).firstElementChild as HTMLElement; + expect(partialWrapper.style.marginTop).toBe(''); + + const { cellElement: fullCell } = renderTableCell({ + ...createBaseDeps(), + cellMeasure, + cell, + }); + + const fullWrapper = (fullCell.firstElementChild as HTMLElement).firstElementChild as HTMLElement; + expect(fullWrapper.style.marginTop).toBe('11px'); + }); + }); + describe('list marker rendering', () => { const createParagraphWithMarker = (markerText: string, markerWidth = 20, gutterWidth = 8, indentLeft = 30) => { const para: ParagraphBlock = { diff --git a/packages/layout-engine/painters/dom/src/table/renderTableCell.ts b/packages/layout-engine/painters/dom/src/table/renderTableCell.ts index 96ad375ba..979b8c196 100644 --- a/packages/layout-engine/painters/dom/src/table/renderTableCell.ts +++ b/packages/layout-engine/painters/dom/src/table/renderTableCell.ts @@ -862,7 +862,6 @@ export const renderTableCell = (deps: TableCellRenderDependencies): TableCellRen const lines = paragraphMeasure.lines; const blockLineCount = lines?.length || 0; - paragraphTopById.set(block.id, flowCursorY); /** * Extract Word layout information from paragraph attributes. * This contains computed marker positioning and indent details from the word-layout engine. @@ -925,6 +924,14 @@ export const renderTableCell = (deps: TableCellRenderDependencies): TableCellRen applyParagraphBorderStyles(paraWrapper, block.attrs?.borders); applyParagraphShadingStyles(paraWrapper, block.attrs?.shading); + // Apply paragraph spacing.before when rendering from the top of the paragraph. + const spacingBefore = (block as ParagraphBlock).attrs?.spacing?.before; + if (localStartLine === 0 && typeof spacingBefore === 'number' && spacingBefore > 0) { + paraWrapper.style.marginTop = `${spacingBefore}px`; + flowCursorY += spacingBefore; + } + paragraphTopById.set(block.id, flowCursorY); + // Calculate height of rendered content for proper block accumulation let renderedHeight = 0; From 3d584e61bdd2cf894640d1d5fa1a96b4c206807b Mon Sep 17 00:00:00 2001 From: VladaHarbour Date: Mon, 26 Jan 2026 20:54:25 +0200 Subject: [PATCH 2/2] fix: remove extra code in layout bridge --- packages/layout-engine/layout-bridge/src/index.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/layout-engine/layout-bridge/src/index.ts b/packages/layout-engine/layout-bridge/src/index.ts index 379bfee9a..2282cc2cc 100644 --- a/packages/layout-engine/layout-bridge/src/index.ts +++ b/packages/layout-engine/layout-bridge/src/index.ts @@ -1594,11 +1594,6 @@ export function selectionToRects( if (typeof totalHeight === 'number' && totalHeight > height) { height = totalHeight; } - const spacingBefore = (paraBlock.attrs as { spacing?: { before?: number } } | undefined)?.spacing - ?.before; - if (typeof spacingBefore === 'number' && spacingBefore > 0) { - height += spacingBefore; - } const spacingAfter = (paraBlock.attrs as { spacing?: { after?: number } } | undefined)?.spacing?.after; if (typeof spacingAfter === 'number' && spacingAfter > 0) { height += spacingAfter;