Skip to content
Merged
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
2 changes: 2 additions & 0 deletions packages/layout-engine/contracts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export type { TabStop };
// Export table contracts
export { OOXML_PCT_DIVISOR, type TableWidthAttr, type TableColumnSpec } from './engines/tables.js';

export { effectiveTableCellSpacing } from './table-cell-spacing.js';

// Export justify utilities
export {
shouldApplyJustify,
Expand Down
26 changes: 26 additions & 0 deletions packages/layout-engine/contracts/src/table-cell-spacing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest';
import { effectiveTableCellSpacing } from './table-cell-spacing.js';

describe('effectiveTableCellSpacing', () => {
it('returns 0 when spacing is undefined', () => {
expect(effectiveTableCellSpacing(undefined, false, 0)).toBe(0);
expect(effectiveTableCellSpacing(undefined, true, 10)).toBe(0);
});

it('returns 0 when spacing is <= 0', () => {
expect(effectiveTableCellSpacing(0, false, 0)).toBe(0);
expect(effectiveTableCellSpacing(-5, true, 0)).toBe(0);
});

it('returns full spacing when not at boundary', () => {
expect(effectiveTableCellSpacing(20, false, 10)).toBe(20);
expect(effectiveTableCellSpacing(20, false, 0)).toBe(20);
});

it('returns excess over padding when at boundary', () => {
expect(effectiveTableCellSpacing(20, true, 10)).toBe(10);
expect(effectiveTableCellSpacing(20, true, 0)).toBe(20);
expect(effectiveTableCellSpacing(10, true, 10)).toBe(0);
expect(effectiveTableCellSpacing(5, true, 10)).toBe(0);
});
});
17 changes: 17 additions & 0 deletions packages/layout-engine/contracts/src/table-cell-spacing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Effective paragraph spacing in table cells.
*
* Word absorbs the first paragraph's spacing.before into the cell's top padding,
* and the last paragraph's spacing.after into the cell's bottom padding.
* This helper returns the amount to add to height/position: at a boundary,
* only the excess of spacing over padding; otherwise the full spacing.
*
* Use for both spacing.before (isBoundary = first block, padding = paddingTop)
* and spacing.after (isBoundary = last block, padding = paddingBottom).
*/
export function effectiveTableCellSpacing(spacing: number | undefined, isBoundary: boolean, padding: number): number {
if (typeof spacing !== 'number' || spacing <= 0) {
return 0;
}
return isBoundary ? Math.max(0, spacing - padding) : spacing;
}
22 changes: 15 additions & 7 deletions packages/layout-engine/layout-bridge/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {
ParagraphBlock,
ParagraphMeasure,
} from '@superdoc/contracts';
import { computeLinePmRange as computeLinePmRangeUnified } from '@superdoc/contracts';
import { computeLinePmRange as computeLinePmRangeUnified, effectiveTableCellSpacing } from '@superdoc/contracts';
import { charOffsetToPm, findCharacterAtX, measureCharacterX } from './text-measurement.js';
import { clickToPositionDom, findPageElement } from './dom-mapping.js';
import {
Expand Down Expand Up @@ -1695,10 +1695,12 @@ export function selectionToRects(
if (typeof totalHeight === 'number' && totalHeight > height) {
height = totalHeight;
}
const spacingAfter = (paraBlock.attrs as { spacing?: { after?: number } } | undefined)?.spacing?.after;
if (typeof spacingAfter === 'number' && spacingAfter > 0) {
height += spacingAfter;
}
const isFirstBlock = i === 0;
const isLastBlock = i === cellBlocks.length - 1;
const spacingBefore = (paraBlock as ParagraphBlock).attrs?.spacing?.before;
height += effectiveTableCellSpacing(spacingBefore, isFirstBlock, padding.top);
const spacingAfter = (paraBlock as ParagraphBlock).attrs?.spacing?.after;
height += effectiveTableCellSpacing(spacingAfter, isLastBlock, padding.bottom);
}

renderedBlocks.push({ block: paraBlock, measure: paraMeasure, startLine, endLine, height });
Expand All @@ -1718,7 +1720,7 @@ export function selectionToRects(

let blockTopCursor = padding.top + verticalOffset;

renderedBlocks.forEach((info) => {
renderedBlocks.forEach((info, blockIndex) => {
const paragraphMarkerWidth = info.measure.marker?.markerWidth ?? 0;
// List items in table cells are also rendered with left alignment
const cellIsListItem = isListItem(paragraphMarkerWidth, info.block);
Expand All @@ -1731,6 +1733,11 @@ export function selectionToRects(

const intersectingLines = findLinesIntersectingRange(info.block, info.measure, from, to);

// Match renderer: spacing.before is only applied when rendering from the start of the block (startLine === 0).
const rawSpacingBefore = (info.block as ParagraphBlock).attrs?.spacing?.before;
const effectiveSpacingBeforePx =
info.startLine === 0 ? effectiveTableCellSpacing(rawSpacingBefore, blockIndex === 0, padding.top) : 0;

intersectingLines.forEach(({ line, index }) => {
if (index < info.startLine || index >= info.endLine) {
return;
Expand Down Expand Up @@ -1768,7 +1775,8 @@ export function selectionToRects(
);
const lineOffset =
lineHeightBeforeIndex(info.measure, index) - lineHeightBeforeIndex(info.measure, info.startLine);
const rectY = fragment.y + contentOffsetY + rowOffset + blockTopCursor + lineOffset;
const rectY =
fragment.y + contentOffsetY + rowOffset + blockTopCursor + effectiveSpacingBeforePx + lineOffset;

rects.push({
x: rectX,
Expand Down
Loading
Loading