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
8 changes: 8 additions & 0 deletions packages/layout-engine/contracts/src/resolved-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ export type ResolvedFragmentItem = {
paragraphBorderHash?: string;
/** Pre-extracted paragraph borders for between-border rendering. */
paragraphBorders?: ParagraphBorders;
/** Pre-computed change-detection signature (blockVersion + fragment-specific data). */
version?: string;
};

/** Resolved paragraph content for non-table paragraph/list-item fragments. */
Expand Down Expand Up @@ -241,6 +243,8 @@ export type ResolvedTableItem = {
effectiveColumnWidths: number[];
/** Pre-computed SDT container key for boundary grouping (`structuredContent:<id>` or `documentSection:<id>`). */
sdtContainerKey?: string | null;
/** Pre-computed change-detection signature (blockVersion + fragment-specific data). */
version?: string;
};

/**
Expand Down Expand Up @@ -279,6 +283,8 @@ export type ResolvedImageItem = {
metadata?: ImageFragmentMetadata;
/** Pre-computed SDT container key for boundary grouping (typically null for images). */
sdtContainerKey?: string | null;
/** Pre-computed change-detection signature (blockVersion + fragment-specific data). */
version?: string;
};

/**
Expand Down Expand Up @@ -315,6 +321,8 @@ export type ResolvedDrawingItem = {
block: DrawingBlock;
/** Pre-computed SDT container key for boundary grouping (typically null for drawings). */
sdtContainerKey?: string | null;
/** Pre-computed change-detection signature (blockVersion + fragment-specific data). */
version?: string;
};

/** Type guard: checks whether a resolved paint item is a ResolvedTableItem. */
Expand Down
116 changes: 116 additions & 0 deletions packages/layout-engine/layout-resolved/src/hashUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type { BorderSpec, CellBorders, Run, TableBorders, TableBorderValue } from '@superdoc/contracts';

/**
* Hash helpers for block version computation.
*
* Duplicated from painters/dom/src/paragraph-hash-utils.ts to avoid a circular
* dependency (painter-dom -> layout-resolved is not allowed). Keep the two
* copies in sync.
*/

// ---------------------------------------------------------------------------
// Table/Cell border hashing
// ---------------------------------------------------------------------------

const isNoneBorder = (value: TableBorderValue): value is { none: true } => {
return typeof value === 'object' && value !== null && 'none' in value && (value as { none: true }).none === true;
};

const isBorderSpec = (value: unknown): value is BorderSpec => {
return typeof value === 'object' && value !== null && !('none' in value);
};

export const hashBorderSpec = (border: BorderSpec): string => {
const parts: string[] = [];
if (border.style !== undefined) parts.push(`s:${border.style}`);
if (border.width !== undefined) parts.push(`w:${border.width}`);
if (border.color !== undefined) parts.push(`c:${border.color}`);
if (border.space !== undefined) parts.push(`sp:${border.space}`);
return parts.join(',');
};

const hashTableBorderValue = (borderValue: TableBorderValue | undefined): string => {
if (borderValue === undefined) return '';
if (borderValue === null) return 'null';
if (isNoneBorder(borderValue)) return 'none';
if (isBorderSpec(borderValue)) {
return hashBorderSpec(borderValue);
}
return '';
};

export const hashTableBorders = (borders: TableBorders | undefined): string => {
if (!borders) return '';
const parts: string[] = [];
if (borders.top !== undefined) parts.push(`t:[${hashTableBorderValue(borders.top)}]`);
if (borders.right !== undefined) parts.push(`r:[${hashTableBorderValue(borders.right)}]`);
if (borders.bottom !== undefined) parts.push(`b:[${hashTableBorderValue(borders.bottom)}]`);
if (borders.left !== undefined) parts.push(`l:[${hashTableBorderValue(borders.left)}]`);
if (borders.insideH !== undefined) parts.push(`ih:[${hashTableBorderValue(borders.insideH)}]`);
if (borders.insideV !== undefined) parts.push(`iv:[${hashTableBorderValue(borders.insideV)}]`);
return parts.join(';');
};

export const hashCellBorders = (borders: CellBorders | undefined): string => {
if (!borders) return '';
const parts: string[] = [];
if (borders.top) parts.push(`t:[${hashBorderSpec(borders.top)}]`);
if (borders.right) parts.push(`r:[${hashBorderSpec(borders.right)}]`);
if (borders.bottom) parts.push(`b:[${hashBorderSpec(borders.bottom)}]`);
if (borders.left) parts.push(`l:[${hashBorderSpec(borders.left)}]`);
return parts.join(';');
};

// ---------------------------------------------------------------------------
// Run property accessors
// ---------------------------------------------------------------------------

const hasStringProp = (run: Run, prop: string): run is Run & Record<string, string> => {
return prop in run && typeof (run as Record<string, unknown>)[prop] === 'string';
};

const hasNumberProp = (run: Run, prop: string): run is Run & Record<string, number> => {
return prop in run && typeof (run as Record<string, unknown>)[prop] === 'number';
};

const hasBooleanProp = (run: Run, prop: string): run is Run & Record<string, boolean> => {
return prop in run && typeof (run as Record<string, unknown>)[prop] === 'boolean';
};

export const getRunStringProp = (run: Run, prop: string): string => {
if (hasStringProp(run, prop)) {
return run[prop];
}
return '';
};

export const getRunNumberProp = (run: Run, prop: string): number => {
if (hasNumberProp(run, prop)) {
return run[prop];
}
return 0;
};

export const getRunBooleanProp = (run: Run, prop: string): boolean => {
if (hasBooleanProp(run, prop)) {
return run[prop];
}
return false;
};

export const getRunUnderlineStyle = (run: Run): string => {
if ('underline' in run && typeof run.underline === 'boolean') {
return run.underline ? 'single' : '';
}
if ('underline' in run && run.underline && typeof run.underline === 'object') {
return (run.underline as { style?: string }).style ?? '';
}
return '';
};

export const getRunUnderlineColor = (run: Run): string => {
if ('underline' in run && run.underline && typeof run.underline === 'object') {
return (run.underline as { color?: string }).color ?? '';
}
return '';
};
Loading
Loading