Skip to content
Closed
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: 1 addition & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@superdoc-dev/cli",
"version": "0.6.0",
"version": "0.7.0",
"type": "module",
"bin": {
"superdoc": "./dist/index.js"
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/platforms/cli-darwin-arm64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@superdoc-dev/cli-darwin-arm64",
"version": "0.6.0",
"version": "0.7.0",
"os": [
"darwin"
],
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/platforms/cli-darwin-x64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@superdoc-dev/cli-darwin-x64",
"version": "0.6.0",
"version": "0.7.0",
"os": [
"darwin"
],
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/platforms/cli-linux-arm64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@superdoc-dev/cli-linux-arm64",
"version": "0.6.0",
"version": "0.7.0",
"os": [
"linux"
],
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/platforms/cli-linux-x64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@superdoc-dev/cli-linux-x64",
"version": "0.6.0",
"version": "0.7.0",
"os": [
"linux"
],
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/platforms/cli-windows-x64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@superdoc-dev/cli-windows-x64",
"version": "0.6.0",
"version": "0.7.0",
"os": [
"win32"
],
Expand Down
25 changes: 25 additions & 0 deletions packages/layout-engine/layout-engine/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,31 @@ describe('layoutDocument', () => {
expect(layout.columns).toMatchObject({ count: 2, gap: 20, withSeparator: true });
});

it('preserves explicit column widths on page-level column metadata', () => {
const options: LayoutOptions = {
pageSize: { w: 600, h: 800 },
margins: { top: 40, right: 40, bottom: 40, left: 40 },
columns: { count: 2, gap: 20, widths: [100, 400], equalWidth: false, withSeparator: true },
};
const layout = layoutDocument([block], [makeMeasure([350, 350, 350])], options);

expect(layout.pages).toHaveLength(1);
expect(layout.pages[0].columns).toEqual({
count: 2,
gap: 20,
widths: [100, 400],
equalWidth: false,
withSeparator: true,
});
expect(layout.columns).toEqual({
count: 2,
gap: 20,
widths: [100, 400],
equalWidth: false,
withSeparator: true,
});
});

it('does not set "page.columns" on single column layout', () => {
const options: LayoutOptions = {
pageSize: { w: 600, h: 800 },
Expand Down
7 changes: 2 additions & 5 deletions packages/layout-engine/layout-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1099,7 +1099,7 @@ export function layoutDocument(blocks: FlowBlock[], measures: Measure[], options
}

if (activeColumns.count > 1) {
page.columns = { count: activeColumns.count, gap: activeColumns.gap, withSeparator: activeColumns.withSeparator };
page.columns = cloneColumnLayout(activeColumns);
}

// Set vertical alignment from active section state
Expand Down Expand Up @@ -2604,10 +2604,7 @@ export function layoutDocument(blocks: FlowBlock[], measures: Measure[], options
// after processing sections. Page/region-specific column changes are encoded
// implicitly via fragment positions. Consumers should not assume this is
// a static document-wide value.
columns:
activeColumns.count > 1
? { count: activeColumns.count, gap: activeColumns.gap, withSeparator: activeColumns.withSeparator }
: undefined,
columns: activeColumns.count > 1 ? cloneColumnLayout(activeColumns) : undefined,
};
}

Expand Down
17 changes: 17 additions & 0 deletions packages/layout-engine/layout-engine/src/section-props.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,21 @@ describe('computeNextSectionPropsAtBreak', () => {
expect(map.get(0)?.columns).toEqual({ count: 2, gap: 48, withSeparator: true });
expect(map.get(2)?.columns).toEqual({ count: 2, gap: 48, withSeparator: true });
});

it('preserves explicit column widths and equalWidth in snapshots', () => {
const sourceColumns = { count: 2, gap: 48, widths: [120, 360], equalWidth: false, withSeparator: true };
const blocks: FlowBlock[] = [sectionBreak({ id: 'sb-0', columns: sourceColumns })];
const map = computeNextSectionPropsAtBreak(blocks);
const snapshot = map.get(0);

expect(snapshot?.columns).toEqual({
count: 2,
gap: 48,
widths: [120, 360],
equalWidth: false,
withSeparator: true,
});
expect(snapshot?.columns).not.toBe(sourceColumns);
expect(snapshot?.columns?.widths).not.toBe(sourceColumns.widths);
});
});
14 changes: 8 additions & 6 deletions packages/layout-engine/layout-engine/src/section-props.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ColumnLayout, FlowBlock, SectionVerticalAlign } from '@superdoc/contracts';
import { cloneColumnLayout } from './column-utils.js';

/**
* Section-level formatting properties that control page layout.
Expand All @@ -21,6 +22,11 @@ export type SectionProps = {
vAlign?: SectionVerticalAlign;
};

const snapshotColumns = (columns?: ColumnLayout): ColumnLayout | undefined => {
if (!columns) return undefined;
return cloneColumnLayout(columns);
};

/**
* Extracts section properties from a section break block if any are present.
* Returns null if the block has no section-related properties.
Expand Down Expand Up @@ -59,7 +65,7 @@ const _snapshotSectionProps = (block: FlowBlock): SectionProps | null => {
}
if (block.columns) {
hasProps = true;
props.columns = { count: block.columns.count, gap: block.columns.gap, withSeparator: block.columns.withSeparator };
props.columns = snapshotColumns(block.columns);
}
if (block.orientation) {
hasProps = true;
Expand Down Expand Up @@ -135,11 +141,7 @@ export function computeNextSectionPropsAtBreak(blocks: FlowBlock[]): Map<number,
props.pageSize = { w: source.pageSize.w, h: source.pageSize.h };
}
if (source.columns) {
props.columns = {
count: source.columns.count,
gap: source.columns.gap,
withSeparator: source.columns.withSeparator,
};
props.columns = snapshotColumns(source.columns);
}
if (source.orientation) {
props.orientation = source.orientation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ describe('DomPainter renderColumnSeparators', () => {
expect(seps.map((s) => s.style.left)).toEqual(['296px', '520px']);
});

it('uses explicit column widths when drawing separators for page.columns', () => {
const page = buildPage({
columns: { count: 2, gap: 48, widths: [200, 952], equalWidth: false, withSeparator: true },
});
paintOnce(buildLayout(page), mount);

const seps = querySeparators(mount);
expect(seps).toHaveLength(1);
// contentWidth=624, availableWidth=576. Explicit widths [200, 952] are
// normalized to [100, 476], so the separator belongs at 96 + 100 + 24 = 220.
expect(seps[0].style.left).toBe('220px');
});

it('renders nothing when withSeparator is false', () => {
const page = buildPage({ columns: { count: 2, gap: 48, withSeparator: false } });
paintOnce(buildLayout(page), mount);
Expand Down Expand Up @@ -211,5 +224,24 @@ describe('DomPainter renderColumnSeparators', () => {
expect(seps[0].style.top).toBe('96px');
expect(seps[0].style.height).toBe('864px');
});

it('uses explicit column widths when drawing separators for columnRegions', () => {
const page = buildPage({
columnRegions: [
{
yStart: 96,
yEnd: 500,
columns: { count: 2, gap: 48, widths: [200, 952], equalWidth: false, withSeparator: true },
},
],
});
paintOnce(buildLayout(page), mount);

const seps = querySeparators(mount);
expect(seps).toHaveLength(1);
expect(seps[0].style.top).toBe('96px');
expect(seps[0].style.height).toBe('404px');
expect(seps[0].style.left).toBe('220px');
});
});
});
46 changes: 40 additions & 6 deletions packages/layout-engine/painters/dom/src/renderer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {
ChartDrawing,
ColumnLayout,
CustomGeometryData,
DrawingBlock,
DrawingFragment,
Expand Down Expand Up @@ -61,6 +62,7 @@ import {
calculateJustifySpacing,
computeLinePmRange,
getCellSpacingPx,
normalizeColumnLayout,
normalizeBaselineShift,
resolveBaseFontSizeForVerticalText,
shouldApplyJustify,
Expand Down Expand Up @@ -2346,15 +2348,13 @@ export class DomPainter {
if (!columns.withSeparator) continue;
if (columns.count <= 1) continue;

const columnWidth = (contentWidth - columns.gap * (columns.count - 1)) / columns.count;
// Given the separator will have 1px width, ensure column has a larger width.
if (columnWidth <= 1) continue;

const regionHeight = yEnd - yStart;
if (regionHeight <= 0) continue;

for (let i = 0; i < columns.count - 1; i++) {
const separatorX = leftMargin + (i + 1) * columnWidth + i * columns.gap + columns.gap / 2;
const separatorPositions = this.getColumnSeparatorPositions(columns, leftMargin, contentWidth);
if (separatorPositions.length === 0) continue;

for (const separatorX of separatorPositions) {
const separatorEl = this.doc.createElement('div');

separatorEl.style.position = 'absolute';
Expand All @@ -2369,6 +2369,40 @@ export class DomPainter {
}
}

private getColumnSeparatorPositions(columns: ColumnLayout, leftMargin: number, contentWidth: number): number[] {
const hasExplicitWidths = Array.isArray(columns.widths) && columns.widths.length > 0;

if (!hasExplicitWidths) {
const equalWidth = (contentWidth - columns.gap * (columns.count - 1)) / columns.count;
if (equalWidth <= 1) return [];

const separatorPositions: number[] = [];
for (let index = 0; index < columns.count - 1; index += 1) {
separatorPositions.push(leftMargin + (index + 1) * equalWidth + index * columns.gap + columns.gap / 2);
}
return separatorPositions;
}

const normalizedColumns = normalizeColumnLayout(columns, contentWidth);
if (normalizedColumns.count <= 1) return [];

const columnWidths =
normalizedColumns.widths ?? Array.from({ length: normalizedColumns.count }, () => normalizedColumns.width);
// A 1px separator only makes sense when every participating column is wider than the separator itself.
if (columnWidths.some((columnWidth) => columnWidth <= 1)) return [];

const separatorPositions: number[] = [];
let cursorX = leftMargin;

for (let index = 0; index < normalizedColumns.count - 1; index += 1) {
const currentColumnWidth = columnWidths[index] ?? normalizedColumns.width;
separatorPositions.push(cursorX + currentColumnWidth + normalizedColumns.gap / 2);
cursorX += currentColumnWidth + normalizedColumns.gap;
}

return separatorPositions;
}

private renderDecorationsForPage(pageEl: HTMLElement, page: Page, pageIndex: number): void {
if (this.isSemanticFlow) return;
this.renderDecorationSection(pageEl, page, pageIndex, 'header');
Expand Down
2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@superdoc-dev/react",
"version": "1.1.1",
"version": "1.2.0",
"description": "Official React wrapper for the SuperDoc document editor",
"type": "module",
"main": "./dist/index.cjs",
Expand Down
40 changes: 40 additions & 0 deletions packages/react/src/SuperDocEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,46 @@ describe('SuperDocEditor', () => {
{ timeout: 5000 },
);
});

it('should route onTransaction through the latest callback after rerender', async () => {
const ref = createRef<SuperDocRef>();
const onReady = vi.fn();
const firstOnTransaction = vi.fn();
const secondOnTransaction = vi.fn();

const { rerender } = render(<SuperDocEditor ref={ref} onReady={onReady} onTransaction={firstOnTransaction} />);

await waitFor(() => expect(onReady).toHaveBeenCalled(), { timeout: 5000 });

const instance = ref.current?.getInstance();
expect(instance).toBeTruthy();

const transactionEvent = {
editor: {},
sourceEditor: {},
transaction: { docChanged: true },
surface: 'body',
};

const firstCallCountBeforeManualDispatch = firstOnTransaction.mock.calls.length;
(instance as any).config.onTransaction(transactionEvent);

expect(firstOnTransaction).toHaveBeenLastCalledWith(transactionEvent);
expect(firstOnTransaction).toHaveBeenCalledTimes(firstCallCountBeforeManualDispatch + 1);
expect(secondOnTransaction).not.toHaveBeenCalled();

rerender(<SuperDocEditor ref={ref} onReady={onReady} onTransaction={secondOnTransaction} />);

expect(ref.current?.getInstance()).toBe(instance);

const firstCallCountBeforeRerenderDispatch = firstOnTransaction.mock.calls.length;
const secondCallCountBeforeManualDispatch = secondOnTransaction.mock.calls.length;
(instance as any).config.onTransaction(transactionEvent);

expect(firstOnTransaction).toHaveBeenCalledTimes(firstCallCountBeforeRerenderDispatch);
expect(secondOnTransaction).toHaveBeenLastCalledWith(transactionEvent);
expect(secondOnTransaction).toHaveBeenCalledTimes(secondCallCountBeforeManualDispatch + 1);
});
});

describe('onEditorDestroy', () => {
Expand Down
11 changes: 10 additions & 1 deletion packages/react/src/SuperDocEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
SuperDocReadyEvent,
SuperDocEditorCreateEvent,
SuperDocEditorUpdateEvent,
SuperDocTransactionEvent,
SuperDocContentErrorEvent,
SuperDocExceptionEvent,
} from './types';
Expand Down Expand Up @@ -46,6 +47,7 @@ function SuperDocEditorInner(props: SuperDocEditorProps, ref: ForwardedRef<Super
onEditorCreate,
onEditorDestroy,
onEditorUpdate,
onTransaction,
onContentError,
onException,
// Key props that trigger rebuild when changed
Expand Down Expand Up @@ -85,6 +87,7 @@ function SuperDocEditorInner(props: SuperDocEditorProps, ref: ForwardedRef<Super
onEditorCreate,
onEditorDestroy,
onEditorUpdate,
onTransaction,
onContentError,
onException,
});
Expand All @@ -96,10 +99,11 @@ function SuperDocEditorInner(props: SuperDocEditorProps, ref: ForwardedRef<Super
onEditorCreate,
onEditorDestroy,
onEditorUpdate,
onTransaction,
onContentError,
onException,
};
}, [onReady, onEditorCreate, onEditorDestroy, onEditorUpdate, onContentError, onException]);
}, [onReady, onEditorCreate, onEditorDestroy, onEditorUpdate, onTransaction, onContentError, onException]);

// Queue mode changes that happen during init
const pendingModeRef = useRef<DocumentMode | null>(null);
Expand Down Expand Up @@ -192,6 +196,11 @@ function SuperDocEditorInner(props: SuperDocEditorProps, ref: ForwardedRef<Super
callbacksRef.current.onEditorUpdate?.(event);
}
},
onTransaction: (event: SuperDocTransactionEvent) => {
if (!destroyed) {
callbacksRef.current.onTransaction?.(event);
}
},
onContentError: (event: SuperDocContentErrorEvent) => {
if (!destroyed) {
callbacksRef.current.onContentError?.(event);
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type {
SuperDocReadyEvent,
SuperDocEditorCreateEvent,
SuperDocEditorUpdateEvent,
SuperDocTransactionEvent,
SuperDocContentErrorEvent,
SuperDocExceptionEvent,
} from './types';
Loading
Loading