Skip to content

Commit f757b74

Browse files
committed
Merge branch 'main' into stable
2 parents 3bf6bb6 + e1dc6d5 commit f757b74

18 files changed

Lines changed: 507 additions & 13 deletions

File tree

packages/layout-engine/pm-adapter/src/converters/math-block.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,34 @@ describe('handleMathBlockNode', () => {
8080
handleMathBlockNode(makeNode({ textContent: 'b' }) as any, context);
8181
expect(blocks[0].id).not.toBe(blocks[1].id);
8282
});
83+
84+
it('resolves paragraph spacing from paragraphProperties', () => {
85+
const { context, blocks } = makeContext();
86+
const node = makeNode({
87+
textContent: 'x',
88+
paragraphProperties: { spacing: { before: 240, after: 160, line: 276, lineRule: 'auto' } },
89+
});
90+
91+
handleMathBlockNode(node as any, context);
92+
93+
const block = blocks[0] as ParagraphBlock;
94+
expect(block.attrs?.spacing).toBeDefined();
95+
expect(block.attrs?.spacing?.before).toBeGreaterThan(0);
96+
expect(block.attrs?.spacing?.after).toBeGreaterThan(0);
97+
});
98+
99+
it('falls back to paragraph alignment when justification is unknown', () => {
100+
const { context, blocks } = makeContext();
101+
const node = makeNode({
102+
textContent: 'x',
103+
justification: 'unknown',
104+
paragraphProperties: { justification: 'right' },
105+
});
106+
107+
handleMathBlockNode(node as any, context);
108+
109+
const block = blocks[0] as ParagraphBlock;
110+
// When math justification is unknown, falls back to paragraphAttrs alignment
111+
expect(block.attrs?.alignment).toBeDefined();
112+
});
83113
});

packages/layout-engine/pm-adapter/src/converters/math-block.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ParagraphBlock, MathRun } from '@superdoc/contracts';
22
import type { PMNode, NodeHandlerContext } from '../types.js';
33
import { estimateMathDimensions } from './math-constants.js';
4+
import { computeParagraphAttrs } from '../attributes/index.js';
45

56
const JUSTIFICATION_TO_ALIGN: Record<string, 'left' | 'center' | 'right'> = {
67
center: 'center',
@@ -32,12 +33,17 @@ export function handleMathBlockNode(node: PMNode, context: NodeHandlerContext):
3233
pmEnd: pos?.end,
3334
};
3435

36+
// Resolve paragraph spacing from the parent w:p's properties (carried via paragraphProperties attr).
37+
// This ensures display math paragraphs get the same spacing as their containing paragraph.
38+
const { paragraphAttrs } = computeParagraphAttrs(node, context.converterContext);
39+
3540
const block: ParagraphBlock = {
3641
kind: 'paragraph',
3742
id: nextBlockId('paragraph'),
3843
runs: [mathRun],
3944
attrs: {
40-
alignment: JUSTIFICATION_TO_ALIGN[justification] ?? 'center',
45+
...paragraphAttrs,
46+
alignment: JUSTIFICATION_TO_ALIGN[justification] ?? paragraphAttrs.alignment ?? 'center',
4147
},
4248
};
4349

packages/sdk/codegen/src/generate-intent-tools.mjs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import path from 'node:path';
21
import { readFile } from 'node:fs/promises';
2+
import path from 'node:path';
33
import { loadContract, REPO_ROOT, stripBoundParams, writeGeneratedFile } from './shared.mjs';
44

55
const TOOLS_OUTPUT_DIR = path.join(REPO_ROOT, 'packages/sdk/tools');
@@ -495,15 +495,11 @@ function toOpenAiTool(entry) {
495495
}
496496

497497
function toAnthropicTool(entry) {
498-
const tool = {
498+
return {
499499
name: entry.toolName,
500500
description: entry.description,
501501
input_schema: entry.inputSchema,
502502
};
503-
if (entry.inputExamples?.length) {
504-
tool.input_examples = entry.inputExamples;
505-
}
506-
return tool;
507503
}
508504

509505
function toVercelTool(entry) {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { mount } from '@vue/test-utils';
3+
import ImageResizeOverlay from './ImageResizeOverlay.vue';
4+
5+
vi.mock('@superdoc/layout-bridge', () => ({
6+
measureCache: {
7+
invalidate: vi.fn(),
8+
},
9+
}));
10+
11+
function createMockEditor(overrides = {}) {
12+
return {
13+
options: { documentMode: 'editing' },
14+
isEditable: true,
15+
view: {
16+
dom: document.createElement('div'),
17+
state: { doc: { nodeAt: vi.fn() }, tr: { setNodeMarkup: vi.fn().mockReturnThis() } },
18+
dispatch: vi.fn(),
19+
},
20+
...overrides,
21+
};
22+
}
23+
24+
describe('ImageResizeOverlay', () => {
25+
describe('isResizeDisabled guard', () => {
26+
it('should report resize disabled when documentMode is viewing', () => {
27+
const editor = createMockEditor({ options: { documentMode: 'viewing' }, isEditable: false });
28+
const imageEl = document.createElement('div');
29+
30+
const wrapper = mount(ImageResizeOverlay, {
31+
props: { editor, visible: true, imageElement: imageEl },
32+
});
33+
34+
expect(wrapper.vm.isResizeDisabled).toBe(true);
35+
});
36+
37+
it('should report resize disabled when editor is not editable', () => {
38+
const editor = createMockEditor({ isEditable: false });
39+
const imageEl = document.createElement('div');
40+
41+
const wrapper = mount(ImageResizeOverlay, {
42+
props: { editor, visible: true, imageElement: imageEl },
43+
});
44+
45+
expect(wrapper.vm.isResizeDisabled).toBe(true);
46+
});
47+
48+
it('should not report resize disabled in editing mode', () => {
49+
const editor = createMockEditor();
50+
const imageEl = document.createElement('div');
51+
52+
const wrapper = mount(ImageResizeOverlay, {
53+
props: { editor, visible: true, imageElement: imageEl },
54+
});
55+
56+
expect(wrapper.vm.isResizeDisabled).toBe(false);
57+
});
58+
});
59+
});

packages/super-editor/src/editors/v1/components/ImageResizeOverlay.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ const props = defineProps({
7272
7373
const emit = defineEmits(['resize-start', 'resize-move', 'resize-end', 'resize-success', 'resize-error']);
7474
75+
const isResizeDisabled = computed(() => props.editor?.options?.documentMode === 'viewing' || !props.editor?.isEditable);
76+
7577
/**
7678
* Parsed image metadata from data-image-metadata attribute
7779
*/
@@ -320,6 +322,8 @@ function onHandleMouseDown(event, handlePosition) {
320322
event.preventDefault();
321323
event.stopPropagation();
322324
325+
if (isResizeDisabled.value) return;
326+
323327
if (!isValidEditor(props.editor) || !imageMetadata.value || !props.imageElement) return;
324328
325329
const rect = props.imageElement.getBoundingClientRect();

packages/super-editor/src/editors/v1/components/SuperEditor.test.js

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ const EditorConstructor = vi.hoisted(() => {
1818
});
1919
this.off = vi.fn();
2020
this.view = { focus: vi.fn() };
21+
this.setDocumentMode = vi.fn((mode) => {
22+
this.options.documentMode = mode;
23+
this.listeners.documentModeChange?.({ documentMode: mode, editor: this });
24+
});
2125
this.destroy = vi.fn();
2226
});
2327

@@ -1328,6 +1332,126 @@ describe('SuperEditor.vue', () => {
13281332
wrapper.unmount();
13291333
vi.useRealTimers();
13301334
});
1335+
1336+
it('should hide image resize overlay and skip image hover updates in viewing mode', async () => {
1337+
vi.useFakeTimers();
1338+
EditorConstructor.loadXmlData.mockResolvedValueOnce(['<docx />', {}, {}, {}]);
1339+
1340+
const wrapper = mount(SuperEditor, {
1341+
props: {
1342+
documentId: 'doc-image-view-guard',
1343+
options: {},
1344+
},
1345+
});
1346+
1347+
await flushPromises();
1348+
await flushPromises();
1349+
1350+
const updateSpy = vi.spyOn(wrapper.vm, 'updateImageResizeOverlay');
1351+
1352+
Object.defineProperty(wrapper.vm, 'activeEditor', {
1353+
value: {
1354+
value: {
1355+
options: { documentMode: 'viewing' },
1356+
isEditable: false,
1357+
view: { focus: vi.fn() },
1358+
},
1359+
},
1360+
});
1361+
wrapper.vm.getDocumentMode = () => 'viewing';
1362+
wrapper.vm.isViewingMode = () => true;
1363+
1364+
wrapper.vm.imageResizeState.visible = true;
1365+
wrapper.vm.imageResizeState.imageElement = document.createElement('div');
1366+
wrapper.vm.imageResizeState.blockId = 'image-block';
1367+
1368+
wrapper.vm.handleOverlayUpdates(new MouseEvent('mousemove'));
1369+
1370+
expect(updateSpy).not.toHaveBeenCalled();
1371+
expect(wrapper.vm.imageResizeState.visible).toBe(false);
1372+
expect(wrapper.vm.imageResizeState.imageElement).toBe(null);
1373+
expect(wrapper.vm.imageResizeState.blockId).toBe(null);
1374+
1375+
wrapper.unmount();
1376+
vi.useRealTimers();
1377+
});
1378+
1379+
it('should not apply image selection outline in viewing mode', async () => {
1380+
vi.useFakeTimers();
1381+
EditorConstructor.loadXmlData.mockResolvedValueOnce(['<docx />', {}, {}, {}]);
1382+
1383+
const wrapper = mount(SuperEditor, {
1384+
props: {
1385+
documentId: 'doc-image-selection-view-guard',
1386+
options: {},
1387+
},
1388+
});
1389+
1390+
await flushPromises();
1391+
await flushPromises();
1392+
1393+
Object.defineProperty(wrapper.vm, 'activeEditor', {
1394+
value: {
1395+
value: {
1396+
options: { documentMode: 'viewing' },
1397+
isEditable: false,
1398+
view: { focus: vi.fn() },
1399+
},
1400+
},
1401+
});
1402+
wrapper.vm.getDocumentMode = () => 'viewing';
1403+
wrapper.vm.isViewingMode = () => true;
1404+
1405+
const imageEl = document.createElement('div');
1406+
wrapper.vm.setSelectedImage(imageEl, 'image-block', 42);
1407+
1408+
expect(imageEl.classList.contains('superdoc-image-selected')).toBe(false);
1409+
expect(wrapper.vm.selectedImageState.element).toBe(null);
1410+
expect(wrapper.vm.selectedImageState.blockId).toBe(null);
1411+
expect(wrapper.vm.selectedImageState.pmStart).toBe(null);
1412+
1413+
wrapper.unmount();
1414+
vi.useRealTimers();
1415+
});
1416+
1417+
it('should clear image selection when props switch to viewing mode', async () => {
1418+
vi.useFakeTimers();
1419+
EditorConstructor.loadXmlData.mockResolvedValueOnce(['<docx />', {}, {}, {}]);
1420+
1421+
const wrapper = mount(SuperEditor, {
1422+
props: {
1423+
documentId: 'doc-image-selection-mode-switch',
1424+
options: { documentMode: 'editing' },
1425+
},
1426+
});
1427+
1428+
await flushPromises();
1429+
await flushPromises();
1430+
1431+
const imageEl = document.createElement('div');
1432+
imageEl.classList.add('superdoc-image-selected');
1433+
wrapper.vm.selectedImageState.element = imageEl;
1434+
wrapper.vm.selectedImageState.blockId = 'image-block';
1435+
wrapper.vm.selectedImageState.pmStart = 42;
1436+
wrapper.vm.imageResizeState.visible = true;
1437+
wrapper.vm.imageResizeState.imageElement = imageEl;
1438+
wrapper.vm.imageResizeState.blockId = 'image-block';
1439+
1440+
await wrapper.setProps({
1441+
options: { documentMode: 'viewing' },
1442+
});
1443+
1444+
expect(imageEl.classList.contains('superdoc-image-selected')).toBe(false);
1445+
expect(wrapper.vm.selectedImageState.element).toBe(null);
1446+
expect(wrapper.vm.selectedImageState.blockId).toBe(null);
1447+
expect(wrapper.vm.selectedImageState.pmStart).toBe(null);
1448+
expect(wrapper.vm.imageResizeState.visible).toBe(false);
1449+
expect(wrapper.vm.imageResizeState.imageElement).toBe(null);
1450+
expect(wrapper.vm.imageResizeState.blockId).toBe(null);
1451+
1452+
wrapper.unmount();
1453+
vi.useRealTimers();
1454+
});
13311455
});
13321456
});
13331457
});

0 commit comments

Comments
 (0)