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
9 changes: 8 additions & 1 deletion packages/layout-engine/contracts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@ export { rescaleColumnWidths } from './table-column-rescale.js';
export { getCellSpacingPx } from './cell-spacing.js';

// OOXML z-index normalization (moved from pm-adapter for cross-stage use)
export { normalizeZIndex, coerceRelativeHeight, isPlainObject, OOXML_Z_INDEX_BASE } from './ooxml-z-index.js';
export {
normalizeZIndex,
coerceRelativeHeight,
isPlainObject,
OOXML_Z_INDEX_BASE,
resolveFloatingZIndex,
getFragmentZIndex,
} from './ooxml-z-index.js';

// Export justify utilities
export {
Expand Down
194 changes: 194 additions & 0 deletions packages/layout-engine/contracts/src/ooxml-z-index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { describe, it, expect } from 'vitest';
import {
coerceRelativeHeight,
normalizeZIndex,
OOXML_Z_INDEX_BASE,
resolveFloatingZIndex,
getFragmentZIndex,
} from './ooxml-z-index.js';

describe('ooxml-z-index', () => {
describe('coerceRelativeHeight', () => {
it('returns number when given a finite number', () => {
expect(coerceRelativeHeight(251658240)).toBe(251658240);
expect(coerceRelativeHeight(0)).toBe(0);
});

it('returns number when given a numeric string', () => {
expect(coerceRelativeHeight('251658240')).toBe(251658240);
expect(coerceRelativeHeight('251659318')).toBe(251659318);
});

it('returns undefined for non-finite number', () => {
expect(coerceRelativeHeight(NaN)).toBeUndefined();
expect(coerceRelativeHeight(Infinity)).toBeUndefined();
});

it('returns undefined for empty or invalid string', () => {
expect(coerceRelativeHeight('')).toBeUndefined();
expect(coerceRelativeHeight(' ')).toBeUndefined();
expect(coerceRelativeHeight('abc')).toBeUndefined();
});

it('returns undefined for null, undefined, or non-number/string', () => {
expect(coerceRelativeHeight(null)).toBeUndefined();
expect(coerceRelativeHeight(undefined)).toBeUndefined();
expect(coerceRelativeHeight({})).toBeUndefined();
});
});

describe('normalizeZIndex', () => {
it('returns 0 for OOXML base relativeHeight', () => {
expect(normalizeZIndex({ relativeHeight: OOXML_Z_INDEX_BASE })).toBe(0);
expect(normalizeZIndex({ relativeHeight: '251658240' })).toBe(0);
});

it('returns positive z-index for relativeHeight above base', () => {
expect(normalizeZIndex({ relativeHeight: OOXML_Z_INDEX_BASE + 2 })).toBe(2);
expect(normalizeZIndex({ relativeHeight: OOXML_Z_INDEX_BASE + 51 })).toBe(51);
expect(normalizeZIndex({ relativeHeight: '251658291' })).toBe(51);
});

it('returns undefined when relativeHeight is missing or invalid', () => {
expect(normalizeZIndex({})).toBeUndefined();
expect(normalizeZIndex(null)).toBeUndefined();
expect(normalizeZIndex(undefined)).toBeUndefined();
expect(normalizeZIndex({ relativeHeight: '' })).toBeUndefined();
});
});

describe('resolveFloatingZIndex', () => {
it('returns 0 when behindDoc is true', () => {
expect(resolveFloatingZIndex(true, 42)).toBe(0);
expect(resolveFloatingZIndex(true, undefined)).toBe(0);
expect(resolveFloatingZIndex(true, 0)).toBe(0);
});

it('returns raw value when non-behindDoc and raw >= 1', () => {
expect(resolveFloatingZIndex(false, 5)).toBe(5);
expect(resolveFloatingZIndex(false, 100)).toBe(100);
});

it('clamps raw 0 to 1 for non-behindDoc', () => {
expect(resolveFloatingZIndex(false, 0)).toBe(1);
});

it('returns fallback when raw is undefined', () => {
expect(resolveFloatingZIndex(false, undefined)).toBe(1);
expect(resolveFloatingZIndex(false, undefined, 5)).toBe(5);
});

it('clamps fallback to at least 1', () => {
expect(resolveFloatingZIndex(false, undefined, 0)).toBe(1);
expect(resolveFloatingZIndex(false, undefined, -1)).toBe(1);
});
});

describe('getFragmentZIndex', () => {
it('uses block.zIndex when set', () => {
const block = {
kind: 'image' as const,
id: 'img-1',
src: 'x.png',
zIndex: 42,
attrs: { originalAttributes: { relativeHeight: OOXML_Z_INDEX_BASE } },
};
expect(getFragmentZIndex(block)).toBe(42);
});

it('derives z-index from attrs.originalAttributes.relativeHeight (number)', () => {
const block = {
kind: 'image' as const,
id: 'img-1',
src: 'x.png',
attrs: { originalAttributes: { relativeHeight: OOXML_Z_INDEX_BASE + 10 } },
};
expect(getFragmentZIndex(block)).toBe(10);
});

it('derives z-index from attrs.originalAttributes.relativeHeight (string)', () => {
const block = {
kind: 'image' as const,
id: 'img-1',
src: 'x.png',
attrs: { originalAttributes: { relativeHeight: '251658250' } },
};
expect(getFragmentZIndex(block)).toBe(10);
});

it('preserves high z-index for wrapped anchored objects', () => {
const block = {
kind: 'image' as const,
id: 'img-1',
src: 'x.png',
anchor: { isAnchored: true, behindDoc: false },
wrap: { type: 'Through' as const },
zIndex: 7168,
};
expect(getFragmentZIndex(block)).toBe(7168);
});

it('preserves relativeHeight z-index for wrap None anchored objects', () => {
const block = {
kind: 'image' as const,
id: 'img-1',
src: 'x.png',
anchor: { isAnchored: true, behindDoc: false },
wrap: { type: 'None' as const },
attrs: { originalAttributes: { relativeHeight: OOXML_Z_INDEX_BASE + 10 } },
};
expect(getFragmentZIndex(block)).toBe(10);
});

it('returns 0 when anchor.behindDoc is true and no zIndex/originalAttributes', () => {
const block = {
kind: 'image' as const,
id: 'img-1',
src: 'x.png',
anchor: { isAnchored: true, behindDoc: true },
};
expect(getFragmentZIndex(block)).toBe(0);
});

it('returns 1 when not behindDoc and no zIndex/originalAttributes', () => {
const block = {
kind: 'image' as const,
id: 'img-1',
src: 'x.png',
};
expect(getFragmentZIndex(block)).toBe(1);
});

it('does not treat base relativeHeight as behindDoc when behindDoc is false', () => {
const block = {
kind: 'image' as const,
id: 'img-1',
src: 'x.png',
anchor: { isAnchored: true, behindDoc: false },
attrs: { originalAttributes: { relativeHeight: OOXML_Z_INDEX_BASE } },
};
expect(getFragmentZIndex(block)).toBeGreaterThan(0);
});

it('forces behindDoc fragments to zIndex 0 even with relativeHeight', () => {
const block = {
kind: 'image' as const,
id: 'img-1',
src: 'x.png',
anchor: { isAnchored: true, behindDoc: true },
attrs: { originalAttributes: { relativeHeight: OOXML_Z_INDEX_BASE + 5 } },
};
expect(getFragmentZIndex(block)).toBe(0);
});

it('works for drawing blocks', () => {
const block = {
kind: 'drawing' as const,
id: 'd-1',
drawingKind: 'vectorShape' as const,
attrs: { originalAttributes: { relativeHeight: OOXML_Z_INDEX_BASE + 5 } },
};
expect(getFragmentZIndex(block)).toBe(5);
});
});
});
21 changes: 21 additions & 0 deletions packages/layout-engine/contracts/src/ooxml-z-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* These helpers convert to small positive CSS z-index values.
*/

import type { ImageBlock, DrawingBlock } from './index.js';

/** Checks whether `value` is a non-null, non-array object. */
export const isPlainObject = (value: unknown): value is Record<string, unknown> =>
value !== null && typeof value === 'object' && !Array.isArray(value);
Expand Down Expand Up @@ -57,3 +59,22 @@ export function normalizeZIndex(originalAttributes: unknown): number | undefined
if (relativeHeight === undefined) return undefined;
return Math.max(0, relativeHeight - OOXML_Z_INDEX_BASE);
}

/**
* Resolves floating z-index based on behindDoc flag and raw OOXML value.
*/
export function resolveFloatingZIndex(behindDoc: boolean, raw: number | undefined, fallback = 1): number {
if (behindDoc) return 0;
if (raw === undefined) return Math.max(1, fallback);
return Math.max(1, raw);
}

/**
* Returns z-index for an image or drawing block.
* Uses block.zIndex when present, otherwise derives from originalAttributes.
*/
export function getFragmentZIndex(block: ImageBlock | DrawingBlock): number {
const attrs = block.attrs as { originalAttributes?: unknown } | undefined;
const raw = typeof block.zIndex === 'number' ? block.zIndex : normalizeZIndex(attrs?.originalAttributes);
return resolveFloatingZIndex(block.anchor?.behindDoc === true, raw);
}
3 changes: 1 addition & 2 deletions packages/layout-engine/layout-engine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
},
"dependencies": {
"@superdoc/common": "workspace:*",
"@superdoc/contracts": "workspace:*",
"@superdoc/pm-adapter": "workspace:*"
"@superdoc/contracts": "workspace:*"
}
}
16 changes: 4 additions & 12 deletions packages/layout-engine/layout-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import type {
FlowMode,
NormalizedColumnLayout,
} from '@superdoc/contracts';
import { normalizeColumnLayout } from '@superdoc/contracts';
import { normalizeColumnLayout, getFragmentZIndex } from '@superdoc/contracts';
import { createFloatingObjectManager, computeAnchorX } from './floating-objects.js';
import { computeNextSectionPropsAtBreak } from './section-props';
import {
Expand All @@ -51,7 +51,6 @@ import { createPaginator, type PageState, type ConstraintBoundary } from './pagi
import { formatPageNumber } from './pageNumbering.js';
import { shouldSuppressSpacingForEmpty, shouldSuppressOwnSpacing } from './layout-utils.js';
import { balancePageColumns } from './column-balancing.js';
import { getFragmentZIndex } from '@superdoc/pm-adapter/utilities.js';
import { cloneColumnLayout, widthsEqual } from './column-utils.js';

type PageSize = { w: number; h: number };
Expand Down Expand Up @@ -2789,13 +2788,6 @@ export type { PageNumberFormat, DisplayPageInfo } from './pageNumbering.js';
export { resolvePageNumberTokens } from './resolvePageTokens.js';
export type { NumberingContext, ResolvePageTokensResult } from './resolvePageTokens.js';

// Export table utilities for reuse by painter-dom
export { rescaleColumnWidths, getCellLines, getEmbeddedRowLines } from './layout-table.js';
export {
describeCellRenderBlocks,
computeCellSliceContentHeight,
computeFullCellContentHeight,
createCellSliceCursor,
type CellRenderBlock,
type CellSliceCursor,
} from './table-cell-slice.js';
// Table utilities consumed by layout-bridge and cross-package sync tests
export { getCellLines, getEmbeddedRowLines } from './layout-table.js';
export { describeCellRenderBlocks, computeCellSliceContentHeight } from './table-cell-slice.js';
2 changes: 1 addition & 1 deletion packages/layout-engine/layout-engine/src/layout-drawing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { DrawingBlock, DrawingMeasure, DrawingFragment } from '@superdoc/co
import type { NormalizedColumns } from './layout-image.js';
import type { PageState } from './paginator.js';
import { extractBlockPmRange } from './layout-utils.js';
import { getFragmentZIndex } from '@superdoc/pm-adapter/utilities.js';
import { getFragmentZIndex } from '@superdoc/contracts';

/**
* Context for laying out a drawing block (vector shape) within the page layout.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
shouldSuppressOwnSpacing,
} from './layout-utils.js';
import { computeAnchorX } from './floating-objects.js';
import { getFragmentZIndex } from '@superdoc/pm-adapter/utilities.js';
import { getFragmentZIndex } from '@superdoc/contracts';

const spacingDebugEnabled = false;
/**
Expand Down
3 changes: 1 addition & 2 deletions packages/layout-engine/layout-engine/src/layout-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,7 @@ function resolveTableFrame(
*
* @returns Rescaled column widths if clamping occurred, undefined otherwise.
*/
// Canonical implementation moved to @superdoc/contracts; re-imported for local use and re-exported.
export { rescaleColumnWidths } from '@superdoc/contracts';
// Canonical implementation lives in @superdoc/contracts; imported for local use.
import { rescaleColumnWidths } from '@superdoc/contracts';

const COLUMN_MIN_WIDTH_PX = 25;
Expand Down
54 changes: 8 additions & 46 deletions packages/layout-engine/pm-adapter/src/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1443,49 +1443,11 @@ export function normalizeTextInsets(
return { top, right, bottom, left };
}

// Canonical implementations moved to @superdoc/contracts; re-imported for local use and re-exported.
export { OOXML_Z_INDEX_BASE, coerceRelativeHeight, normalizeZIndex } from '@superdoc/contracts';
import { normalizeZIndex } from '@superdoc/contracts';

/**
* Resolves the CSS z-index for a floating object based on its behindDoc flag
* and an OOXML-derived raw value.
*
* - behindDoc objects always return 0.
* - Non-behindDoc objects are clamped to at least 1 so they never share the
* behindDoc sentinel value (0).
*
* @param behindDoc - Whether the object is behind body text
* @param raw - OOXML-derived z-index (from normalizeZIndex or block.zIndex)
* @param fallback - Value to use when raw is undefined (default: 1)
* @returns Resolved z-index
*/
export function resolveFloatingZIndex(behindDoc: boolean, raw: number | undefined, fallback = 1): number {
if (behindDoc) return 0;
if (raw === undefined) return Math.max(1, fallback);
return Math.max(1, raw);
}

/**
* Returns z-index for an image or drawing block.
*
* We cannot rely on `block.zIndex` only: when the flow-block cache hits, the
* paragraph handler reuses cached blocks and never calls the image/shape
* converters, so those blocks never get `zIndex` set. This helper uses
* `block.zIndex` when present, otherwise derives from
* `block.attrs.originalAttributes.relativeHeight` via normalizeZIndex,
* otherwise behindDoc ? 0 : 1.
*
* Rendering policy:
* - behindDoc anchored objects always return 0.
* - Anchored objects with text wrapping (Square/Tight/Through/TopAndBottom, or
* missing wrap metadata) keep OOXML relativeHeight ordering but are clamped
* to at least 1 (never 0 unless behindDoc=true).
* - Front/no-wrap anchored objects (wrap None) also preserve OOXML relativeHeight order.
*/
export function getFragmentZIndex(block: ImageBlock | DrawingBlock): number {
const attrs = block.attrs as { originalAttributes?: unknown } | undefined;
const raw = typeof block.zIndex === 'number' ? block.zIndex : normalizeZIndex(attrs?.originalAttributes);

return resolveFloatingZIndex(block.anchor?.behindDoc === true, raw);
}
// Canonical implementations moved to @superdoc/contracts; re-exported for backward compatibility.
export {
OOXML_Z_INDEX_BASE,
coerceRelativeHeight,
normalizeZIndex,
resolveFloatingZIndex,
getFragmentZIndex,
} from '@superdoc/contracts';
3 changes: 0 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading