From 8603cd72176a4a641305040431c86ab70b7c7e87 Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Wed, 4 Mar 2026 13:12:11 -0800 Subject: [PATCH 1/2] feat(document-api): initial image commands --- apps/cli/scripts/export-sdk-contract.ts | 14 + .../src/__tests__/conformance/scenarios.ts | 327 +++++++ apps/cli/src/cli/operation-hints.ts | 65 ++ apps/cli/src/lib/error-mapping.ts | 32 + .../document-api/available-operations.mdx | 17 +- .../reference/_generated-manifest.json | 41 +- .../reference/capabilities/get.mdx | 688 +++++++++++++- .../document-api/reference/create/image.mdx | 270 ++++++ .../document-api/reference/create/index.mdx | 1 + .../reference/images/convert-to-floating.mdx | 161 ++++ .../reference/images/convert-to-inline.mdx | 161 ++++ .../document-api/reference/images/delete.mdx | 161 ++++ .../document-api/reference/images/get.mdx | 85 ++ .../document-api/reference/images/index.mdx | 30 + .../document-api/reference/images/list.mdx | 110 +++ .../document-api/reference/images/move.mdx | 245 +++++ .../reference/images/set-anchor-options.mdx | 193 ++++ .../reference/images/set-position.mdx | 204 ++++ .../reference/images/set-size.mdx | 198 ++++ .../reference/images/set-wrap-distances.mdx | 189 ++++ .../reference/images/set-wrap-side.mdx | 173 ++++ .../reference/images/set-wrap-type.mdx | 175 ++++ .../reference/images/set-z-order.mdx | 179 ++++ apps/docs/document-api/reference/index.mdx | 22 +- apps/docs/document-engine/sdks.mdx | 38 + .../src/contract/contract.test.ts | 1 + .../src/contract/operation-definitions.ts | 230 ++++- .../src/contract/operation-registry.ts | 38 + .../src/contract/reference-doc-map.ts | 5 + packages/document-api/src/contract/schemas.ts | 264 ++++++ packages/document-api/src/create/create.ts | 2 + packages/document-api/src/images/images.ts | 257 +++++ .../document-api/src/images/images.types.ts | 193 ++++ packages/document-api/src/index.ts | 115 +++ packages/document-api/src/invoke/invoke.ts | 18 + .../document-api/src/types/media.types.ts | 37 +- .../core/super-converter/image-dimensions.js | 168 ++++ .../super-converter/image-dimensions.test.js | 235 +++++ .../anchor/helpers/translate-anchor-node.js | 6 +- .../wp/helpers/decode-image-node-helpers.js | 100 +- .../wp/helpers/encode-image-node-helpers.js | 20 + .../contract-conformance.test.ts | 895 ++++++++++++++++++ .../assemble-adapters.ts | 33 + .../capabilities-adapter.ts | 13 + .../helpers/image-resolver.ts | 90 ++ .../helpers/node-info-mapper.ts | 14 +- .../plan-engine/images-wrappers.ts | 757 +++++++++++++++ .../extensions/content-block/content-block.js | 5 +- .../src/extensions/image/image.js | 23 +- .../extensions/shape-group/ShapeGroupView.js | 3 +- .../src/extensions/types/node-attributes.ts | 4 + .../vector-shape/VectorShapeView.js | 12 +- .../tests/images/all-commands.ts | 548 +++++++++++ .../tests/images/assets/test-image.webp | Bin 0 -> 234646 bytes 54 files changed, 7801 insertions(+), 64 deletions(-) create mode 100644 apps/docs/document-api/reference/create/image.mdx create mode 100644 apps/docs/document-api/reference/images/convert-to-floating.mdx create mode 100644 apps/docs/document-api/reference/images/convert-to-inline.mdx create mode 100644 apps/docs/document-api/reference/images/delete.mdx create mode 100644 apps/docs/document-api/reference/images/get.mdx create mode 100644 apps/docs/document-api/reference/images/index.mdx create mode 100644 apps/docs/document-api/reference/images/list.mdx create mode 100644 apps/docs/document-api/reference/images/move.mdx create mode 100644 apps/docs/document-api/reference/images/set-anchor-options.mdx create mode 100644 apps/docs/document-api/reference/images/set-position.mdx create mode 100644 apps/docs/document-api/reference/images/set-size.mdx create mode 100644 apps/docs/document-api/reference/images/set-wrap-distances.mdx create mode 100644 apps/docs/document-api/reference/images/set-wrap-side.mdx create mode 100644 apps/docs/document-api/reference/images/set-wrap-type.mdx create mode 100644 apps/docs/document-api/reference/images/set-z-order.mdx create mode 100644 packages/document-api/src/images/images.ts create mode 100644 packages/document-api/src/images/images.types.ts create mode 100644 packages/super-editor/src/core/super-converter/image-dimensions.js create mode 100644 packages/super-editor/src/core/super-converter/image-dimensions.test.js create mode 100644 packages/super-editor/src/document-api-adapters/helpers/image-resolver.ts create mode 100644 packages/super-editor/src/document-api-adapters/plan-engine/images-wrappers.ts create mode 100644 tests/doc-api-stories/tests/images/all-commands.ts create mode 100644 tests/doc-api-stories/tests/images/assets/test-image.webp diff --git a/apps/cli/scripts/export-sdk-contract.ts b/apps/cli/scripts/export-sdk-contract.ts index 62cb23faa1..46333d3d2a 100644 --- a/apps/cli/scripts/export-sdk-contract.ts +++ b/apps/cli/scripts/export-sdk-contract.ts @@ -194,6 +194,20 @@ const INTENT_NAMES = { 'doc.history.get': 'get_history', 'doc.history.undo': 'undo', 'doc.history.redo': 'redo', + 'doc.create.image': 'create_image', + 'doc.images.list': 'list_images', + 'doc.images.get': 'get_image', + 'doc.images.delete': 'delete_image', + 'doc.images.move': 'move_image', + 'doc.images.convertToInline': 'convert_image_to_inline', + 'doc.images.convertToFloating': 'convert_image_to_floating', + 'doc.images.setSize': 'set_image_size', + 'doc.images.setWrapType': 'set_image_wrap_type', + 'doc.images.setWrapSide': 'set_image_wrap_side', + 'doc.images.setWrapDistances': 'set_image_wrap_distances', + 'doc.images.setPosition': 'set_image_position', + 'doc.images.setAnchorOptions': 'set_image_anchor_options', + 'doc.images.setZOrder': 'set_image_z_order', } as const satisfies Record; // --------------------------------------------------------------------------- diff --git a/apps/cli/src/__tests__/conformance/scenarios.ts b/apps/cli/src/__tests__/conformance/scenarios.ts index 6fecef8c12..884ebfe2bb 100644 --- a/apps/cli/src/__tests__/conformance/scenarios.ts +++ b/apps/cli/src/__tests__/conformance/scenarios.ts @@ -559,6 +559,112 @@ async function createDocWithMarkedTocEntry( return { docPath: markedDoc, entryAddress }; } +const CONFORMANCE_IMAGE_DATA_URI = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAASsJTYQAAAAASUVORK5CYII='; + +type ImagePlacement = 'inline' | 'floating'; +type ImageFixture = { + docPath: string; + imageId: string; +}; + +function pickImageId( + items: Record[], + context: string, + placement?: ImagePlacement, +): { imageId: string; item: Record } { + const match = + placement === undefined + ? items[0] + : (items.find((item) => { + const address = item.address; + if (!address || typeof address !== 'object') return false; + return (address as Record).placement === placement; + }) ?? items[0]); + + if (!match) { + throw new Error(`[${context}] No images available.`); + } + + const imageId = match.sdImageId; + if (typeof imageId !== 'string' || imageId.length === 0) { + throw new Error(`[${context}] Unable to resolve image id from list output.`); + } + + return { imageId, item: match }; +} + +async function resolveImageFixture( + harness: ConformanceHarness, + stateDir: string, + docPath: string, + context: string, + placement?: ImagePlacement, +): Promise { + const listed = await harness.runCli([...commandTokens('doc.images.list'), docPath, '--limit', '20'], stateDir); + if (listed.result.code !== 0 || listed.envelope.ok !== true) { + throw new Error(`[${context}] Failed to list images.`); + } + + const items = extractDiscoveryItems(listed.envelope.data); + const { imageId } = pickImageId(items, context, placement); + return { docPath, imageId }; +} + +async function createInlineImageFixture( + harness: ConformanceHarness, + stateDir: string, + label: string, +): Promise { + const sourceDoc = await harness.copyFixtureDoc(`${label}-source`); + const outputDoc = harness.createOutputPath(`${label}-with-image`); + const created = await harness.runCli( + [ + ...commandTokens('doc.create.image'), + sourceDoc, + '--src', + CONFORMANCE_IMAGE_DATA_URI, + '--alt', + 'Conformance image', + '--at-json', + JSON.stringify({ kind: 'documentEnd' }), + '--out', + outputDoc, + ], + stateDir, + ); + if (created.result.code !== 0 || created.envelope.ok !== true) { + throw new Error(`[${label}] Failed to create image fixture.`); + } + + return resolveImageFixture(harness, stateDir, outputDoc, `${label}:inline`, 'inline'); +} + +async function createFloatingImageFixture( + harness: ConformanceHarness, + stateDir: string, + label: string, +): Promise { + const inlineFixture = await createInlineImageFixture(harness, stateDir, `${label}-seed-inline`); + const floatingDoc = harness.createOutputPath(`${label}-floating`); + const converted = await harness.runCli( + [ + ...commandTokens('doc.images.convertToFloating'), + inlineFixture.docPath, + '--image-id', + inlineFixture.imageId, + '--out', + floatingDoc, + ], + stateDir, + ); + if (converted.result.code !== 0 || converted.envelope.ok !== true) { + throw new Error(`[${label}] Failed to convert fixture image to floating.`); + } + + return resolveImageFixture(harness, stateDir, floatingDoc, `${label}:floating`, 'floating'); +} + export const SUCCESS_SCENARIOS = { 'doc.open': async (harness: ConformanceHarness): Promise => { const stateDir = await harness.createStateDir('doc-open-success'); @@ -1642,6 +1748,227 @@ export const SUCCESS_SCENARIOS = { ], }; }, + + // --------------------------------------------------------------------------- + // Image operations + // --------------------------------------------------------------------------- + + 'doc.create.image': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('doc-create-image-success'); + const docPath = await harness.copyFixtureDoc('doc-create-image'); + return { + stateDir, + args: [ + ...commandTokens('doc.create.image'), + docPath, + '--src', + CONFORMANCE_IMAGE_DATA_URI, + '--alt', + 'Conformance image', + '--at-json', + JSON.stringify({ kind: 'documentEnd' }), + '--out', + harness.createOutputPath('doc-create-image-output'), + ], + }; + }, + 'doc.images.list': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('doc-images-list-success'); + const fixture = await createInlineImageFixture(harness, stateDir, 'doc-images-list'); + return { + stateDir, + args: [...commandTokens('doc.images.list'), fixture.docPath, '--limit', '20'], + }; + }, + 'doc.images.get': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('doc-images-get-success'); + const fixture = await createInlineImageFixture(harness, stateDir, 'doc-images-get'); + return { + stateDir, + args: [...commandTokens('doc.images.get'), fixture.docPath, '--image-id', fixture.imageId], + }; + }, + 'doc.images.delete': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('doc-images-delete-success'); + const fixture = await createInlineImageFixture(harness, stateDir, 'doc-images-delete'); + return { + stateDir, + args: [ + ...commandTokens('doc.images.delete'), + fixture.docPath, + '--image-id', + fixture.imageId, + '--out', + harness.createOutputPath('doc-images-delete-output'), + ], + }; + }, + 'doc.images.move': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('doc-images-move-success'); + const fixture = await createInlineImageFixture(harness, stateDir, 'doc-images-move'); + return { + stateDir, + args: [ + ...commandTokens('doc.images.move'), + fixture.docPath, + '--image-id', + fixture.imageId, + '--to-json', + JSON.stringify({ kind: 'documentStart' }), + '--out', + harness.createOutputPath('doc-images-move-output'), + ], + }; + }, + 'doc.images.convertToInline': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('doc-images-convert-to-inline-success'); + const fixture = await createFloatingImageFixture(harness, stateDir, 'doc-images-convert-to-inline'); + return { + stateDir, + args: [ + ...commandTokens('doc.images.convertToInline'), + fixture.docPath, + '--image-id', + fixture.imageId, + '--out', + harness.createOutputPath('doc-images-convert-to-inline-output'), + ], + }; + }, + 'doc.images.convertToFloating': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('doc-images-convert-to-floating-success'); + const fixture = await createInlineImageFixture(harness, stateDir, 'doc-images-convert-to-floating'); + return { + stateDir, + args: [ + ...commandTokens('doc.images.convertToFloating'), + fixture.docPath, + '--image-id', + fixture.imageId, + '--out', + harness.createOutputPath('doc-images-convert-to-floating-output'), + ], + }; + }, + 'doc.images.setSize': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('doc-images-set-size-success'); + const fixture = await createInlineImageFixture(harness, stateDir, 'doc-images-set-size'); + return { + stateDir, + args: [ + ...commandTokens('doc.images.setSize'), + fixture.docPath, + '--image-id', + fixture.imageId, + '--size-json', + JSON.stringify({ width: 240, height: 120 }), + '--out', + harness.createOutputPath('doc-images-set-size-output'), + ], + }; + }, + 'doc.images.setWrapType': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('doc-images-set-wrap-type-success'); + const fixture = await createFloatingImageFixture(harness, stateDir, 'doc-images-set-wrap-type'); + return { + stateDir, + args: [ + ...commandTokens('doc.images.setWrapType'), + fixture.docPath, + '--image-id', + fixture.imageId, + '--type', + 'Tight', + '--out', + harness.createOutputPath('doc-images-set-wrap-type-output'), + ], + }; + }, + 'doc.images.setWrapSide': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('doc-images-set-wrap-side-success'); + const fixture = await createFloatingImageFixture(harness, stateDir, 'doc-images-set-wrap-side'); + return { + stateDir, + args: [ + ...commandTokens('doc.images.setWrapSide'), + fixture.docPath, + '--image-id', + fixture.imageId, + '--side', + 'left', + '--out', + harness.createOutputPath('doc-images-set-wrap-side-output'), + ], + }; + }, + 'doc.images.setWrapDistances': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('doc-images-set-wrap-distances-success'); + const fixture = await createFloatingImageFixture(harness, stateDir, 'doc-images-set-wrap-distances'); + return { + stateDir, + args: [ + ...commandTokens('doc.images.setWrapDistances'), + fixture.docPath, + '--image-id', + fixture.imageId, + '--distances-json', + JSON.stringify({ distTop: 100, distBottom: 100 }), + '--out', + harness.createOutputPath('doc-images-set-wrap-distances-output'), + ], + }; + }, + 'doc.images.setPosition': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('doc-images-set-position-success'); + const fixture = await createFloatingImageFixture(harness, stateDir, 'doc-images-set-position'); + return { + stateDir, + args: [ + ...commandTokens('doc.images.setPosition'), + fixture.docPath, + '--image-id', + fixture.imageId, + '--position-json', + JSON.stringify({ hRelativeFrom: 'column', alignH: 'center' }), + '--out', + harness.createOutputPath('doc-images-set-position-output'), + ], + }; + }, + 'doc.images.setAnchorOptions': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('doc-images-set-anchor-options-success'); + const fixture = await createFloatingImageFixture(harness, stateDir, 'doc-images-set-anchor-options'); + return { + stateDir, + args: [ + ...commandTokens('doc.images.setAnchorOptions'), + fixture.docPath, + '--image-id', + fixture.imageId, + '--options-json', + JSON.stringify({ behindDoc: true, allowOverlap: false }), + '--out', + harness.createOutputPath('doc-images-set-anchor-options-output'), + ], + }; + }, + 'doc.images.setZOrder': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('doc-images-set-z-order-success'); + const fixture = await createFloatingImageFixture(harness, stateDir, 'doc-images-set-z-order'); + return { + stateDir, + args: [ + ...commandTokens('doc.images.setZOrder'), + fixture.docPath, + '--image-id', + fixture.imageId, + '--z-order-json', + JSON.stringify({ relativeHeight: 500 }), + '--out', + harness.createOutputPath('doc-images-set-z-order-output'), + ], + }; + }, 'doc.toc.list': async (harness: ConformanceHarness): Promise => { const stateDir = await harness.createStateDir('doc-toc-list-success'); const docPath = await harness.copyTocFixtureDoc('doc-toc-list', stateDir); diff --git a/apps/cli/src/cli/operation-hints.ts b/apps/cli/src/cli/operation-hints.ts index eae120f593..74bd34b381 100644 --- a/apps/cli/src/cli/operation-hints.ts +++ b/apps/cli/src/cli/operation-hints.ts @@ -176,6 +176,22 @@ export const SUCCESS_VERB: Record = { 'history.get': 'retrieved history state', 'history.undo': 'undid last change', 'history.redo': 'redid last change', + + // Images + 'create.image': 'created image', + 'images.list': 'listed images', + 'images.get': 'resolved image', + 'images.delete': 'deleted image', + 'images.move': 'moved image', + 'images.convertToInline': 'converted to inline', + 'images.convertToFloating': 'converted to floating', + 'images.setSize': 'set image size', + 'images.setWrapType': 'set wrap type', + 'images.setWrapSide': 'set wrap side', + 'images.setWrapDistances': 'set wrap distances', + 'images.setPosition': 'set position', + 'images.setAnchorOptions': 'set anchor options', + 'images.setZOrder': 'set z-order', }; // --------------------------------------------------------------------------- @@ -311,6 +327,22 @@ export const OUTPUT_FORMAT: Record = { 'history.get': 'plain', 'history.undo': 'plain', 'history.redo': 'plain', + + // Images + 'create.image': 'createResult', + 'images.list': 'plain', + 'images.get': 'plain', + 'images.delete': 'plain', + 'images.move': 'plain', + 'images.convertToInline': 'plain', + 'images.convertToFloating': 'plain', + 'images.setSize': 'plain', + 'images.setWrapType': 'plain', + 'images.setWrapSide': 'plain', + 'images.setWrapDistances': 'plain', + 'images.setPosition': 'plain', + 'images.setAnchorOptions': 'plain', + 'images.setZOrder': 'plain', }; // --------------------------------------------------------------------------- @@ -430,6 +462,22 @@ export const RESPONSE_ENVELOPE_KEY: Record 'history.get': 'result', 'history.undo': 'result', 'history.redo': 'result', + + // Images + 'create.image': 'result', + 'images.list': 'result', + 'images.get': 'result', + 'images.delete': 'result', + 'images.move': 'result', + 'images.convertToInline': 'result', + 'images.convertToFloating': 'result', + 'images.setSize': 'result', + 'images.setWrapType': 'result', + 'images.setWrapSide': 'result', + 'images.setWrapDistances': 'result', + 'images.setPosition': 'result', + 'images.setAnchorOptions': 'result', + 'images.setZOrder': 'result', }; // --------------------------------------------------------------------------- @@ -464,6 +512,7 @@ export type OperationFamily = | 'comments' | 'lists' | 'tables' + | 'images' | 'toc' | 'textMutation' | 'create' @@ -577,4 +626,20 @@ export const OPERATION_FAMILY: Record = 'history.get': 'query', 'history.undo': 'general', 'history.redo': 'general', + + // Images + 'create.image': 'images', + 'images.list': 'images', + 'images.get': 'images', + 'images.delete': 'images', + 'images.move': 'images', + 'images.convertToInline': 'images', + 'images.convertToFloating': 'images', + 'images.setSize': 'images', + 'images.setWrapType': 'images', + 'images.setWrapSide': 'images', + 'images.setWrapDistances': 'images', + 'images.setPosition': 'images', + 'images.setAnchorOptions': 'images', + 'images.setZOrder': 'images', }; diff --git a/apps/cli/src/lib/error-mapping.ts b/apps/cli/src/lib/error-mapping.ts index 20e0cf74f5..a0840710c7 100644 --- a/apps/cli/src/lib/error-mapping.ts +++ b/apps/cli/src/lib/error-mapping.ts @@ -97,6 +97,26 @@ function mapListsError(operationId: CliExposedOperationId, error: unknown, code: return new CliError('COMMAND_FAILED', message, { operationId, details }); } +function mapImagesError(operationId: CliExposedOperationId, error: unknown, code: string | undefined): CliError { + const message = extractErrorMessage(error); + const details = extractErrorDetails(error); + + if (code === 'TARGET_NOT_FOUND') { + return new CliError('TARGET_NOT_FOUND', message, { operationId, details }); + } + + if (code === 'INVALID_TARGET') { + return new CliError('INVALID_ARGUMENT', message, { operationId, details }); + } + + if (code === 'CAPABILITY_UNAVAILABLE' || code === 'COMMAND_UNAVAILABLE') { + return new CliError('COMMAND_FAILED', message, { operationId, details }); + } + + if (error instanceof CliError) return error; + return new CliError('COMMAND_FAILED', message, { operationId, details }); +} + function mapTablesError(operationId: CliExposedOperationId, error: unknown, code: string | undefined): CliError { const message = extractErrorMessage(error); const details = extractErrorDetails(error); @@ -305,6 +325,7 @@ const FAMILY_MAPPERS: Record< comments: mapCommentsError, lists: mapListsError, tables: mapTablesError, + images: mapImagesError, toc: mapTocError, textMutation: mapTextMutationError, create: mapCreateError, @@ -460,6 +481,17 @@ export function mapFailedReceipt(operationId: CliExposedOperationId, result: unk return new CliError('COMMAND_FAILED', failureMessage, { operationId, failure }); } + // Images family + if (family === 'images') { + if (failureCode === 'TARGET_NOT_FOUND') { + return new CliError('TARGET_NOT_FOUND', failureMessage, { operationId, failure }); + } + if (failureCode === 'INVALID_TARGET') { + return new CliError('INVALID_ARGUMENT', failureMessage, { operationId, failure }); + } + return new CliError('COMMAND_FAILED', failureMessage, { operationId, failure }); + } + // Tables family if (family === 'tables') { if (failureCode === 'TARGET_NOT_FOUND') { diff --git a/apps/docs/document-api/available-operations.mdx b/apps/docs/document-api/available-operations.mdx index 4c9ba72cc0..e8a22a7335 100644 --- a/apps/docs/document-api/available-operations.mdx +++ b/apps/docs/document-api/available-operations.mdx @@ -18,9 +18,10 @@ Use the tables below to see what operations are available and where each one is | Capabilities | 1 | 0 | 1 | [Reference](/document-api/reference/capabilities/index) | | Comments | 5 | 0 | 5 | [Reference](/document-api/reference/comments/index) | | Core | 10 | 0 | 10 | [Reference](/document-api/reference/core/index) | -| Create | 5 | 0 | 5 | [Reference](/document-api/reference/create/index) | +| Create | 6 | 0 | 6 | [Reference](/document-api/reference/create/index) | | Format | 44 | 1 | 45 | [Reference](/document-api/reference/format/index) | | History | 3 | 0 | 3 | [Reference](/document-api/reference/history/index) | +| Images | 13 | 0 | 13 | [Reference](/document-api/reference/images/index) | | Lists | 17 | 0 | 17 | [Reference](/document-api/reference/lists/index) | | Mutations | 2 | 0 | 2 | [Reference](/document-api/reference/mutations/index) | | Paragraph Formatting | 17 | 0 | 17 | [Reference](/document-api/reference/format/paragraph/index) | @@ -56,6 +57,7 @@ Use the tables below to see what operations are available and where each one is | editor.doc.create.sectionBreak(...) | [`create.sectionBreak`](/document-api/reference/create/section-break) | | editor.doc.create.table(...) | [`create.table`](/document-api/reference/create/table) | | editor.doc.create.tableOfContents(...) | [`create.tableOfContents`](/document-api/reference/create/table-of-contents) | +| editor.doc.create.image(...) | [`create.image`](/document-api/reference/create/image) | | editor.doc.format.apply(...) | [`format.apply`](/document-api/reference/format/apply) | | editor.doc.format.bold(...) | [`format.bold`](/document-api/reference/format/bold) | | editor.doc.format.italic(...) | [`format.italic`](/document-api/reference/format/italic) | @@ -104,6 +106,19 @@ Use the tables below to see what operations are available and where each one is | editor.doc.history.get(...) | [`history.get`](/document-api/reference/history/get) | | editor.doc.history.undo(...) | [`history.undo`](/document-api/reference/history/undo) | | editor.doc.history.redo(...) | [`history.redo`](/document-api/reference/history/redo) | +| editor.doc.images.list(...) | [`images.list`](/document-api/reference/images/list) | +| editor.doc.images.get(...) | [`images.get`](/document-api/reference/images/get) | +| editor.doc.images.delete(...) | [`images.delete`](/document-api/reference/images/delete) | +| editor.doc.images.move(...) | [`images.move`](/document-api/reference/images/move) | +| editor.doc.images.convertToInline(...) | [`images.convertToInline`](/document-api/reference/images/convert-to-inline) | +| editor.doc.images.convertToFloating(...) | [`images.convertToFloating`](/document-api/reference/images/convert-to-floating) | +| editor.doc.images.setSize(...) | [`images.setSize`](/document-api/reference/images/set-size) | +| editor.doc.images.setWrapType(...) | [`images.setWrapType`](/document-api/reference/images/set-wrap-type) | +| editor.doc.images.setWrapSide(...) | [`images.setWrapSide`](/document-api/reference/images/set-wrap-side) | +| editor.doc.images.setWrapDistances(...) | [`images.setWrapDistances`](/document-api/reference/images/set-wrap-distances) | +| editor.doc.images.setPosition(...) | [`images.setPosition`](/document-api/reference/images/set-position) | +| editor.doc.images.setAnchorOptions(...) | [`images.setAnchorOptions`](/document-api/reference/images/set-anchor-options) | +| editor.doc.images.setZOrder(...) | [`images.setZOrder`](/document-api/reference/images/set-z-order) | | editor.doc.lists.list(...) | [`lists.list`](/document-api/reference/lists/list) | | editor.doc.lists.get(...) | [`lists.get`](/document-api/reference/lists/get) | | editor.doc.lists.insert(...) | [`lists.insert`](/document-api/reference/lists/insert) | diff --git a/apps/docs/document-api/reference/_generated-manifest.json b/apps/docs/document-api/reference/_generated-manifest.json index 975bec65ea..bbf5d03ea1 100644 --- a/apps/docs/document-api/reference/_generated-manifest.json +++ b/apps/docs/document-api/reference/_generated-manifest.json @@ -13,6 +13,7 @@ "apps/docs/document-api/reference/comments/patch.mdx", "apps/docs/document-api/reference/core/index.mdx", "apps/docs/document-api/reference/create/heading.mdx", + "apps/docs/document-api/reference/create/image.mdx", "apps/docs/document-api/reference/create/index.mdx", "apps/docs/document-api/reference/create/paragraph.mdx", "apps/docs/document-api/reference/create/section-break.mdx", @@ -92,6 +93,20 @@ "apps/docs/document-api/reference/history/index.mdx", "apps/docs/document-api/reference/history/redo.mdx", "apps/docs/document-api/reference/history/undo.mdx", + "apps/docs/document-api/reference/images/convert-to-floating.mdx", + "apps/docs/document-api/reference/images/convert-to-inline.mdx", + "apps/docs/document-api/reference/images/delete.mdx", + "apps/docs/document-api/reference/images/get.mdx", + "apps/docs/document-api/reference/images/index.mdx", + "apps/docs/document-api/reference/images/list.mdx", + "apps/docs/document-api/reference/images/move.mdx", + "apps/docs/document-api/reference/images/set-anchor-options.mdx", + "apps/docs/document-api/reference/images/set-position.mdx", + "apps/docs/document-api/reference/images/set-size.mdx", + "apps/docs/document-api/reference/images/set-wrap-distances.mdx", + "apps/docs/document-api/reference/images/set-wrap-side.mdx", + "apps/docs/document-api/reference/images/set-wrap-type.mdx", + "apps/docs/document-api/reference/images/set-z-order.mdx", "apps/docs/document-api/reference/index.mdx", "apps/docs/document-api/reference/info.mdx", "apps/docs/document-api/reference/insert.mdx", @@ -244,7 +259,8 @@ "create.heading", "create.sectionBreak", "create.table", - "create.tableOfContents" + "create.tableOfContents", + "create.image" ], "pagePath": "apps/docs/document-api/reference/create/index.mdx", "title": "Create" @@ -493,8 +509,29 @@ ], "pagePath": "apps/docs/document-api/reference/toc/index.mdx", "title": "Table of Contents" + }, + { + "aliasMemberPaths": [], + "key": "images", + "operationIds": [ + "images.list", + "images.get", + "images.delete", + "images.move", + "images.convertToInline", + "images.convertToFloating", + "images.setSize", + "images.setWrapType", + "images.setWrapSide", + "images.setWrapDistances", + "images.setPosition", + "images.setAnchorOptions", + "images.setZOrder" + ], + "pagePath": "apps/docs/document-api/reference/images/index.mdx", + "title": "Images" } ], "marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}", - "sourceHash": "e06db0688bf06e7963ac3e777833076f02a71b4fa840cf301ca76a51efedc48f" + "sourceHash": "b0802fa2163aafb18783cc4e673a0857aaff5a3254e95c1f727ca13cb96119b8" } diff --git a/apps/docs/document-api/reference/capabilities/get.mdx b/apps/docs/document-api/reference/capabilities/get.mdx index 18f347e6cc..9693e95301 100644 --- a/apps/docs/document-api/reference/capabilities/get.mdx +++ b/apps/docs/document-api/reference/capabilities/get.mdx @@ -312,6 +312,11 @@ _No fields._ | `operations.create.heading.dryRun` | boolean | yes | | | `operations.create.heading.reasons` | enum[] | no | | | `operations.create.heading.tracked` | boolean | yes | | +| `operations.create.image` | object | yes | | +| `operations.create.image.available` | boolean | yes | | +| `operations.create.image.dryRun` | boolean | yes | | +| `operations.create.image.reasons` | enum[] | no | | +| `operations.create.image.tracked` | boolean | yes | | | `operations.create.paragraph` | object | yes | | | `operations.create.paragraph.available` | boolean | yes | | | `operations.create.paragraph.dryRun` | boolean | yes | | @@ -687,6 +692,71 @@ _No fields._ | `operations.history.undo.dryRun` | boolean | yes | | | `operations.history.undo.reasons` | enum[] | no | | | `operations.history.undo.tracked` | boolean | yes | | +| `operations.images.convertToFloating` | object | yes | | +| `operations.images.convertToFloating.available` | boolean | yes | | +| `operations.images.convertToFloating.dryRun` | boolean | yes | | +| `operations.images.convertToFloating.reasons` | enum[] | no | | +| `operations.images.convertToFloating.tracked` | boolean | yes | | +| `operations.images.convertToInline` | object | yes | | +| `operations.images.convertToInline.available` | boolean | yes | | +| `operations.images.convertToInline.dryRun` | boolean | yes | | +| `operations.images.convertToInline.reasons` | enum[] | no | | +| `operations.images.convertToInline.tracked` | boolean | yes | | +| `operations.images.delete` | object | yes | | +| `operations.images.delete.available` | boolean | yes | | +| `operations.images.delete.dryRun` | boolean | yes | | +| `operations.images.delete.reasons` | enum[] | no | | +| `operations.images.delete.tracked` | boolean | yes | | +| `operations.images.get` | object | yes | | +| `operations.images.get.available` | boolean | yes | | +| `operations.images.get.dryRun` | boolean | yes | | +| `operations.images.get.reasons` | enum[] | no | | +| `operations.images.get.tracked` | boolean | yes | | +| `operations.images.list` | object | yes | | +| `operations.images.list.available` | boolean | yes | | +| `operations.images.list.dryRun` | boolean | yes | | +| `operations.images.list.reasons` | enum[] | no | | +| `operations.images.list.tracked` | boolean | yes | | +| `operations.images.move` | object | yes | | +| `operations.images.move.available` | boolean | yes | | +| `operations.images.move.dryRun` | boolean | yes | | +| `operations.images.move.reasons` | enum[] | no | | +| `operations.images.move.tracked` | boolean | yes | | +| `operations.images.setAnchorOptions` | object | yes | | +| `operations.images.setAnchorOptions.available` | boolean | yes | | +| `operations.images.setAnchorOptions.dryRun` | boolean | yes | | +| `operations.images.setAnchorOptions.reasons` | enum[] | no | | +| `operations.images.setAnchorOptions.tracked` | boolean | yes | | +| `operations.images.setPosition` | object | yes | | +| `operations.images.setPosition.available` | boolean | yes | | +| `operations.images.setPosition.dryRun` | boolean | yes | | +| `operations.images.setPosition.reasons` | enum[] | no | | +| `operations.images.setPosition.tracked` | boolean | yes | | +| `operations.images.setSize` | object | yes | | +| `operations.images.setSize.available` | boolean | yes | | +| `operations.images.setSize.dryRun` | boolean | yes | | +| `operations.images.setSize.reasons` | enum[] | no | | +| `operations.images.setSize.tracked` | boolean | yes | | +| `operations.images.setWrapDistances` | object | yes | | +| `operations.images.setWrapDistances.available` | boolean | yes | | +| `operations.images.setWrapDistances.dryRun` | boolean | yes | | +| `operations.images.setWrapDistances.reasons` | enum[] | no | | +| `operations.images.setWrapDistances.tracked` | boolean | yes | | +| `operations.images.setWrapSide` | object | yes | | +| `operations.images.setWrapSide.available` | boolean | yes | | +| `operations.images.setWrapSide.dryRun` | boolean | yes | | +| `operations.images.setWrapSide.reasons` | enum[] | no | | +| `operations.images.setWrapSide.tracked` | boolean | yes | | +| `operations.images.setWrapType` | object | yes | | +| `operations.images.setWrapType.available` | boolean | yes | | +| `operations.images.setWrapType.dryRun` | boolean | yes | | +| `operations.images.setWrapType.reasons` | enum[] | no | | +| `operations.images.setWrapType.tracked` | boolean | yes | | +| `operations.images.setZOrder` | object | yes | | +| `operations.images.setZOrder.available` | boolean | yes | | +| `operations.images.setZOrder.dryRun` | boolean | yes | | +| `operations.images.setZOrder.reasons` | enum[] | no | | +| `operations.images.setZOrder.tracked` | boolean | yes | | | `operations.info` | object | yes | | | `operations.info.available` | boolean | yes | | | `operations.info.dryRun` | boolean | yes | | @@ -1552,6 +1622,14 @@ _No fields._ ], "tracked": true }, + "create.image": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, "create.paragraph": { "available": true, "dryRun": true, @@ -2152,6 +2230,110 @@ _No fields._ ], "tracked": true }, + "images.convertToFloating": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "images.convertToInline": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "images.delete": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "images.get": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "images.list": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "images.move": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "images.setAnchorOptions": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "images.setPosition": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "images.setSize": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "images.setWrapDistances": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "images.setWrapSide": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "images.setWrapType": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "images.setZOrder": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, "info": { "available": true, "dryRun": true, @@ -4897,6 +5079,41 @@ _No fields._ ], "type": "object" }, + "create.image": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, "create.paragraph": { "additionalProperties": false, "properties": { @@ -7522,6 +7739,461 @@ _No fields._ ], "type": "object" }, + "images.convertToFloating": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "images.convertToInline": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "images.delete": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "images.get": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "images.list": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "images.move": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "images.setAnchorOptions": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "images.setPosition": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "images.setSize": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "images.setWrapDistances": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "images.setWrapSide": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "images.setWrapType": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "images.setZOrder": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, "info": { "additionalProperties": false, "properties": { @@ -11170,7 +11842,21 @@ _No fields._ "toc.editEntry", "history.get", "history.undo", - "history.redo" + "history.redo", + "create.image", + "images.list", + "images.get", + "images.delete", + "images.move", + "images.convertToInline", + "images.convertToFloating", + "images.setSize", + "images.setWrapType", + "images.setWrapSide", + "images.setWrapDistances", + "images.setPosition", + "images.setAnchorOptions", + "images.setZOrder" ], "type": "object" }, diff --git a/apps/docs/document-api/reference/create/image.mdx b/apps/docs/document-api/reference/create/image.mdx new file mode 100644 index 0000000000..7a8ef32a4d --- /dev/null +++ b/apps/docs/document-api/reference/create/image.mdx @@ -0,0 +1,270 @@ +--- +title: create.image +sidebarTitle: create.image +description: Insert a new image at the target position. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Insert a new image at the target position. + +- Operation ID: `create.image` +- API member path: `editor.doc.create.image(...)` +- Mutates document: `yes` +- Idempotency: `non-idempotent` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a CreateImageResult with the new image address. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `alt` | string | no | | +| `at` | object(kind="documentStart") \\| object(kind="documentEnd") \\| object(kind="before") \\| object(kind="after") \\| object(kind="inParagraph") | no | One of: object(kind="documentStart"), object(kind="documentEnd"), object(kind="before"), object(kind="after"), object(kind="inParagraph") | +| `size` | object | no | | +| `size.height` | number | no | | +| `size.width` | number | no | | +| `src` | string | yes | | +| `title` | string | no | | + +### Example request + +```json +{ + "alt": "example", + "src": "example", + "title": "example" +} +``` + +## Output fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `failure` | object | no | | +| `image` | object | no | | +| `success` | boolean | no | | + +### Example response + +```json +{ + "image": {}, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "alt": { + "type": "string" + }, + "at": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "documentStart" + } + }, + "required": [ + "kind" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "documentEnd" + } + }, + "required": [ + "kind" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "before" + }, + "target": { + "$ref": "#/$defs/BlockNodeAddress" + } + }, + "required": [ + "kind", + "target" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "after" + }, + "target": { + "$ref": "#/$defs/BlockNodeAddress" + } + }, + "required": [ + "kind", + "target" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "inParagraph" + }, + "offset": { + "type": "integer" + }, + "target": { + "$ref": "#/$defs/BlockNodeAddress" + } + }, + "required": [ + "kind", + "target" + ], + "type": "object" + } + ] + }, + "size": { + "additionalProperties": false, + "properties": { + "height": { + "type": "number" + }, + "width": { + "type": "number" + } + }, + "type": "object" + }, + "src": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": [ + "src" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "type": "object" + }, + "image": { + "type": "object" + }, + "success": { + "type": "boolean" + } + }, + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "image": { + "type": "object" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "image" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET", + "INVALID_INPUT" + ] + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/create/index.mdx b/apps/docs/document-api/reference/create/index.mdx index ec8432c8bb..e6a2a33d7a 100644 --- a/apps/docs/document-api/reference/create/index.mdx +++ b/apps/docs/document-api/reference/create/index.mdx @@ -19,4 +19,5 @@ Structured creation helpers. | create.sectionBreak | `create.sectionBreak` | Yes | `non-idempotent` | No | Yes | | create.table | `create.table` | Yes | `non-idempotent` | Yes | Yes | | create.tableOfContents | `create.tableOfContents` | Yes | `non-idempotent` | No | Yes | +| create.image | `create.image` | Yes | `non-idempotent` | No | Yes | diff --git a/apps/docs/document-api/reference/images/convert-to-floating.mdx b/apps/docs/document-api/reference/images/convert-to-floating.mdx new file mode 100644 index 0000000000..40acf5c575 --- /dev/null +++ b/apps/docs/document-api/reference/images/convert-to-floating.mdx @@ -0,0 +1,161 @@ +--- +title: images.convertToFloating +sidebarTitle: images.convertToFloating +description: Convert an inline image to floating placement. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Convert an inline image to floating placement. + +- Operation ID: `images.convertToFloating` +- API member path: `editor.doc.images.convertToFloating(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns an ImagesMutationResult; reports NO_OP if already floating. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `imageId` | string | yes | | + +### Example request + +```json +{ + "imageId": "example" +} +``` + +## Output fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `failure` | object | no | | +| `image` | object | no | | +| `success` | boolean | no | | + +### Example response + +```json +{ + "image": {}, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "imageId": { + "type": "string" + } + }, + "required": [ + "imageId" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "type": "object" + }, + "image": { + "type": "object" + }, + "success": { + "type": "boolean" + } + }, + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "image": { + "type": "object" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "image" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/images/convert-to-inline.mdx b/apps/docs/document-api/reference/images/convert-to-inline.mdx new file mode 100644 index 0000000000..682c420ca2 --- /dev/null +++ b/apps/docs/document-api/reference/images/convert-to-inline.mdx @@ -0,0 +1,161 @@ +--- +title: images.convertToInline +sidebarTitle: images.convertToInline +description: Convert a floating image to inline placement. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Convert a floating image to inline placement. + +- Operation ID: `images.convertToInline` +- API member path: `editor.doc.images.convertToInline(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns an ImagesMutationResult; reports NO_OP if already inline. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `imageId` | string | yes | | + +### Example request + +```json +{ + "imageId": "example" +} +``` + +## Output fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `failure` | object | no | | +| `image` | object | no | | +| `success` | boolean | no | | + +### Example response + +```json +{ + "image": {}, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "imageId": { + "type": "string" + } + }, + "required": [ + "imageId" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "type": "object" + }, + "image": { + "type": "object" + }, + "success": { + "type": "boolean" + } + }, + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "image": { + "type": "object" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "image" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/images/delete.mdx b/apps/docs/document-api/reference/images/delete.mdx new file mode 100644 index 0000000000..2c44aad77b --- /dev/null +++ b/apps/docs/document-api/reference/images/delete.mdx @@ -0,0 +1,161 @@ +--- +title: images.delete +sidebarTitle: images.delete +description: Delete an image from the document. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Delete an image from the document. + +- Operation ID: `images.delete` +- API member path: `editor.doc.images.delete(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns an ImagesMutationResult indicating success or failure. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `imageId` | string | yes | | + +### Example request + +```json +{ + "imageId": "example" +} +``` + +## Output fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `failure` | object | no | | +| `image` | object | no | | +| `success` | boolean | no | | + +### Example response + +```json +{ + "image": {}, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "imageId": { + "type": "string" + } + }, + "required": [ + "imageId" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "type": "object" + }, + "image": { + "type": "object" + }, + "success": { + "type": "boolean" + } + }, + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "image": { + "type": "object" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "image" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/images/get.mdx b/apps/docs/document-api/reference/images/get.mdx new file mode 100644 index 0000000000..ddbfccfbb5 --- /dev/null +++ b/apps/docs/document-api/reference/images/get.mdx @@ -0,0 +1,85 @@ +--- +title: images.get +sidebarTitle: images.get +description: Get details for a specific image by its stable ID. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Get details for a specific image by its stable ID. + +- Operation ID: `images.get` +- API member path: `editor.doc.images.get(...)` +- Mutates document: `no` +- Idempotency: `idempotent` +- Supports tracked mode: `no` +- Supports dry run: `no` +- Deterministic target resolution: `yes` + +## Expected result + +Returns an ImageSummary with full image properties. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `imageId` | string | yes | | + +### Example request + +```json +{ + "imageId": "example" +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` + +## Non-applied failure codes + +- None + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "imageId": { + "type": "string" + } + }, + "required": [ + "imageId" + ], + "type": "object" +} +``` + + + +```json +{ + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/images/index.mdx b/apps/docs/document-api/reference/images/index.mdx new file mode 100644 index 0000000000..3cc8366309 --- /dev/null +++ b/apps/docs/document-api/reference/images/index.mdx @@ -0,0 +1,30 @@ +--- +title: Images operations +sidebarTitle: Images +description: Images operation reference from the canonical Document API contract. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +[Back to full reference](../index) + +Image lifecycle, placement, and wrap configuration. + +| Operation | Member path | Mutates | Idempotency | Tracked | Dry run | +| --- | --- | --- | --- | --- | --- | +| images.list | `images.list` | No | `idempotent` | No | No | +| images.get | `images.get` | No | `idempotent` | No | No | +| images.delete | `images.delete` | Yes | `conditional` | No | Yes | +| images.move | `images.move` | Yes | `non-idempotent` | No | Yes | +| images.convertToInline | `images.convertToInline` | Yes | `conditional` | No | Yes | +| images.convertToFloating | `images.convertToFloating` | Yes | `conditional` | No | Yes | +| images.setSize | `images.setSize` | Yes | `conditional` | No | Yes | +| images.setWrapType | `images.setWrapType` | Yes | `conditional` | No | Yes | +| images.setWrapSide | `images.setWrapSide` | Yes | `conditional` | No | Yes | +| images.setWrapDistances | `images.setWrapDistances` | Yes | `conditional` | No | Yes | +| images.setPosition | `images.setPosition` | Yes | `conditional` | No | Yes | +| images.setAnchorOptions | `images.setAnchorOptions` | Yes | `conditional` | No | Yes | +| images.setZOrder | `images.setZOrder` | Yes | `conditional` | No | Yes | + diff --git a/apps/docs/document-api/reference/images/list.mdx b/apps/docs/document-api/reference/images/list.mdx new file mode 100644 index 0000000000..416d0ea073 --- /dev/null +++ b/apps/docs/document-api/reference/images/list.mdx @@ -0,0 +1,110 @@ +--- +title: images.list +sidebarTitle: images.list +description: List all images in the document. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +List all images in the document. + +- Operation ID: `images.list` +- API member path: `editor.doc.images.list(...)` +- Mutates document: `no` +- Idempotency: `idempotent` +- Supports tracked mode: `no` +- Supports dry run: `no` +- Deterministic target resolution: `yes` + +## Expected result + +Returns an ImagesListResult with total count and image summaries. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `limit` | integer | no | | +| `offset` | integer | no | | + +### Example request + +```json +{ + "limit": 50, + "offset": 0 +} +``` + +## Output fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `items` | object[] | yes | | +| `total` | integer | yes | | + +### Example response + +```json +{ + "items": [ + {} + ], + "total": 1 +} +``` + +## Pre-apply throws + +- None + +## Non-applied failure codes + +- None + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + } + }, + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "items": { + "items": { + "type": "object" + }, + "type": "array" + }, + "total": { + "type": "integer" + } + }, + "required": [ + "total", + "items" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/images/move.mdx b/apps/docs/document-api/reference/images/move.mdx new file mode 100644 index 0000000000..aa0739fb69 --- /dev/null +++ b/apps/docs/document-api/reference/images/move.mdx @@ -0,0 +1,245 @@ +--- +title: images.move +sidebarTitle: images.move +description: Move an image to a new location in the document. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Move an image to a new location in the document. + +- Operation ID: `images.move` +- API member path: `editor.doc.images.move(...)` +- Mutates document: `yes` +- Idempotency: `non-idempotent` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns an ImagesMutationResult indicating success or failure. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `imageId` | string | yes | | +| `to` | object(kind="documentStart") \\| object(kind="documentEnd") \\| object(kind="before") \\| object(kind="after") \\| object(kind="inParagraph") | yes | One of: object(kind="documentStart"), object(kind="documentEnd"), object(kind="before"), object(kind="after"), object(kind="inParagraph") | + +### Example request + +```json +{ + "imageId": "example", + "to": { + "kind": "documentStart" + } +} +``` + +## Output fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `failure` | object | no | | +| `image` | object | no | | +| `success` | boolean | no | | + +### Example response + +```json +{ + "image": {}, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "imageId": { + "type": "string" + }, + "to": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "documentStart" + } + }, + "required": [ + "kind" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "documentEnd" + } + }, + "required": [ + "kind" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "before" + }, + "target": { + "$ref": "#/$defs/BlockNodeAddress" + } + }, + "required": [ + "kind", + "target" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "after" + }, + "target": { + "$ref": "#/$defs/BlockNodeAddress" + } + }, + "required": [ + "kind", + "target" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "inParagraph" + }, + "offset": { + "type": "integer" + }, + "target": { + "$ref": "#/$defs/BlockNodeAddress" + } + }, + "required": [ + "kind", + "target" + ], + "type": "object" + } + ] + } + }, + "required": [ + "imageId", + "to" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "type": "object" + }, + "image": { + "type": "object" + }, + "success": { + "type": "boolean" + } + }, + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "image": { + "type": "object" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "image" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/images/set-anchor-options.mdx b/apps/docs/document-api/reference/images/set-anchor-options.mdx new file mode 100644 index 0000000000..88b2c4579a --- /dev/null +++ b/apps/docs/document-api/reference/images/set-anchor-options.mdx @@ -0,0 +1,193 @@ +--- +title: images.setAnchorOptions +sidebarTitle: images.setAnchorOptions +description: Set anchor behavior options for a floating image. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set anchor behavior options for a floating image. + +- Operation ID: `images.setAnchorOptions` +- API member path: `editor.doc.images.setAnchorOptions(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns an ImagesMutationResult. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `imageId` | string | yes | | +| `options` | object | yes | | +| `options.allowOverlap` | boolean | no | | +| `options.behindDoc` | boolean | no | | +| `options.layoutInCell` | boolean | no | | +| `options.lockAnchor` | boolean | no | | +| `options.simplePos` | boolean | no | | + +### Example request + +```json +{ + "imageId": "example", + "options": { + "allowOverlap": true, + "behindDoc": true + } +} +``` + +## Output fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `failure` | object | no | | +| `image` | object | no | | +| `success` | boolean | no | | + +### Example response + +```json +{ + "image": {}, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "imageId": { + "type": "string" + }, + "options": { + "additionalProperties": false, + "properties": { + "allowOverlap": { + "type": "boolean" + }, + "behindDoc": { + "type": "boolean" + }, + "layoutInCell": { + "type": "boolean" + }, + "lockAnchor": { + "type": "boolean" + }, + "simplePos": { + "type": "boolean" + } + }, + "type": "object" + } + }, + "required": [ + "imageId", + "options" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "type": "object" + }, + "image": { + "type": "object" + }, + "success": { + "type": "boolean" + } + }, + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "image": { + "type": "object" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "image" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/images/set-position.mdx b/apps/docs/document-api/reference/images/set-position.mdx new file mode 100644 index 0000000000..2b160075cf --- /dev/null +++ b/apps/docs/document-api/reference/images/set-position.mdx @@ -0,0 +1,204 @@ +--- +title: images.setPosition +sidebarTitle: images.setPosition +description: Set the anchor position for a floating image. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set the anchor position for a floating image. + +- Operation ID: `images.setPosition` +- API member path: `editor.doc.images.setPosition(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns an ImagesMutationResult. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `imageId` | string | yes | | +| `position` | object | yes | | +| `position.alignH` | string | no | | +| `position.alignV` | string | no | | +| `position.hRelativeFrom` | string | no | | +| `position.marginOffset` | object | no | | +| `position.marginOffset.horizontal` | number | no | | +| `position.marginOffset.top` | number | no | | +| `position.vRelativeFrom` | string | no | | + +### Example request + +```json +{ + "imageId": "example", + "position": { + "hRelativeFrom": "example", + "vRelativeFrom": "example" + } +} +``` + +## Output fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `failure` | object | no | | +| `image` | object | no | | +| `success` | boolean | no | | + +### Example response + +```json +{ + "image": {}, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "imageId": { + "type": "string" + }, + "position": { + "additionalProperties": false, + "properties": { + "alignH": { + "type": "string" + }, + "alignV": { + "type": "string" + }, + "hRelativeFrom": { + "type": "string" + }, + "marginOffset": { + "additionalProperties": false, + "properties": { + "horizontal": { + "type": "number" + }, + "top": { + "type": "number" + } + }, + "type": "object" + }, + "vRelativeFrom": { + "type": "string" + } + }, + "type": "object" + } + }, + "required": [ + "imageId", + "position" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "type": "object" + }, + "image": { + "type": "object" + }, + "success": { + "type": "boolean" + } + }, + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "image": { + "type": "object" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "image" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/images/set-size.mdx b/apps/docs/document-api/reference/images/set-size.mdx new file mode 100644 index 0000000000..babfa810f4 --- /dev/null +++ b/apps/docs/document-api/reference/images/set-size.mdx @@ -0,0 +1,198 @@ +--- +title: images.setSize +sidebarTitle: images.setSize +description: Set explicit width/height for an image. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set explicit width/height for an image. + +- Operation ID: `images.setSize` +- API member path: `editor.doc.images.setSize(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns an ImagesMutationResult; reports NO_OP if the size already matches. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `imageId` | string | yes | | +| `size` | object | yes | | +| `size.height` | number | yes | | +| `size.unit` | enum | no | `"px"`, `"pt"`, `"twip"` | +| `size.width` | number | yes | | + +### Example request + +```json +{ + "imageId": "example", + "size": { + "height": 12.5, + "unit": "px", + "width": 12.5 + } +} +``` + +## Output fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `failure` | object | no | | +| `image` | object | no | | +| `success` | boolean | no | | + +### Example response + +```json +{ + "image": {}, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "imageId": { + "type": "string" + }, + "size": { + "additionalProperties": false, + "properties": { + "height": { + "exclusiveMinimum": 0, + "type": "number" + }, + "unit": { + "enum": [ + "px", + "pt", + "twip" + ], + "type": "string" + }, + "width": { + "exclusiveMinimum": 0, + "type": "number" + } + }, + "required": [ + "width", + "height" + ], + "type": "object" + } + }, + "required": [ + "imageId", + "size" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "type": "object" + }, + "image": { + "type": "object" + }, + "success": { + "type": "boolean" + } + }, + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "image": { + "type": "object" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "image" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/images/set-wrap-distances.mdx b/apps/docs/document-api/reference/images/set-wrap-distances.mdx new file mode 100644 index 0000000000..8c6c48af9b --- /dev/null +++ b/apps/docs/document-api/reference/images/set-wrap-distances.mdx @@ -0,0 +1,189 @@ +--- +title: images.setWrapDistances +sidebarTitle: images.setWrapDistances +description: Set the text-wrap distance margins for a floating image. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set the text-wrap distance margins for a floating image. + +- Operation ID: `images.setWrapDistances` +- API member path: `editor.doc.images.setWrapDistances(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns an ImagesMutationResult; reports NO_OP if already set. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `distances` | object | yes | | +| `distances.distBottom` | number | no | | +| `distances.distLeft` | number | no | | +| `distances.distRight` | number | no | | +| `distances.distTop` | number | no | | +| `imageId` | string | yes | | + +### Example request + +```json +{ + "distances": { + "distBottom": 12.5, + "distTop": 12.5 + }, + "imageId": "example" +} +``` + +## Output fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `failure` | object | no | | +| `image` | object | no | | +| `success` | boolean | no | | + +### Example response + +```json +{ + "image": {}, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "distances": { + "additionalProperties": false, + "properties": { + "distBottom": { + "type": "number" + }, + "distLeft": { + "type": "number" + }, + "distRight": { + "type": "number" + }, + "distTop": { + "type": "number" + } + }, + "type": "object" + }, + "imageId": { + "type": "string" + } + }, + "required": [ + "imageId", + "distances" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "type": "object" + }, + "image": { + "type": "object" + }, + "success": { + "type": "boolean" + } + }, + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "image": { + "type": "object" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "image" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/images/set-wrap-side.mdx b/apps/docs/document-api/reference/images/set-wrap-side.mdx new file mode 100644 index 0000000000..2e2ec081da --- /dev/null +++ b/apps/docs/document-api/reference/images/set-wrap-side.mdx @@ -0,0 +1,173 @@ +--- +title: images.setWrapSide +sidebarTitle: images.setWrapSide +description: Set which side(s) text wraps around a floating image. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set which side(s) text wraps around a floating image. + +- Operation ID: `images.setWrapSide` +- API member path: `editor.doc.images.setWrapSide(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns an ImagesMutationResult; reports NO_OP if already set. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `imageId` | string | yes | | +| `side` | enum | yes | `"bothSides"`, `"left"`, `"right"`, `"largest"` | + +### Example request + +```json +{ + "imageId": "example", + "side": "bothSides" +} +``` + +## Output fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `failure` | object | no | | +| `image` | object | no | | +| `success` | boolean | no | | + +### Example response + +```json +{ + "image": {}, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "imageId": { + "type": "string" + }, + "side": { + "enum": [ + "bothSides", + "left", + "right", + "largest" + ], + "type": "string" + } + }, + "required": [ + "imageId", + "side" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "type": "object" + }, + "image": { + "type": "object" + }, + "success": { + "type": "boolean" + } + }, + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "image": { + "type": "object" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "image" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/images/set-wrap-type.mdx b/apps/docs/document-api/reference/images/set-wrap-type.mdx new file mode 100644 index 0000000000..ce54acbff4 --- /dev/null +++ b/apps/docs/document-api/reference/images/set-wrap-type.mdx @@ -0,0 +1,175 @@ +--- +title: images.setWrapType +sidebarTitle: images.setWrapType +description: Set the text wrapping type for a floating image. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set the text wrapping type for a floating image. + +- Operation ID: `images.setWrapType` +- API member path: `editor.doc.images.setWrapType(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns an ImagesMutationResult; reports NO_OP if already set. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `imageId` | string | yes | | +| `type` | enum | yes | `"None"`, `"Square"`, `"Through"`, `"Tight"`, `"TopAndBottom"`, `"Inline"` | + +### Example request + +```json +{ + "imageId": "example", + "type": "None" +} +``` + +## Output fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `failure` | object | no | | +| `image` | object | no | | +| `success` | boolean | no | | + +### Example response + +```json +{ + "image": {}, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "imageId": { + "type": "string" + }, + "type": { + "enum": [ + "None", + "Square", + "Through", + "Tight", + "TopAndBottom", + "Inline" + ], + "type": "string" + } + }, + "required": [ + "imageId", + "type" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "type": "object" + }, + "image": { + "type": "object" + }, + "success": { + "type": "boolean" + } + }, + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "image": { + "type": "object" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "image" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/images/set-z-order.mdx b/apps/docs/document-api/reference/images/set-z-order.mdx new file mode 100644 index 0000000000..5c073522e2 --- /dev/null +++ b/apps/docs/document-api/reference/images/set-z-order.mdx @@ -0,0 +1,179 @@ +--- +title: images.setZOrder +sidebarTitle: images.setZOrder +description: Set the z-order (relativeHeight) for a floating image. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set the z-order (relativeHeight) for a floating image. + +- Operation ID: `images.setZOrder` +- API member path: `editor.doc.images.setZOrder(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns an ImagesMutationResult. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `imageId` | string | yes | | +| `zOrder` | object | yes | | +| `zOrder.relativeHeight` | number | yes | | + +### Example request + +```json +{ + "imageId": "example", + "zOrder": { + "relativeHeight": 12.5 + } +} +``` + +## Output fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `failure` | object | no | | +| `image` | object | no | | +| `success` | boolean | no | | + +### Example response + +```json +{ + "image": {}, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "imageId": { + "type": "string" + }, + "zOrder": { + "additionalProperties": false, + "properties": { + "relativeHeight": { + "type": "number" + } + }, + "required": [ + "relativeHeight" + ], + "type": "object" + } + }, + "required": [ + "imageId", + "zOrder" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "type": "object" + }, + "image": { + "type": "object" + }, + "success": { + "type": "boolean" + } + }, + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "image": { + "type": "object" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "image" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/index.mdx b/apps/docs/document-api/reference/index.mdx index d30b11c1ed..2965a67cfd 100644 --- a/apps/docs/document-api/reference/index.mdx +++ b/apps/docs/document-api/reference/index.mdx @@ -23,7 +23,7 @@ Document API is currently alpha and subject to breaking changes. | Core | 10 | 0 | 10 | [Open](/document-api/reference/core/index) | | Blocks | 1 | 0 | 1 | [Open](/document-api/reference/blocks/index) | | Capabilities | 1 | 0 | 1 | [Open](/document-api/reference/capabilities/index) | -| Create | 5 | 0 | 5 | [Open](/document-api/reference/create/index) | +| Create | 6 | 0 | 6 | [Open](/document-api/reference/create/index) | | Sections | 18 | 0 | 18 | [Open](/document-api/reference/sections/index) | | Format | 44 | 1 | 45 | [Open](/document-api/reference/format/index) | | Styles | 1 | 0 | 1 | [Open](/document-api/reference/styles/index) | @@ -37,6 +37,7 @@ Document API is currently alpha and subject to breaking changes. | Tables | 42 | 0 | 42 | [Open](/document-api/reference/tables/index) | | History | 3 | 0 | 3 | [Open](/document-api/reference/history/index) | | Table of Contents | 10 | 0 | 10 | [Open](/document-api/reference/toc/index) | +| Images | 13 | 0 | 13 | [Open](/document-api/reference/images/index) | ## Available operations @@ -78,6 +79,7 @@ The tables below are grouped by namespace. | create.sectionBreak | editor.doc.create.sectionBreak(...) | Create a section break at the target location with optional initial section properties. | | create.table | editor.doc.create.table(...) | Create a new table at the target position. | | create.tableOfContents | editor.doc.create.tableOfContents(...) | Insert a new table of contents at the target position. | +| create.image | editor.doc.create.image(...) | Insert a new image at the target position. | #### Sections @@ -309,3 +311,21 @@ The tables below are grouped by namespace. | toc.listEntries | editor.doc.toc.listEntries(...) | List all TC (table of contents entry) fields in the document body. | | toc.getEntry | editor.doc.toc.getEntry(...) | Retrieve details of a specific TC (table of contents entry) field. | | toc.editEntry | editor.doc.toc.editEntry(...) | Update the properties of a TC (table of contents entry) field. | + +#### Images + +| Operation | API member path | Description | +| --- | --- | --- | +| images.list | editor.doc.images.list(...) | List all images in the document. | +| images.get | editor.doc.images.get(...) | Get details for a specific image by its stable ID. | +| images.delete | editor.doc.images.delete(...) | Delete an image from the document. | +| images.move | editor.doc.images.move(...) | Move an image to a new location in the document. | +| images.convertToInline | editor.doc.images.convertToInline(...) | Convert a floating image to inline placement. | +| images.convertToFloating | editor.doc.images.convertToFloating(...) | Convert an inline image to floating placement. | +| images.setSize | editor.doc.images.setSize(...) | Set explicit width/height for an image. | +| images.setWrapType | editor.doc.images.setWrapType(...) | Set the text wrapping type for a floating image. | +| images.setWrapSide | editor.doc.images.setWrapSide(...) | Set which side(s) text wraps around a floating image. | +| images.setWrapDistances | editor.doc.images.setWrapDistances(...) | Set the text-wrap distance margins for a floating image. | +| images.setPosition | editor.doc.images.setPosition(...) | Set the anchor position for a floating image. | +| images.setAnchorOptions | editor.doc.images.setAnchorOptions(...) | Set anchor behavior options for a floating image. | +| images.setZOrder | editor.doc.images.setZOrder(...) | Set the z-order (relativeHeight) for a floating image. | diff --git a/apps/docs/document-engine/sdks.mdx b/apps/docs/document-engine/sdks.mdx index 55c94d4d43..6737703e8b 100644 --- a/apps/docs/document-engine/sdks.mdx +++ b/apps/docs/document-engine/sdks.mdx @@ -474,6 +474,7 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p | `doc.create.sectionBreak` | `create section-break` | Create a section break at the target location with optional initial section properties. | | `doc.create.table` | `create table` | Create a new table at the target position. | | `doc.create.tableOfContents` | `create table-of-contents` | Insert a new table of contents at the target position. | +| `doc.create.image` | `create image` | Insert a new image at the target position. | #### Sections @@ -637,6 +638,24 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p | `doc.describe` | `describe` | List all available CLI operations and contract metadata. | | `doc.describeCommand` | `describe command` | Show detailed metadata for a single CLI operation. | +#### Images + +| Operation | CLI command | Description | +| --- | --- | --- | +| `doc.images.list` | `images list` | List all images in the document. | +| `doc.images.get` | `images get` | Get details for a specific image by its stable ID. | +| `doc.images.delete` | `images delete` | Delete an image from the document. | +| `doc.images.move` | `images move` | Move an image to a new location in the document. | +| `doc.images.convertToInline` | `images convert-to-inline` | Convert a floating image to inline placement. | +| `doc.images.convertToFloating` | `images convert-to-floating` | Convert an inline image to floating placement. | +| `doc.images.setSize` | `images set-size` | Set explicit width/height for an image. | +| `doc.images.setWrapType` | `images set-wrap-type` | Set the text wrapping type for a floating image. | +| `doc.images.setWrapSide` | `images set-wrap-side` | Set which side(s) text wraps around a floating image. | +| `doc.images.setWrapDistances` | `images set-wrap-distances` | Set the text-wrap distance margins for a floating image. | +| `doc.images.setPosition` | `images set-position` | Set the anchor position for a floating image. | +| `doc.images.setAnchorOptions` | `images set-anchor-options` | Set anchor behavior options for a floating image. | +| `doc.images.setZOrder` | `images set-z-order` | Set the z-order (relativeHeight) for a floating image. | + @@ -764,6 +783,7 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p | `doc.create.section_break` | `create section-break` | Create a section break at the target location with optional initial section properties. | | `doc.create.table` | `create table` | Create a new table at the target position. | | `doc.create.table_of_contents` | `create table-of-contents` | Insert a new table of contents at the target position. | +| `doc.create.image` | `create image` | Insert a new image at the target position. | #### Sections @@ -927,6 +947,24 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p | `doc.describe` | `describe` | List all available CLI operations and contract metadata. | | `doc.describe_command` | `describe command` | Show detailed metadata for a single CLI operation. | +#### Images + +| Operation | CLI command | Description | +| --- | --- | --- | +| `doc.images.list` | `images list` | List all images in the document. | +| `doc.images.get` | `images get` | Get details for a specific image by its stable ID. | +| `doc.images.delete` | `images delete` | Delete an image from the document. | +| `doc.images.move` | `images move` | Move an image to a new location in the document. | +| `doc.images.convert_to_inline` | `images convert-to-inline` | Convert a floating image to inline placement. | +| `doc.images.convert_to_floating` | `images convert-to-floating` | Convert an inline image to floating placement. | +| `doc.images.set_size` | `images set-size` | Set explicit width/height for an image. | +| `doc.images.set_wrap_type` | `images set-wrap-type` | Set the text wrapping type for a floating image. | +| `doc.images.set_wrap_side` | `images set-wrap-side` | Set which side(s) text wraps around a floating image. | +| `doc.images.set_wrap_distances` | `images set-wrap-distances` | Set the text-wrap distance margins for a floating image. | +| `doc.images.set_position` | `images set-position` | Set the anchor position for a floating image. | +| `doc.images.set_anchor_options` | `images set-anchor-options` | Set anchor behavior options for a floating image. | +| `doc.images.set_z_order` | `images set-z-order` | Set the z-order (relativeHeight) for a floating image. | + {/* SDK_OPERATIONS_END */} diff --git a/packages/document-api/src/contract/contract.test.ts b/packages/document-api/src/contract/contract.test.ts index 29cf571098..2088f07643 100644 --- a/packages/document-api/src/contract/contract.test.ts +++ b/packages/document-api/src/contract/contract.test.ts @@ -150,6 +150,7 @@ describe('document-api contract catalog', () => { 'tables', 'history', 'toc', + 'images', ]; for (const id of OPERATION_IDS) { expect(validGroups, `${id} has invalid referenceGroup`).toContain(OPERATION_DEFINITIONS[id].referenceGroup); diff --git a/packages/document-api/src/contract/operation-definitions.ts b/packages/document-api/src/contract/operation-definitions.ts index f786cf38fa..ee97c1764b 100644 --- a/packages/document-api/src/contract/operation-definitions.ts +++ b/packages/document-api/src/contract/operation-definitions.ts @@ -49,7 +49,8 @@ export type ReferenceGroupKey = | 'mutations' | 'tables' | 'history' - | 'toc'; + | 'toc' + | 'images'; // --------------------------------------------------------------------------- // Entry shape @@ -150,6 +151,9 @@ const T_PLAN_ENGINE = [ const T_NOT_FOUND_COMMAND = ['TARGET_NOT_FOUND', 'INVALID_TARGET', 'CAPABILITY_UNAVAILABLE'] as const; const T_NOT_FOUND_COMMAND_TRACKED = [...T_NOT_FOUND_COMMAND] as const; +// Image operations can throw AMBIGUOUS_TARGET when multiple images share an sdImageId. +const T_IMAGE_COMMAND = ['TARGET_NOT_FOUND', 'AMBIGUOUS_TARGET', 'INVALID_TARGET', 'CAPABILITY_UNAVAILABLE'] as const; + const T_QUERY_MATCH = ['MATCH_NOT_FOUND', 'AMBIGUOUS_MATCH', 'INVALID_INPUT', 'INTERNAL_ERROR'] as const; const T_SECTION_CREATE = [ 'TARGET_NOT_FOUND', @@ -2340,6 +2344,230 @@ export const OPERATION_DEFINITIONS = { referenceDocPath: 'history/redo.mdx', referenceGroup: 'history', }, + + // ------------------------------------------------------------------------- + // Create: image + // ------------------------------------------------------------------------- + + 'create.image': { + memberPath: 'create.image', + description: 'Insert a new image at the target position.', + expectedResult: 'Returns a CreateImageResult with the new image address.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'non-idempotent', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['INVALID_TARGET', 'INVALID_INPUT'], + throws: [...T_NOT_FOUND_COMMAND, 'INVALID_INPUT'], + }), + referenceDocPath: 'create/image.mdx', + referenceGroup: 'create', + }, + + // ------------------------------------------------------------------------- + // Images: lifecycle + placement + // ------------------------------------------------------------------------- + + 'images.list': { + memberPath: 'images.list', + description: 'List all images in the document.', + expectedResult: 'Returns an ImagesListResult with total count and image summaries.', + requiresDocumentContext: true, + metadata: readOperation({ idempotency: 'idempotent', deterministicTargetResolution: true }), + referenceDocPath: 'images/list.mdx', + referenceGroup: 'images', + }, + + 'images.get': { + memberPath: 'images.get', + description: 'Get details for a specific image by its stable ID.', + expectedResult: 'Returns an ImageSummary with full image properties.', + requiresDocumentContext: true, + metadata: readOperation({ + idempotency: 'idempotent', + throws: ['TARGET_NOT_FOUND', 'AMBIGUOUS_TARGET'], + deterministicTargetResolution: true, + }), + referenceDocPath: 'images/get.mdx', + referenceGroup: 'images', + }, + + 'images.delete': { + memberPath: 'images.delete', + description: 'Delete an image from the document.', + expectedResult: 'Returns an ImagesMutationResult indicating success or failure.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_IMAGE_COMMAND, + }), + referenceDocPath: 'images/delete.mdx', + referenceGroup: 'images', + }, + + 'images.move': { + memberPath: 'images.move', + description: 'Move an image to a new location in the document.', + expectedResult: 'Returns an ImagesMutationResult indicating success or failure.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'non-idempotent', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['INVALID_TARGET'], + throws: T_IMAGE_COMMAND, + }), + referenceDocPath: 'images/move.mdx', + referenceGroup: 'images', + }, + + 'images.convertToInline': { + memberPath: 'images.convertToInline', + description: 'Convert a floating image to inline placement.', + expectedResult: 'Returns an ImagesMutationResult; reports NO_OP if already inline.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_IMAGE_COMMAND, + }), + referenceDocPath: 'images/convert-to-inline.mdx', + referenceGroup: 'images', + }, + + 'images.convertToFloating': { + memberPath: 'images.convertToFloating', + description: 'Convert an inline image to floating placement.', + expectedResult: 'Returns an ImagesMutationResult; reports NO_OP if already floating.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_IMAGE_COMMAND, + }), + referenceDocPath: 'images/convert-to-floating.mdx', + referenceGroup: 'images', + }, + + 'images.setSize': { + memberPath: 'images.setSize', + description: 'Set explicit width/height for an image.', + expectedResult: 'Returns an ImagesMutationResult; reports NO_OP if the size already matches.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: [...T_IMAGE_COMMAND, 'INVALID_INPUT'], + }), + referenceDocPath: 'images/set-size.mdx', + referenceGroup: 'images', + }, + + 'images.setWrapType': { + memberPath: 'images.setWrapType', + description: 'Set the text wrapping type for a floating image.', + expectedResult: 'Returns an ImagesMutationResult; reports NO_OP if already set.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_IMAGE_COMMAND, + }), + referenceDocPath: 'images/set-wrap-type.mdx', + referenceGroup: 'images', + }, + + 'images.setWrapSide': { + memberPath: 'images.setWrapSide', + description: 'Set which side(s) text wraps around a floating image.', + expectedResult: 'Returns an ImagesMutationResult; reports NO_OP if already set.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_IMAGE_COMMAND, + }), + referenceDocPath: 'images/set-wrap-side.mdx', + referenceGroup: 'images', + }, + + 'images.setWrapDistances': { + memberPath: 'images.setWrapDistances', + description: 'Set the text-wrap distance margins for a floating image.', + expectedResult: 'Returns an ImagesMutationResult; reports NO_OP if already set.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_IMAGE_COMMAND, + }), + referenceDocPath: 'images/set-wrap-distances.mdx', + referenceGroup: 'images', + }, + + 'images.setPosition': { + memberPath: 'images.setPosition', + description: 'Set the anchor position for a floating image.', + expectedResult: 'Returns an ImagesMutationResult.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_IMAGE_COMMAND, + }), + referenceDocPath: 'images/set-position.mdx', + referenceGroup: 'images', + }, + + 'images.setAnchorOptions': { + memberPath: 'images.setAnchorOptions', + description: 'Set anchor behavior options for a floating image.', + expectedResult: 'Returns an ImagesMutationResult.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_IMAGE_COMMAND, + }), + referenceDocPath: 'images/set-anchor-options.mdx', + referenceGroup: 'images', + }, + + 'images.setZOrder': { + memberPath: 'images.setZOrder', + description: 'Set the z-order (relativeHeight) for a floating image.', + expectedResult: 'Returns an ImagesMutationResult.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_IMAGE_COMMAND, + }), + referenceDocPath: 'images/set-z-order.mdx', + referenceGroup: 'images', + }, } as const satisfies Record; // --------------------------------------------------------------------------- diff --git a/packages/document-api/src/contract/operation-registry.ts b/packages/document-api/src/contract/operation-registry.ts index b75497dfba..5d0ebb1694 100644 --- a/packages/document-api/src/contract/operation-registry.ts +++ b/packages/document-api/src/contract/operation-registry.ts @@ -121,6 +121,26 @@ import type { SectionsSetVerticalAlignInput, } from '../sections/sections.types.js'; import type { QueryMatchInput, QueryMatchOutput } from '../types/query-match.types.js'; +import type { + CreateImageInput, + CreateImageResult, + ImagesListInput, + ImagesListResult, + ImagesGetInput, + ImageSummary, + ImagesDeleteInput, + ImagesMutationResult, + MoveImageInput, + ConvertToInlineInput, + ConvertToFloatingInput, + SetSizeInput, + SetWrapTypeInput, + SetWrapSideInput, + SetWrapDistancesInput, + SetPositionInput, + SetAnchorOptionsInput, + SetZOrderInput, +} from '../images/images.types.js'; import type { MutationsApplyInput, MutationsPreviewInput, @@ -572,6 +592,24 @@ export interface OperationRegistry extends FormatInlineAliasOperationRegistry { 'toc.listEntries': { input: TocListEntriesQuery | undefined; options: never; output: TocListEntriesResult }; 'toc.getEntry': { input: TocGetEntryInput; options: never; output: TocEntryInfo }; 'toc.editEntry': { input: TocEditEntryInput; options: MutationOptions; output: TocEntryMutationResult }; + + // --- create.image --- + 'create.image': { input: CreateImageInput; options: MutationOptions; output: CreateImageResult }; + + // --- images.* --- + 'images.list': { input: ImagesListInput | undefined; options: never; output: ImagesListResult }; + 'images.get': { input: ImagesGetInput; options: never; output: ImageSummary }; + 'images.delete': { input: ImagesDeleteInput; options: MutationOptions; output: ImagesMutationResult }; + 'images.move': { input: MoveImageInput; options: MutationOptions; output: ImagesMutationResult }; + 'images.convertToInline': { input: ConvertToInlineInput; options: MutationOptions; output: ImagesMutationResult }; + 'images.convertToFloating': { input: ConvertToFloatingInput; options: MutationOptions; output: ImagesMutationResult }; + 'images.setSize': { input: SetSizeInput; options: MutationOptions; output: ImagesMutationResult }; + 'images.setWrapType': { input: SetWrapTypeInput; options: MutationOptions; output: ImagesMutationResult }; + 'images.setWrapSide': { input: SetWrapSideInput; options: MutationOptions; output: ImagesMutationResult }; + 'images.setWrapDistances': { input: SetWrapDistancesInput; options: MutationOptions; output: ImagesMutationResult }; + 'images.setPosition': { input: SetPositionInput; options: MutationOptions; output: ImagesMutationResult }; + 'images.setAnchorOptions': { input: SetAnchorOptionsInput; options: MutationOptions; output: ImagesMutationResult }; + 'images.setZOrder': { input: SetZOrderInput; options: MutationOptions; output: ImagesMutationResult }; } // --- Bidirectional completeness checks --- diff --git a/packages/document-api/src/contract/reference-doc-map.ts b/packages/document-api/src/contract/reference-doc-map.ts index 0c0286e80b..80cba678ca 100644 --- a/packages/document-api/src/contract/reference-doc-map.ts +++ b/packages/document-api/src/contract/reference-doc-map.ts @@ -106,6 +106,11 @@ const GROUP_METADATA: Record = { success: tocEntryMutationSuccessSchema, failure: tocEntryMutationFailureSchema, }, + + // --- images --- + + // Shared image location schema — discriminated union on `kind`. + // Used by create.image (at) and images.move (to). + + 'create.image': { + input: objectSchema( + { + src: { type: 'string' }, + alt: { type: 'string' }, + title: { type: 'string' }, + size: objectSchema({ width: { type: 'number' }, height: { type: 'number' } }), + at: { + oneOf: [ + objectSchema({ kind: { const: 'documentStart' } }, ['kind']), + objectSchema({ kind: { const: 'documentEnd' } }, ['kind']), + objectSchema({ kind: { const: 'before' }, target: blockNodeAddressSchema }, ['kind', 'target']), + objectSchema({ kind: { const: 'after' }, target: blockNodeAddressSchema }, ['kind', 'target']), + objectSchema( + { kind: { const: 'inParagraph' }, target: blockNodeAddressSchema, offset: { type: 'integer' } }, + ['kind', 'target'], + ), + ], + }, + }, + ['src'], + ), + output: objectSchema({ success: { type: 'boolean' }, image: { type: 'object' }, failure: { type: 'object' } }), + success: objectSchema({ success: { const: true }, image: { type: 'object' } }, ['success', 'image']), + failure: objectSchema( + { + success: { const: false }, + failure: objectSchema({ code: { enum: ['INVALID_TARGET', 'INVALID_INPUT'] }, message: { type: 'string' } }, [ + 'code', + 'message', + ]), + }, + ['success', 'failure'], + ), + }, + 'images.list': { + input: objectSchema({ offset: { type: 'integer' }, limit: { type: 'integer' } }), + output: objectSchema({ total: { type: 'integer' }, items: arraySchema({ type: 'object' }) }, ['total', 'items']), + }, + 'images.get': { + input: objectSchema({ imageId: { type: 'string' } }, ['imageId']), + output: { type: 'object' as const }, + }, + 'images.delete': { + input: objectSchema({ imageId: { type: 'string' } }, ['imageId']), + output: objectSchema({ success: { type: 'boolean' }, image: { type: 'object' }, failure: { type: 'object' } }), + success: objectSchema({ success: { const: true }, image: { type: 'object' } }, ['success', 'image']), + failure: objectSchema( + { + success: { const: false }, + failure: objectSchema({ code: { type: 'string' }, message: { type: 'string' } }, ['code', 'message']), + }, + ['success', 'failure'], + ), + }, + 'images.move': { + input: objectSchema( + { + imageId: { type: 'string' }, + to: { + oneOf: [ + objectSchema({ kind: { const: 'documentStart' } }, ['kind']), + objectSchema({ kind: { const: 'documentEnd' } }, ['kind']), + objectSchema({ kind: { const: 'before' }, target: blockNodeAddressSchema }, ['kind', 'target']), + objectSchema({ kind: { const: 'after' }, target: blockNodeAddressSchema }, ['kind', 'target']), + objectSchema( + { kind: { const: 'inParagraph' }, target: blockNodeAddressSchema, offset: { type: 'integer' } }, + ['kind', 'target'], + ), + ], + }, + }, + ['imageId', 'to'], + ), + output: objectSchema({ success: { type: 'boolean' }, image: { type: 'object' }, failure: { type: 'object' } }), + success: objectSchema({ success: { const: true }, image: { type: 'object' } }, ['success', 'image']), + failure: objectSchema( + { + success: { const: false }, + failure: objectSchema({ code: { type: 'string' }, message: { type: 'string' } }, ['code', 'message']), + }, + ['success', 'failure'], + ), + }, + 'images.convertToInline': { + input: objectSchema({ imageId: { type: 'string' } }, ['imageId']), + output: objectSchema({ success: { type: 'boolean' }, image: { type: 'object' }, failure: { type: 'object' } }), + success: objectSchema({ success: { const: true }, image: { type: 'object' } }, ['success', 'image']), + failure: objectSchema( + { + success: { const: false }, + failure: objectSchema({ code: { type: 'string' }, message: { type: 'string' } }, ['code', 'message']), + }, + ['success', 'failure'], + ), + }, + 'images.convertToFloating': { + input: objectSchema({ imageId: { type: 'string' } }, ['imageId']), + output: objectSchema({ success: { type: 'boolean' }, image: { type: 'object' }, failure: { type: 'object' } }), + success: objectSchema({ success: { const: true }, image: { type: 'object' } }, ['success', 'image']), + failure: objectSchema( + { + success: { const: false }, + failure: objectSchema({ code: { type: 'string' }, message: { type: 'string' } }, ['code', 'message']), + }, + ['success', 'failure'], + ), + }, + 'images.setSize': { + input: objectSchema( + { + imageId: { type: 'string' }, + size: objectSchema( + { + width: { type: 'number', exclusiveMinimum: 0 }, + height: { type: 'number', exclusiveMinimum: 0 }, + unit: { type: 'string', enum: ['px', 'pt', 'twip'] }, + }, + ['width', 'height'], + ), + }, + ['imageId', 'size'], + ), + output: objectSchema({ success: { type: 'boolean' }, image: { type: 'object' }, failure: { type: 'object' } }), + success: objectSchema({ success: { const: true }, image: { type: 'object' } }, ['success', 'image']), + failure: objectSchema( + { + success: { const: false }, + failure: objectSchema({ code: { type: 'string' }, message: { type: 'string' } }, ['code', 'message']), + }, + ['success', 'failure'], + ), + }, + 'images.setWrapType': { + input: objectSchema( + { + imageId: { type: 'string' }, + type: { type: 'string', enum: ['None', 'Square', 'Through', 'Tight', 'TopAndBottom', 'Inline'] }, + }, + ['imageId', 'type'], + ), + output: objectSchema({ success: { type: 'boolean' }, image: { type: 'object' }, failure: { type: 'object' } }), + success: objectSchema({ success: { const: true }, image: { type: 'object' } }, ['success', 'image']), + failure: objectSchema( + { + success: { const: false }, + failure: objectSchema({ code: { type: 'string' }, message: { type: 'string' } }, ['code', 'message']), + }, + ['success', 'failure'], + ), + }, + 'images.setWrapSide': { + input: objectSchema( + { imageId: { type: 'string' }, side: { type: 'string', enum: ['bothSides', 'left', 'right', 'largest'] } }, + ['imageId', 'side'], + ), + output: objectSchema({ success: { type: 'boolean' }, image: { type: 'object' }, failure: { type: 'object' } }), + success: objectSchema({ success: { const: true }, image: { type: 'object' } }, ['success', 'image']), + failure: objectSchema( + { + success: { const: false }, + failure: objectSchema({ code: { type: 'string' }, message: { type: 'string' } }, ['code', 'message']), + }, + ['success', 'failure'], + ), + }, + 'images.setWrapDistances': { + input: objectSchema( + { + imageId: { type: 'string' }, + distances: objectSchema({ + distTop: { type: 'number' }, + distBottom: { type: 'number' }, + distLeft: { type: 'number' }, + distRight: { type: 'number' }, + }), + }, + ['imageId', 'distances'], + ), + output: objectSchema({ success: { type: 'boolean' }, image: { type: 'object' }, failure: { type: 'object' } }), + success: objectSchema({ success: { const: true }, image: { type: 'object' } }, ['success', 'image']), + failure: objectSchema( + { + success: { const: false }, + failure: objectSchema({ code: { type: 'string' }, message: { type: 'string' } }, ['code', 'message']), + }, + ['success', 'failure'], + ), + }, + 'images.setPosition': { + input: objectSchema( + { + imageId: { type: 'string' }, + position: objectSchema({ + hRelativeFrom: { type: 'string' }, + vRelativeFrom: { type: 'string' }, + alignH: { type: 'string' }, + alignV: { type: 'string' }, + marginOffset: objectSchema({ + horizontal: { type: 'number' }, + top: { type: 'number' }, + }), + }), + }, + ['imageId', 'position'], + ), + output: objectSchema({ success: { type: 'boolean' }, image: { type: 'object' }, failure: { type: 'object' } }), + success: objectSchema({ success: { const: true }, image: { type: 'object' } }, ['success', 'image']), + failure: objectSchema( + { + success: { const: false }, + failure: objectSchema({ code: { type: 'string' }, message: { type: 'string' } }, ['code', 'message']), + }, + ['success', 'failure'], + ), + }, + 'images.setAnchorOptions': { + input: objectSchema( + { + imageId: { type: 'string' }, + options: objectSchema({ + behindDoc: { type: 'boolean' }, + allowOverlap: { type: 'boolean' }, + layoutInCell: { type: 'boolean' }, + lockAnchor: { type: 'boolean' }, + simplePos: { type: 'boolean' }, + }), + }, + ['imageId', 'options'], + ), + output: objectSchema({ success: { type: 'boolean' }, image: { type: 'object' }, failure: { type: 'object' } }), + success: objectSchema({ success: { const: true }, image: { type: 'object' } }, ['success', 'image']), + failure: objectSchema( + { + success: { const: false }, + failure: objectSchema({ code: { type: 'string' }, message: { type: 'string' } }, ['code', 'message']), + }, + ['success', 'failure'], + ), + }, + 'images.setZOrder': { + input: objectSchema( + { + imageId: { type: 'string' }, + zOrder: objectSchema({ relativeHeight: { type: 'number' } }, ['relativeHeight']), + }, + ['imageId', 'zOrder'], + ), + output: objectSchema({ success: { type: 'boolean' }, image: { type: 'object' }, failure: { type: 'object' } }), + success: objectSchema({ success: { const: true }, image: { type: 'object' } }, ['success', 'image']), + failure: objectSchema( + { + success: { const: false }, + failure: objectSchema({ code: { type: 'string' }, message: { type: 'string' } }, ['code', 'message']), + }, + ['success', 'failure'], + ), + }, }; /** diff --git a/packages/document-api/src/create/create.ts b/packages/document-api/src/create/create.ts index d5823437e8..8b938ced83 100644 --- a/packages/document-api/src/create/create.ts +++ b/packages/document-api/src/create/create.ts @@ -16,6 +16,7 @@ import type { SectionBreakType, } from '../sections/sections.types.js'; import type { CreateTableOfContentsInput, CreateTableOfContentsResult, TocCreateLocation } from '../toc/toc.types.js'; +import type { CreateImageInput, CreateImageResult } from '../images/images.types.js'; import { DocumentApiValidationError } from '../errors.js'; export interface CreateApi { @@ -24,6 +25,7 @@ export interface CreateApi { table(input: CreateTableInput, options?: MutationOptions): CreateTableResult; sectionBreak(input: CreateSectionBreakInput, options?: MutationOptions): CreateSectionBreakResult; tableOfContents(input: CreateTableOfContentsInput, options?: MutationOptions): CreateTableOfContentsResult; + image(input: CreateImageInput, options?: MutationOptions): CreateImageResult; } export type CreateAdapter = CreateApi; diff --git a/packages/document-api/src/images/images.ts b/packages/document-api/src/images/images.ts new file mode 100644 index 0000000000..90544cb512 --- /dev/null +++ b/packages/document-api/src/images/images.ts @@ -0,0 +1,257 @@ +import type { MutationOptions } from '../types/index.js'; +import { DocumentApiValidationError } from '../errors.js'; +import type { + CreateImageInput, + CreateImageResult, + ImagesListInput, + ImagesListResult, + ImagesGetInput, + ImageSummary, + ImagesDeleteInput, + ImagesMutationResult, + MoveImageInput, + ConvertToInlineInput, + ConvertToFloatingInput, + SetSizeInput, + SetWrapTypeInput, + SetWrapSideInput, + SetWrapDistancesInput, + SetPositionInput, + SetAnchorOptionsInput, + SetZOrderInput, +} from './images.types.js'; + +// --------------------------------------------------------------------------- +// Valid value sets +// --------------------------------------------------------------------------- + +const VALID_WRAP_TYPES = new Set(['Inline', 'None', 'Square', 'Tight', 'Through', 'TopAndBottom']); +const VALID_WRAP_SIDES = new Set(['bothSides', 'left', 'right', 'largest']); +const VALID_IMAGE_SIZE_UNITS = new Set(['px', 'pt', 'twip']); + +// --------------------------------------------------------------------------- +// Adapter interface +// --------------------------------------------------------------------------- + +export interface ImagesAdapter { + list(input: ImagesListInput): ImagesListResult; + get(input: ImagesGetInput): ImageSummary; + delete(input: ImagesDeleteInput, options?: MutationOptions): ImagesMutationResult; + move(input: MoveImageInput, options?: MutationOptions): ImagesMutationResult; + convertToInline(input: ConvertToInlineInput, options?: MutationOptions): ImagesMutationResult; + convertToFloating(input: ConvertToFloatingInput, options?: MutationOptions): ImagesMutationResult; + setSize(input: SetSizeInput, options?: MutationOptions): ImagesMutationResult; + setWrapType(input: SetWrapTypeInput, options?: MutationOptions): ImagesMutationResult; + setWrapSide(input: SetWrapSideInput, options?: MutationOptions): ImagesMutationResult; + setWrapDistances(input: SetWrapDistancesInput, options?: MutationOptions): ImagesMutationResult; + setPosition(input: SetPositionInput, options?: MutationOptions): ImagesMutationResult; + setAnchorOptions(input: SetAnchorOptionsInput, options?: MutationOptions): ImagesMutationResult; + setZOrder(input: SetZOrderInput, options?: MutationOptions): ImagesMutationResult; +} + +export type ImagesApi = ImagesAdapter; + +// --------------------------------------------------------------------------- +// Validation helpers +// --------------------------------------------------------------------------- + +function requireString(value: unknown, field: string): asserts value is string { + if (typeof value !== 'string' || value.length === 0) { + throw new DocumentApiValidationError('INVALID_INPUT', `${field} must be a non-empty string.`, { field }); + } +} + +function requireImageId(input: { imageId?: unknown }): void { + requireString(input?.imageId, 'imageId'); +} + +function requireFinitePositiveNumber(value: unknown, field: string): asserts value is number { + if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) { + throw new DocumentApiValidationError('INVALID_INPUT', `${field} must be a finite positive number.`, { + field, + value, + }); + } +} + +// --------------------------------------------------------------------------- +// Execute functions +// --------------------------------------------------------------------------- + +export function executeImagesList(adapter: ImagesAdapter, input: ImagesListInput): ImagesListResult { + return adapter.list(input ?? {}); +} + +export function executeImagesGet(adapter: ImagesAdapter, input: ImagesGetInput): ImageSummary { + requireImageId(input); + return adapter.get(input); +} + +export function executeImagesDelete( + adapter: ImagesAdapter, + input: ImagesDeleteInput, + options?: MutationOptions, +): ImagesMutationResult { + requireImageId(input); + return adapter.delete(input, options); +} + +export function executeImagesMove( + adapter: ImagesAdapter, + input: MoveImageInput, + options?: MutationOptions, +): ImagesMutationResult { + requireImageId(input); + if (!input.to) { + throw new DocumentApiValidationError('INVALID_INPUT', 'images.move requires a "to" location.', { field: 'to' }); + } + return adapter.move(input, options); +} + +export function executeImagesConvertToInline( + adapter: ImagesAdapter, + input: ConvertToInlineInput, + options?: MutationOptions, +): ImagesMutationResult { + requireImageId(input); + return adapter.convertToInline(input, options); +} + +export function executeImagesConvertToFloating( + adapter: ImagesAdapter, + input: ConvertToFloatingInput, + options?: MutationOptions, +): ImagesMutationResult { + requireImageId(input); + return adapter.convertToFloating(input, options); +} + +export function executeImagesSetSize( + adapter: ImagesAdapter, + input: SetSizeInput, + options?: MutationOptions, +): ImagesMutationResult { + requireImageId(input); + if (!input.size || typeof input.size !== 'object') { + throw new DocumentApiValidationError('INVALID_INPUT', 'images.setSize requires a "size" object.', { + field: 'size', + }); + } + + requireFinitePositiveNumber(input.size.width, 'size.width'); + requireFinitePositiveNumber(input.size.height, 'size.height'); + + if (input.size.unit !== undefined && !VALID_IMAGE_SIZE_UNITS.has(input.size.unit)) { + throw new DocumentApiValidationError('INVALID_INPUT', 'size.unit must be one of: px, pt, twip.', { + field: 'size.unit', + allowed: [...VALID_IMAGE_SIZE_UNITS], + }); + } + + return adapter.setSize(input, options); +} + +export function executeImagesSetWrapType( + adapter: ImagesAdapter, + input: SetWrapTypeInput, + options?: MutationOptions, +): ImagesMutationResult { + requireImageId(input); + if (!VALID_WRAP_TYPES.has(input.type)) { + throw new DocumentApiValidationError('INVALID_INPUT', `Invalid wrap type: "${input.type}".`, { + field: 'type', + allowed: [...VALID_WRAP_TYPES], + }); + } + return adapter.setWrapType(input, options); +} + +export function executeImagesSetWrapSide( + adapter: ImagesAdapter, + input: SetWrapSideInput, + options?: MutationOptions, +): ImagesMutationResult { + requireImageId(input); + if (!VALID_WRAP_SIDES.has(input.side)) { + throw new DocumentApiValidationError('INVALID_INPUT', `Invalid wrap side: "${input.side}".`, { + field: 'side', + allowed: [...VALID_WRAP_SIDES], + }); + } + return adapter.setWrapSide(input, options); +} + +export function executeImagesSetWrapDistances( + adapter: ImagesAdapter, + input: SetWrapDistancesInput, + options?: MutationOptions, +): ImagesMutationResult { + requireImageId(input); + if (!input.distances || typeof input.distances !== 'object') { + throw new DocumentApiValidationError('INVALID_INPUT', 'images.setWrapDistances requires a "distances" object.', { + field: 'distances', + }); + } + return adapter.setWrapDistances(input, options); +} + +export function executeImagesSetPosition( + adapter: ImagesAdapter, + input: SetPositionInput, + options?: MutationOptions, +): ImagesMutationResult { + requireImageId(input); + if (!input.position || typeof input.position !== 'object') { + throw new DocumentApiValidationError('INVALID_INPUT', 'images.setPosition requires a "position" object.', { + field: 'position', + }); + } + return adapter.setPosition(input, options); +} + +export function executeImagesSetAnchorOptions( + adapter: ImagesAdapter, + input: SetAnchorOptionsInput, + options?: MutationOptions, +): ImagesMutationResult { + requireImageId(input); + if (!input.options || typeof input.options !== 'object') { + throw new DocumentApiValidationError('INVALID_INPUT', 'images.setAnchorOptions requires an "options" object.', { + field: 'options', + }); + } + return adapter.setAnchorOptions(input, options); +} + +export function executeImagesSetZOrder( + adapter: ImagesAdapter, + input: SetZOrderInput, + options?: MutationOptions, +): ImagesMutationResult { + requireImageId(input); + if (!input.zOrder || !Number.isFinite(input.zOrder.relativeHeight)) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + 'images.setZOrder requires zOrder.relativeHeight as a number.', + { field: 'zOrder.relativeHeight' }, + ); + } + return adapter.setZOrder(input, options); +} + +// --------------------------------------------------------------------------- +// Create image execute (lives here alongside images domain) +// --------------------------------------------------------------------------- + +export interface CreateImageAdapter { + image(input: CreateImageInput, options?: MutationOptions): CreateImageResult; +} + +export function executeCreateImage( + adapter: CreateImageAdapter, + input: CreateImageInput, + options?: MutationOptions, +): CreateImageResult { + requireString(input?.src, 'src'); + return adapter.image(input, options); +} diff --git a/packages/document-api/src/images/images.types.ts b/packages/document-api/src/images/images.types.ts new file mode 100644 index 0000000000..ac7e4f217c --- /dev/null +++ b/packages/document-api/src/images/images.types.ts @@ -0,0 +1,193 @@ +import type { BlockNodeAddress } from '../types/index.js'; +import type { + ImageProperties, + ImageWrapType, + ImageWrapSide, + ImageMarginOffset, + ImageSize, +} from '../types/media.types.js'; + +// --------------------------------------------------------------------------- +// Address +// --------------------------------------------------------------------------- + +/** Stable address for an image node in the document. */ +export interface ImageAddress { + /** Always 'inline' — ProseMirror node kind (all images are PM inline nodes). */ + kind: 'inline'; + nodeType: 'image'; + nodeId: string; + /** OOXML placement semantics: 'inline' = wp:inline, 'floating' = wp:anchor. */ + placement: 'inline' | 'floating'; +} + +// --------------------------------------------------------------------------- +// Location (for create / move) +// --------------------------------------------------------------------------- + +export type ImageCreateLocation = + | { kind: 'documentStart' } + | { kind: 'documentEnd' } + | { kind: 'before'; target: BlockNodeAddress } + | { kind: 'after'; target: BlockNodeAddress } + | { kind: 'inParagraph'; target: BlockNodeAddress; offset?: number }; + +// --------------------------------------------------------------------------- +// Summary +// --------------------------------------------------------------------------- + +export interface ImageSummary { + sdImageId: string; + address: ImageAddress; + properties: ImageProperties; +} + +// --------------------------------------------------------------------------- +// Wrap distances +// --------------------------------------------------------------------------- + +export interface ImageWrapDistances { + distTop?: number; + distBottom?: number; + distLeft?: number; + distRight?: number; +} + +// --------------------------------------------------------------------------- +// Position input +// --------------------------------------------------------------------------- + +export interface ImagePositionInput { + hRelativeFrom?: string; + vRelativeFrom?: string; + alignH?: string; + alignV?: string; + marginOffset?: ImageMarginOffset; +} + +// --------------------------------------------------------------------------- +// Anchor options input +// --------------------------------------------------------------------------- + +export interface ImageAnchorOptionsInput { + behindDoc?: boolean; + allowOverlap?: boolean; + layoutInCell?: boolean; + lockAnchor?: boolean; + simplePos?: boolean; +} + +// --------------------------------------------------------------------------- +// Z-order input +// --------------------------------------------------------------------------- + +export interface ImageZOrderInput { + /** Raw OOXML relativeHeight integer. */ + relativeHeight: number; +} + +// --------------------------------------------------------------------------- +// Operation inputs +// --------------------------------------------------------------------------- + +export interface CreateImageInput { + src: string; + alt?: string; + title?: string; + size?: ImageSize; + at?: ImageCreateLocation; +} + +export interface ImagesListInput { + offset?: number; + limit?: number; +} + +export interface ImagesGetInput { + imageId: string; +} + +export interface ImagesDeleteInput { + imageId: string; +} + +export interface MoveImageInput { + imageId: string; + to: ImageCreateLocation; +} + +export interface ConvertToInlineInput { + imageId: string; +} + +export interface ConvertToFloatingInput { + imageId: string; +} + +export interface SetWrapTypeInput { + imageId: string; + type: ImageWrapType; +} + +export interface SetSizeInput { + imageId: string; + size: ImageSize; +} + +export interface SetWrapSideInput { + imageId: string; + side: ImageWrapSide; +} + +export interface SetWrapDistancesInput { + imageId: string; + distances: ImageWrapDistances; +} + +export interface SetPositionInput { + imageId: string; + position: ImagePositionInput; +} + +export interface SetAnchorOptionsInput { + imageId: string; + options: ImageAnchorOptionsInput; +} + +export interface SetZOrderInput { + imageId: string; + zOrder: ImageZOrderInput; +} + +// --------------------------------------------------------------------------- +// Operation outputs +// --------------------------------------------------------------------------- + +export interface CreateImageSuccessResult { + success: true; + image: ImageAddress; +} + +export interface CreateImageFailureResult { + success: false; + failure: { code: string; message: string }; +} + +export type CreateImageResult = CreateImageSuccessResult | CreateImageFailureResult; + +export interface ImagesListResult { + total: number; + items: ImageSummary[]; +} + +export interface ImagesMutationSuccessResult { + success: true; + image: ImageAddress; +} + +export interface ImagesMutationFailureResult { + success: false; + failure: { code: string; message: string }; +} + +export type ImagesMutationResult = ImagesMutationSuccessResult | ImagesMutationFailureResult; diff --git a/packages/document-api/src/index.ts b/packages/document-api/src/index.ts index a1ab30e63d..133d306d6a 100644 --- a/packages/document-api/src/index.ts +++ b/packages/document-api/src/index.ts @@ -304,6 +304,43 @@ import { executeSectionsSetTitlePage, executeSectionsSetVerticalAlign, } from './sections/sections.js'; +import type { ImagesAdapter, ImagesApi, CreateImageAdapter } from './images/images.js'; +import { + executeImagesList, + executeImagesGet, + executeImagesDelete, + executeImagesMove, + executeImagesConvertToInline, + executeImagesConvertToFloating, + executeImagesSetSize, + executeImagesSetWrapType, + executeImagesSetWrapSide, + executeImagesSetWrapDistances, + executeImagesSetPosition, + executeImagesSetAnchorOptions, + executeImagesSetZOrder, + executeCreateImage, +} from './images/images.js'; +import type { + CreateImageInput, + CreateImageResult, + ImagesListInput, + ImagesListResult, + ImagesGetInput, + ImageSummary, + ImagesDeleteInput, + ImagesMutationResult, + MoveImageInput, + ConvertToInlineInput, + ConvertToFloatingInput, + SetSizeInput, + SetWrapTypeInput, + SetWrapSideInput, + SetWrapDistancesInput, + SetPositionInput, + SetAnchorOptionsInput, + SetZOrderInput, +} from './images/images.types.js'; import type { TocApi, TocAdapter } from './toc/toc.js'; import { executeTocList, @@ -425,6 +462,35 @@ export type { ReviewDecideInput, } from './track-changes/track-changes.js'; export type { BlocksAdapter } from './blocks/blocks.js'; +export type { ImagesAdapter, ImagesApi, CreateImageAdapter } from './images/images.js'; +export type { + ImageAddress, + ImageCreateLocation, + ImageSummary, + ImageWrapDistances, + ImagePositionInput, + ImageAnchorOptionsInput, + ImageZOrderInput, + CreateImageInput, + CreateImageResult, + ImagesListInput, + ImagesListResult, + ImagesGetInput, + ImagesDeleteInput, + ImagesMutationResult, + ImagesMutationSuccessResult, + ImagesMutationFailureResult, + MoveImageInput, + ConvertToInlineInput, + ConvertToFloatingInput, + SetSizeInput, + SetWrapTypeInput, + SetWrapSideInput, + SetWrapDistancesInput, + SetPositionInput, + SetAnchorOptionsInput, + SetZOrderInput, +} from './images/images.types.js'; export type { TocApi, TocAdapter } from './toc/toc.js'; export type { TocAddress, @@ -792,6 +858,10 @@ export interface DocumentApi { * Table of contents operations. */ toc: TocApi; + /** + * Image lifecycle and placement operations. + */ + images: ImagesApi; /** * Selector-based query with cardinality contracts for mutation targeting. */ @@ -846,6 +916,7 @@ export interface DocumentApiAdapters { paragraphs: ParagraphsAdapter; tables: TablesAdapter; toc: TocAdapter; + images: ImagesAdapter & CreateImageAdapter; query: QueryAdapter; mutations: MutationsAdapter; history: HistoryAdapter; @@ -1041,8 +1112,52 @@ export function createDocumentApi(adapters: DocumentApiAdapters): DocumentApi { tableOfContents(input: CreateTableOfContentsInput, options?: MutationOptions): CreateTableOfContentsResult { return executeCreateTableOfContents(adapters.create, input, options); }, + image(input: CreateImageInput, options?: MutationOptions): CreateImageResult { + return executeCreateImage(adapters.images, input, options); + }, }, capabilities, + images: { + list(input?: ImagesListInput): ImagesListResult { + return executeImagesList(adapters.images, input ?? {}); + }, + get(input: ImagesGetInput): ImageSummary { + return executeImagesGet(adapters.images, input); + }, + delete(input: ImagesDeleteInput, options?: MutationOptions): ImagesMutationResult { + return executeImagesDelete(adapters.images, input, options); + }, + move(input: MoveImageInput, options?: MutationOptions): ImagesMutationResult { + return executeImagesMove(adapters.images, input, options); + }, + convertToInline(input: ConvertToInlineInput, options?: MutationOptions): ImagesMutationResult { + return executeImagesConvertToInline(adapters.images, input, options); + }, + convertToFloating(input: ConvertToFloatingInput, options?: MutationOptions): ImagesMutationResult { + return executeImagesConvertToFloating(adapters.images, input, options); + }, + setSize(input: SetSizeInput, options?: MutationOptions): ImagesMutationResult { + return executeImagesSetSize(adapters.images, input, options); + }, + setWrapType(input: SetWrapTypeInput, options?: MutationOptions): ImagesMutationResult { + return executeImagesSetWrapType(adapters.images, input, options); + }, + setWrapSide(input: SetWrapSideInput, options?: MutationOptions): ImagesMutationResult { + return executeImagesSetWrapSide(adapters.images, input, options); + }, + setWrapDistances(input: SetWrapDistancesInput, options?: MutationOptions): ImagesMutationResult { + return executeImagesSetWrapDistances(adapters.images, input, options); + }, + setPosition(input: SetPositionInput, options?: MutationOptions): ImagesMutationResult { + return executeImagesSetPosition(adapters.images, input, options); + }, + setAnchorOptions(input: SetAnchorOptionsInput, options?: MutationOptions): ImagesMutationResult { + return executeImagesSetAnchorOptions(adapters.images, input, options); + }, + setZOrder(input: SetZOrderInput, options?: MutationOptions): ImagesMutationResult { + return executeImagesSetZOrder(adapters.images, input, options); + }, + }, lists: { list(query?: ListsListQuery): ListsListResult { return executeListsList(adapters.lists, query); diff --git a/packages/document-api/src/invoke/invoke.ts b/packages/document-api/src/invoke/invoke.ts index 0290c0c169..bdacc2e33f 100644 --- a/packages/document-api/src/invoke/invoke.ts +++ b/packages/document-api/src/invoke/invoke.ts @@ -241,5 +241,23 @@ export function buildDispatchTable(api: DocumentApi): TypedDispatchTable { 'toc.listEntries': (input) => api.toc.listEntries(input), 'toc.getEntry': (input) => api.toc.getEntry(input), 'toc.editEntry': (input, options) => api.toc.editEntry(input, options), + + // --- create.image --- + 'create.image': (input, options) => api.create.image(input, options), + + // --- images.* --- + 'images.list': (input) => api.images.list(input ?? {}), + 'images.get': (input) => api.images.get(input), + 'images.delete': (input, options) => api.images.delete(input, options), + 'images.move': (input, options) => api.images.move(input, options), + 'images.convertToInline': (input, options) => api.images.convertToInline(input, options), + 'images.convertToFloating': (input, options) => api.images.convertToFloating(input, options), + 'images.setSize': (input, options) => api.images.setSize(input, options), + 'images.setWrapType': (input, options) => api.images.setWrapType(input, options), + 'images.setWrapSide': (input, options) => api.images.setWrapSide(input, options), + 'images.setWrapDistances': (input, options) => api.images.setWrapDistances(input, options), + 'images.setPosition': (input, options) => api.images.setPosition(input, options), + 'images.setAnchorOptions': (input, options) => api.images.setAnchorOptions(input, options), + 'images.setZOrder': (input, options) => api.images.setZOrder(input, options), }; } diff --git a/packages/document-api/src/types/media.types.ts b/packages/document-api/src/types/media.types.ts index 058d20c750..df6ac0fc01 100644 --- a/packages/document-api/src/types/media.types.ts +++ b/packages/document-api/src/types/media.types.ts @@ -12,9 +12,44 @@ export interface ImageSize { unit?: 'px' | 'pt' | 'twip'; } +/** Wrap type for OOXML image placement. */ +export type ImageWrapType = 'Inline' | 'None' | 'Square' | 'Tight' | 'Through' | 'TopAndBottom'; + +/** Wrap side — controls which side(s) text flows around the image. */ +export type ImageWrapSide = 'bothSides' | 'left' | 'right' | 'largest'; + +export interface ImageWrapAttrs { + wrapText?: string; + distTop?: number; + distBottom?: number; + distLeft?: number; + distRight?: number; +} + +export interface ImageWrapInfo { + type: ImageWrapType; + attrs?: ImageWrapAttrs; +} + +export interface ImageAnchorData { + hRelativeFrom?: string; + vRelativeFrom?: string; + alignH?: string; + alignV?: string; +} + +export interface ImageMarginOffset { + horizontal?: number; + top?: number; +} + export interface ImageProperties { src?: string; alt?: string; size?: ImageSize; - wrap?: string; + placement: 'inline' | 'floating'; + wrap: ImageWrapInfo; + anchorData?: ImageAnchorData | null; + marginOffset?: ImageMarginOffset | null; + relativeHeight?: number | null; } diff --git a/packages/super-editor/src/core/super-converter/image-dimensions.js b/packages/super-editor/src/core/super-converter/image-dimensions.js new file mode 100644 index 0000000000..0e1a4f5b01 --- /dev/null +++ b/packages/super-editor/src/core/super-converter/image-dimensions.js @@ -0,0 +1,168 @@ +import { base64ToUint8Array } from './helpers.js'; + +/** + * Read intrinsic image dimensions from raw binary headers. + * Supports PNG, JPEG, GIF, BMP, and WEBP. + * + * @param {Uint8Array} bytes - Raw image bytes + * @returns {{ width: number, height: number } | null} Dimensions or null if unreadable + */ +export function readImageDimensions(bytes) { + if (!(bytes instanceof Uint8Array) || bytes.length < 12) return null; + + // PNG: IHDR chunk at bytes 16-23 (big-endian int32 width, height) + if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) { + if (bytes.length < 24) return null; + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const width = view.getInt32(16); + const height = view.getInt32(20); + if (width > 0 && height > 0) return { width, height }; + return null; + } + + // JPEG: Scan for SOF0 (0xFFC0) or SOF2 (0xFFC2) marker + if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) { + return readJpegDimensions(bytes); + } + + // GIF: Logical screen descriptor at bytes 6-9 (little-endian uint16) + if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x38) { + if (bytes.length < 10) return null; + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const width = view.getUint16(6, true); + const height = view.getUint16(8, true); + if (width > 0 && height > 0) return { width, height }; + return null; + } + + // BMP: DIB header at bytes 18-25 (little-endian int32) + if (bytes[0] === 0x42 && bytes[1] === 0x4d) { + if (bytes.length < 26) return null; + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const width = view.getInt32(18, true); + const height = Math.abs(view.getInt32(22, true)); // height can be negative (top-down) + if (width > 0 && height > 0) return { width, height }; + return null; + } + + // WEBP: RIFF....WEBP + if ( + bytes[0] === 0x52 && + bytes[1] === 0x49 && + bytes[2] === 0x46 && + bytes[3] === 0x46 && + bytes[8] === 0x57 && + bytes[9] === 0x45 && + bytes[10] === 0x42 && + bytes[11] === 0x50 + ) { + return readWebpDimensions(bytes); + } + + return null; +} + +/** + * Scan JPEG markers for SOF0/SOF2 to read width/height. + * @param {Uint8Array} bytes + * @returns {{ width: number, height: number } | null} + */ +function readJpegDimensions(bytes) { + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + let offset = 2; // skip SOI (0xFFD8) + + while (offset + 4 < bytes.length) { + if (bytes[offset] !== 0xff) return null; + const marker = bytes[offset + 1]; + + // SOF0 (0xC0) or SOF2 (0xC2) — baseline or progressive + if (marker === 0xc0 || marker === 0xc2) { + if (offset + 9 > bytes.length) return null; + const height = view.getUint16(offset + 5); + const width = view.getUint16(offset + 7); + if (width > 0 && height > 0) return { width, height }; + return null; + } + + // Skip non-SOF markers: read segment length and advance + if (marker === 0xd9) return null; // EOI — end of image + if (marker === 0xda) return null; // SOS — start of scan (no more metadata) + + const segmentLength = view.getUint16(offset + 2); + offset += 2 + segmentLength; + } + + return null; +} + +/** + * Read WEBP dimensions from VP8, VP8L, or VP8X sub-chunks. + * @param {Uint8Array} bytes + * @returns {{ width: number, height: number } | null} + */ +function readWebpDimensions(bytes) { + if (bytes.length < 16) return null; + + // Check sub-chunk type at byte 12 + const chunkTag = String.fromCharCode(bytes[12], bytes[13], bytes[14], bytes[15]); + + if (chunkTag === 'VP8 ') { + // Lossy VP8: frame header starts at byte 20 (after 12-byte RIFF header + 8 chunk header) + // Bytes 26-27: width (LE uint16, lower 14 bits), 28-29: height (LE uint16, lower 14 bits) + if (bytes.length < 30) return null; + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const width = view.getUint16(26, true) & 0x3fff; + const height = view.getUint16(28, true) & 0x3fff; + if (width > 0 && height > 0) return { width, height }; + return null; + } + + if (chunkTag === 'VP8L') { + // Lossless VP8L: signature byte at 21, then 4 bytes of packed width/height + if (bytes.length < 25) return null; + // Bytes 21-24 contain packed dimensions (after 0x2f signature byte at offset 21) + const b0 = bytes[21]; + const b1 = bytes[22]; + const b2 = bytes[23]; + const b3 = bytes[24]; + const width = (((b1 & 0x3f) << 8) | b0) + 1; + const height = (((b3 & 0x0f) << 10) | (b2 << 2) | (b1 >> 6)) + 1; + if (width > 0 && height > 0) return { width, height }; + return null; + } + + if (chunkTag === 'VP8X') { + // Extended VP8X: canvas size at bytes 24-29 + // width = 24-bit LE uint at byte 24 + 1, height = 24-bit LE uint at byte 27 + 1 + if (bytes.length < 30) return null; + const width = (bytes[24] | (bytes[25] << 8) | (bytes[26] << 16)) + 1; + const height = (bytes[27] | (bytes[28] << 8) | (bytes[29] << 16)) + 1; + if (width > 0 && height > 0) return { width, height }; + return null; + } + + return null; +} + +/** + * Extract dimensions from a data URI's base64 payload. + * + * @param {string} dataUri - A data URI (e.g. "data:image/png;base64,...") + * @returns {{ width: number, height: number } | null} Dimensions or null + */ +export function readImageDimensionsFromDataUri(dataUri) { + if (typeof dataUri !== 'string' || !dataUri.startsWith('data:')) return null; + + const commaIndex = dataUri.indexOf(','); + if (commaIndex === -1) return null; + + const base64Payload = dataUri.slice(commaIndex + 1); + if (!base64Payload) return null; + + try { + const bytes = base64ToUint8Array(base64Payload); + return readImageDimensions(bytes); + } catch { + return null; + } +} diff --git a/packages/super-editor/src/core/super-converter/image-dimensions.test.js b/packages/super-editor/src/core/super-converter/image-dimensions.test.js new file mode 100644 index 0000000000..2afbb1a044 --- /dev/null +++ b/packages/super-editor/src/core/super-converter/image-dimensions.test.js @@ -0,0 +1,235 @@ +import { describe, it, expect } from 'vitest'; +import { readImageDimensions, readImageDimensionsFromDataUri } from './image-dimensions.js'; + +// --------------------------------------------------------------------------- +// Helpers to build minimal valid headers +// --------------------------------------------------------------------------- + +function pngHeader(width, height) { + // Minimal PNG: 8-byte signature + IHDR chunk (13 data bytes = width(4) + height(4) + depth(1) + colorType(1) + compression(1) + filter(1) + interlace(1)) + const buf = new ArrayBuffer(33); + const view = new DataView(buf); + const bytes = new Uint8Array(buf); + + // PNG signature + bytes.set([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + // IHDR chunk: length (13) + view.setUint32(8, 13); + // IHDR tag + bytes.set([0x49, 0x48, 0x44, 0x52], 12); + // width + height (big-endian) + view.setInt32(16, width); + view.setInt32(20, height); + + return bytes; +} + +function jpegHeader(width, height) { + // SOI + APP0 (minimal) + SOF0 with dimensions + const bytes = new Uint8Array(20); + const view = new DataView(bytes.buffer); + + // SOI + bytes[0] = 0xff; + bytes[1] = 0xd8; + // APP0 marker (will be skipped) + bytes[2] = 0xff; + bytes[3] = 0xe0; + view.setUint16(4, 5); // segment length = 5 (minimum: 2 + 3 bytes) + bytes[6] = 0x00; + bytes[7] = 0x00; + bytes[8] = 0x00; + // SOF0 marker + bytes[9] = 0xff; + bytes[10] = 0xc0; + view.setUint16(11, 8); // segment length + bytes[13] = 8; // precision + view.setUint16(14, height); + view.setUint16(16, width); + + return bytes; +} + +function gifHeader(width, height) { + // GIF89a + logical screen descriptor + const bytes = new Uint8Array(13); + const view = new DataView(bytes.buffer); + + // GIF89a signature + bytes.set([0x47, 0x49, 0x46, 0x38, 0x39, 0x61]); + // Width + height (little-endian uint16) + view.setUint16(6, width, true); + view.setUint16(8, height, true); + + return bytes; +} + +function bmpHeader(width, height) { + // BM + file header (14 bytes) + DIB header start with width/height + const bytes = new Uint8Array(26); + const view = new DataView(bytes.buffer); + + bytes[0] = 0x42; // B + bytes[1] = 0x4d; // M + // Skip file header bytes 2-13 + // DIB header: width at offset 18, height at offset 22 (little-endian int32) + view.setInt32(18, width, true); + view.setInt32(22, height, true); + + return bytes; +} + +function webpVP8Header(width, height) { + // RIFF....WEBP VP8 chunk with dimensions + const bytes = new Uint8Array(30); + const view = new DataView(bytes.buffer); + + // RIFF header + bytes.set([0x52, 0x49, 0x46, 0x46]); // RIFF + view.setUint32(4, 22, true); // file size (not critical for parsing) + bytes.set([0x57, 0x45, 0x42, 0x50], 8); // WEBP + // VP8 chunk + bytes.set([0x56, 0x50, 0x38, 0x20], 12); // "VP8 " + view.setUint32(16, 10, true); // chunk size + // Frame header: 3 bytes of frame tag, then keyframe sync code (0x9D012A) + bytes[20] = 0x9d; + bytes[21] = 0x01; + bytes[22] = 0x2a; + // Padding bytes + bytes[23] = 0x00; + bytes[24] = 0x00; + bytes[25] = 0x00; + // width at 26-27 (LE uint16, lower 14 bits), height at 28-29 + view.setUint16(26, width, true); + view.setUint16(28, height, true); + + return bytes; +} + +function webpVP8XHeader(width, height) { + // RIFF....WEBP VP8X chunk with canvas dimensions + const bytes = new Uint8Array(30); + + bytes.set([0x52, 0x49, 0x46, 0x46]); // RIFF + bytes.set([0x57, 0x45, 0x42, 0x50], 8); // WEBP + bytes.set([0x56, 0x50, 0x38, 0x58], 12); // "VP8X" + // VP8X chunk size at 16 (LE uint32) - 10 + bytes[16] = 10; + // Flags at 20 + bytes[20] = 0x00; + // Reserved bytes 21-23 + // Canvas width at 24-26 (24-bit LE, value = width - 1) + const w = width - 1; + bytes[24] = w & 0xff; + bytes[25] = (w >> 8) & 0xff; + bytes[26] = (w >> 16) & 0xff; + // Canvas height at 27-29 (24-bit LE, value = height - 1) + const h = height - 1; + bytes[27] = h & 0xff; + bytes[28] = (h >> 8) & 0xff; + bytes[29] = (h >> 16) & 0xff; + + return bytes; +} + +function toDataUri(bytes, mimeType) { + let binary = ''; + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]); + } + return `data:${mimeType};base64,${btoa(binary)}`; +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe('readImageDimensions', () => { + it('reads PNG dimensions', () => { + expect(readImageDimensions(pngHeader(800, 600))).toEqual({ width: 800, height: 600 }); + }); + + it('reads JPEG dimensions', () => { + expect(readImageDimensions(jpegHeader(1024, 768))).toEqual({ width: 1024, height: 768 }); + }); + + it('reads GIF dimensions', () => { + expect(readImageDimensions(gifHeader(320, 240))).toEqual({ width: 320, height: 240 }); + }); + + it('reads BMP dimensions', () => { + expect(readImageDimensions(bmpHeader(640, 480))).toEqual({ width: 640, height: 480 }); + }); + + it('reads BMP with negative height (top-down)', () => { + expect(readImageDimensions(bmpHeader(640, -480))).toEqual({ width: 640, height: 480 }); + }); + + it('reads WEBP VP8 (lossy) dimensions', () => { + expect(readImageDimensions(webpVP8Header(400, 300))).toEqual({ width: 400, height: 300 }); + }); + + it('reads WEBP VP8X (extended) dimensions', () => { + expect(readImageDimensions(webpVP8XHeader(1920, 1080))).toEqual({ width: 1920, height: 1080 }); + }); + + it('returns null for empty bytes', () => { + expect(readImageDimensions(new Uint8Array(0))).toBeNull(); + }); + + it('returns null for truncated PNG', () => { + const truncated = pngHeader(800, 600).slice(0, 18); + expect(readImageDimensions(truncated)).toBeNull(); + }); + + it('returns null for unknown format', () => { + const unknown = new Uint8Array(32); + unknown.fill(0xab); + expect(readImageDimensions(unknown)).toBeNull(); + }); + + it('returns null for non-Uint8Array input', () => { + expect(readImageDimensions('not bytes')).toBeNull(); + expect(readImageDimensions(null)).toBeNull(); + expect(readImageDimensions(undefined)).toBeNull(); + }); + + it('returns null for PNG with zero dimensions', () => { + expect(readImageDimensions(pngHeader(0, 600))).toBeNull(); + expect(readImageDimensions(pngHeader(800, 0))).toBeNull(); + }); +}); + +describe('readImageDimensionsFromDataUri', () => { + it('reads PNG dimensions from data URI', () => { + const uri = toDataUri(pngHeader(800, 600), 'image/png'); + expect(readImageDimensionsFromDataUri(uri)).toEqual({ width: 800, height: 600 }); + }); + + it('reads JPEG dimensions from data URI', () => { + const uri = toDataUri(jpegHeader(1024, 768), 'image/jpeg'); + expect(readImageDimensionsFromDataUri(uri)).toEqual({ width: 1024, height: 768 }); + }); + + it('reads GIF dimensions from data URI', () => { + const uri = toDataUri(gifHeader(320, 240), 'image/gif'); + expect(readImageDimensionsFromDataUri(uri)).toEqual({ width: 320, height: 240 }); + }); + + it('returns null for non-data-URI string', () => { + expect(readImageDimensionsFromDataUri('https://example.com/image.png')).toBeNull(); + }); + + it('returns null for malformed data URI', () => { + expect(readImageDimensionsFromDataUri('data:image/png')).toBeNull(); // no comma + }); + + it('returns null for empty base64 payload', () => { + expect(readImageDimensionsFromDataUri('data:image/png;base64,')).toBeNull(); + }); + + it('returns null for non-string input', () => { + expect(readImageDimensionsFromDataUri(null)).toBeNull(); + expect(readImageDimensionsFromDataUri(123)).toBeNull(); + }); +}); diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/wp/anchor/helpers/translate-anchor-node.js b/packages/super-editor/src/core/super-converter/v3/handlers/wp/anchor/helpers/translate-anchor-node.js index 468074c0fb..6235fed7aa 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/wp/anchor/helpers/translate-anchor-node.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/wp/anchor/helpers/translate-anchor-node.js @@ -72,7 +72,11 @@ export function translateAnchorNode(params) { ...(nodeElements.attributes || {}), }; - if (inlineAttrs.relativeHeight == null) { + // Prefer the live top-level relativeHeight (updated by images.setZOrder) + // over the stale value in originalAttributes. + if (attrs.relativeHeight != null) { + inlineAttrs.relativeHeight = attrs.relativeHeight; + } else if (inlineAttrs.relativeHeight == null) { inlineAttrs.relativeHeight = 1; } diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/decode-image-node-helpers.js b/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/decode-image-node-helpers.js index 14213b03f1..cd9358ec23 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/decode-image-node-helpers.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/decode-image-node-helpers.js @@ -3,6 +3,7 @@ import { getFallbackImageNameFromDataUri, sanitizeDocxMediaName } from '@convert import { prepareTextAnnotation } from '@converter/v3/handlers/w/sdt/helpers/translate-field-annotation.js'; import { wrapTextInRun } from '@converter/exporter.js'; import { generateDocxRandomId } from '@core/helpers/index.js'; +import { readImageDimensionsFromDataUri } from '@converter/image-dimensions.js'; /** * Decodes image into export XML @@ -24,7 +25,6 @@ export const translateImageNode = (params) => { // Prefer originalSrc for round-trip fidelity (e.g., EMF/WMF files converted to SVG for display) const src = attrs.originalSrc || attrs.src || attrs.imageSrc; - const { originalWidth, originalHeight } = getPngDimensions(src); let imageName; if (params.node.type === 'image') { @@ -38,21 +38,35 @@ export const translateImageNode = (params) => { } imageName = sanitizeDocxMediaName(imageName); - let size = attrs.size - ? { - w: pixelsToEmu(attrs.size.width), - h: pixelsToEmu(attrs.size.height), - } - : imageSize; - - if (originalWidth && originalHeight) { - const boxWidthPx = emuToPixels(size.w); - const boxHeightPx = emuToPixels(size.h); - const { scaledWidth, scaledHeight } = getScaledSize(originalWidth, originalHeight, boxWidthPx, boxHeightPx); - size = { - w: pixelsToEmu(scaledWidth), - h: pixelsToEmu(scaledHeight), - }; + // For fieldAnnotations without a recognizable MIME type, fall back to text + // annotation before attempting size resolution (they have no image data). + if (params.node.type === 'fieldAnnotation' && !imageId) { + const type = src?.split(';')[0].split('/')[1]; + if (!type) { + return prepareTextAnnotation(params); + } + } + + let size = resolveExportSize(attrs, imageSize, src); + + // Scale box size to match intrinsic PNG aspect ratio (legacy behavior). + // Only applies to PNG data URIs — the old getPngDimensions only supported PNG. + if (src?.startsWith('data:image/png')) { + const intrinsicDims = readImageDimensionsFromDataUri(src); + if (intrinsicDims) { + const boxWidthPx = emuToPixels(size.w); + const boxHeightPx = emuToPixels(size.h); + const { scaledWidth, scaledHeight } = getScaledSize( + intrinsicDims.width, + intrinsicDims.height, + boxWidthPx, + boxHeightPx, + ); + size = { + w: pixelsToEmu(scaledWidth), + h: pixelsToEmu(scaledHeight), + }; + } } if (tableCell) { @@ -78,10 +92,8 @@ export const translateImageNode = (params) => { const path = src?.split('word/')[1]; imageId = addNewImageRelationship(params, path); } else if (params.node.type === 'fieldAnnotation' && !imageId) { + // We already handled the no-type case above; here the type IS valid. const type = src?.split(';')[0].split('/')[1]; - if (!type) { - return prepareTextAnnotation(params); - } const sanitizedHash = sanitizeDocxMediaName(attrs.hash, generateDocxRandomId(4)); const fileName = `${imageName}_${sanitizedHash}.${type}`; @@ -266,24 +278,44 @@ export const translateImageNode = (params) => { }; }; -function getPngDimensions(base64) { - if (!base64) return {}; +function isFinitePositive(value) { + return typeof value === 'number' && Number.isFinite(value) && value > 0; +} - const type = base64.split(';')[0].split('/')[1]; - if (!base64 || type !== 'png') { - return { - originalWidth: undefined, - originalHeight: undefined, - }; +/** + * Resolve export size from available sources, with strict validation. + * + * Priority: + * 1. attrs.size with valid finite positive dimensions + * 2. imageSize fallback (from paragraph measure) + * 3. Infer from data URI source bytes + * 4. Legacy fallback: use attrs.size / imageSize as-is (may produce NaN — matches pre-hardening behavior) + * + * @returns {{ w: number, h: number }} + */ +function resolveExportSize(attrs, imageSize, src) { + if (isFinitePositive(attrs.size?.width) && isFinitePositive(attrs.size?.height)) { + return { w: pixelsToEmu(attrs.size.width), h: pixelsToEmu(attrs.size.height) }; } - - let header = base64.split(',')[1].slice(0, 50); - let uint8 = Uint8Array.from(atob(header), (c) => c.charCodeAt(0)); - let dataView = new DataView(uint8.buffer, 0, 28); - + if (isFinitePositive(imageSize?.w) && isFinitePositive(imageSize?.h)) { + return imageSize; + } + if (src?.startsWith('data:')) { + const dims = readImageDimensionsFromDataUri(src); + if (dims) return { w: pixelsToEmu(dims.width), h: pixelsToEmu(dims.height) }; + } + // Legacy fallback: preserve old behavior for callers that pass + // non-validated imageSize or attrs.size (e.g., file-path images without + // explicit dimensions). The create.image path validates upstream. + const raw = attrs.size + ? { w: pixelsToEmu(attrs.size.width), h: pixelsToEmu(attrs.size.height) } + : imageSize || { w: 0, h: 0 }; + + // Clamp non-finite or non-positive values to 1 EMU so we never emit + // NaN or zero in / — both produce corrupt OOXML. return { - originalWidth: dataView.getInt32(16), - originalHeight: dataView.getInt32(20), + w: isFinitePositive(raw.w) ? raw.w : 1, + h: isFinitePositive(raw.h) ? raw.h : 1, }; } diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.js b/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.js index de4803de08..ea85a0d247 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.js @@ -1,3 +1,4 @@ +import { v5 as uuidv5 } from 'uuid'; import { emuToPixels, rotToDegrees, polygonToObj } from '@converter/helpers.js'; import { carbonCopy } from '@core/utilities/carbonCopy.js'; import { @@ -22,6 +23,13 @@ const DRAWING_XML_TAG = 'w:drawing'; const SHAPE_URI = 'http://schemas.microsoft.com/office/word/2010/wordprocessingShape'; const GROUP_URI = 'http://schemas.microsoft.com/office/word/2010/wordprocessingGroup'; +/** + * Namespace UUID for generating deterministic sdImageId values. + * Images imported from DOCX derive their sdImageId from rEmbed + document-part + * filename so the same image always receives the same ID across open cycles. + */ +const SD_IMAGE_ID_NAMESPACE = '7c9e6679-7425-40de-944b-e07fc1f90ae7'; + /** * Normalize a relationship target to a relative media path. * Strips leading slashes and collapses duplicated "word/" prefixes so lookups @@ -429,7 +437,19 @@ export function handleImageNode(node, params, isAnchor) { // which is not what we want for placeholder images that should maintain their original layout. const wrapValue = wrap; + // Extract relativeHeight from anchor attributes for first-class z-order support + const rawRelativeHeight = isAnchor ? Number(attributes['relativeHeight']) : null; + const relativeHeight = rawRelativeHeight != null && Number.isFinite(rawRelativeHeight) ? rawRelativeHeight : null; + + // Derive a deterministic sdImageId from the drawing's docPr id, the rEmbed, + // and the document-part filename so the same image always receives the same + // stable ID across multiple opens of the same DOCX. + const docPrId = docPr?.attributes?.id ?? ''; + const sdImageId = uuidv5(`${currentFile}:${rEmbed}:${docPrId}`, SD_IMAGE_ID_NAMESPACE); + const nodeAttrs = { + sdImageId, + relativeHeight, // originalXml: carbonCopy(node), src: finalSrc, alt: diff --git a/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts b/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts index a3b29f7d51..b2b39e63a2 100644 --- a/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts +++ b/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts @@ -94,6 +94,20 @@ import { tocUnmarkEntryWrapper, tocEditEntryWrapper, } from '../plan-engine/toc-entry-wrappers.js'; +import { + createImageWrapper, + imagesDeleteWrapper, + imagesMoveWrapper, + imagesConvertToInlineWrapper, + imagesConvertToFloatingWrapper, + imagesSetSizeWrapper, + imagesSetWrapTypeWrapper, + imagesSetWrapSideWrapper, + imagesSetWrapDistancesWrapper, + imagesSetPositionWrapper, + imagesSetAnchorOptionsWrapper, + imagesSetZOrderWrapper, +} from '../plan-engine/images-wrappers.js'; import { listsInsertWrapper, listsIndentWrapper, @@ -1905,6 +1919,147 @@ function getFirstTocEntryAddress(editor: Editor): { kind: 'inline'; nodeType: 't }; } +/** + * Creates a mock editor containing one floating image node inside a paragraph. + * The image has `sdImageId: 'img-1'`, `isAnchor: true`, and `wrap: { type: 'Square' }`. + */ +function makeImageEditor(): Editor { + const imageNode = createNode('image', [], { + attrs: { + sdImageId: 'img-1', + src: 'https://example.com/test.png', + alt: 'Test image', + isAnchor: true, + wrap: { type: 'Square', attrs: { wrapText: 'bothSides' } }, + anchorData: { hRelativeFrom: 'column', vRelativeFrom: 'paragraph' }, + marginOffset: null, + relativeHeight: 251658240, + originalAttributes: {}, + size: { width: 100, height: 100 }, + }, + isInline: true, + isLeaf: true, + }); + const paragraph = createNode('paragraph', [imageNode], { + attrs: { sdBlockId: 'p-img' }, + isBlock: true, + inlineContent: true, + }); + const doc = createNode('doc', [paragraph], { isBlock: false }); + + const dispatch = vi.fn(); + const tr = { + insertText: vi.fn().mockReturnThis(), + delete: vi.fn().mockReturnThis(), + insert: vi.fn().mockReturnThis(), + setNodeMarkup: vi.fn().mockReturnThis(), + replaceWith: vi.fn().mockReturnThis(), + setMeta: vi.fn().mockReturnThis(), + mapping: { map: (pos: number) => pos }, + docChanged: true, + steps: [{}], + doc, + }; + + return { + state: { + doc, + tr, + schema: { + nodes: { + image: { + create: vi.fn((attrs: Record) => + createNode('image', [], { attrs, isInline: true, isLeaf: true }), + ), + }, + }, + }, + }, + dispatch, + commands: { + setImage: vi.fn(() => true), + }, + schema: { marks: {} }, + options: {}, + on: () => {}, + } as unknown as Editor; +} + +/** + * Editor with two paragraphs to make image before/after/inParagraph positioning meaningful. + * p1 contains one floating image (img-1), p2 contains text ("Hello"). + */ +function makeMultiBlockImageEditor(): Editor { + const imageNode = createNode('image', [], { + attrs: { + sdImageId: 'img-1', + src: 'https://example.com/test.png', + isAnchor: true, + wrap: { type: 'Square', attrs: { wrapText: 'bothSides' } }, + anchorData: { hRelativeFrom: 'column', vRelativeFrom: 'paragraph' }, + marginOffset: null, + relativeHeight: 251658240, + originalAttributes: {}, + size: { width: 100, height: 100 }, + }, + isInline: true, + isLeaf: true, + }); + // p1: pos=0, nodeSize=3 (1 inline image + 2 wrapper) + const p1 = createNode('paragraph', [imageNode], { + attrs: { sdBlockId: 'p-img' }, + isBlock: true, + inlineContent: true, + }); + const textNode = createNode('text', [], { text: 'Hello' }); + // p2: pos=3, nodeSize=7 (5 text chars + 2 wrapper) + const p2 = createNode('paragraph', [textNode], { + attrs: { sdBlockId: 'p-text' }, + isBlock: true, + inlineContent: true, + }); + const doc = createNode('doc', [p1, p2], { isBlock: false }); + // doc.content.size = 10 + + const dispatch = vi.fn(); + const tr = { + insertText: vi.fn().mockReturnThis(), + delete: vi.fn().mockReturnThis(), + insert: vi.fn().mockReturnThis(), + setNodeMarkup: vi.fn().mockReturnThis(), + replaceWith: vi.fn().mockReturnThis(), + setMeta: vi.fn().mockReturnThis(), + mapping: { map: (pos: number) => pos }, + docChanged: true, + steps: [{}], + doc, + }; + + return { + state: { + doc, + tr, + schema: { + nodes: { + image: { + create: vi.fn((attrs: Record) => + createNode('image', [], { attrs, isInline: true, isLeaf: true }), + ), + }, + }, + }, + }, + dispatch, + commands: { + setImage: vi.fn(() => true), + insertContentAt: vi.fn(() => true), + }, + schema: { marks: {} }, + options: {}, + on: () => {}, + } as unknown as Editor; +} + const mutationVectors: Partial> = { 'blocks.delete': { throwCase: () => { @@ -4017,6 +4172,297 @@ const mutationVectors: Partial> = { ); }, }, + + // ------------------------------------------------------------------------- + // Image operations + // ------------------------------------------------------------------------- + 'create.image': { + throwCase: () => { + // setImage command missing → CAPABILITY_UNAVAILABLE + const editor = makeImageEditor(); + (editor.commands as Record).setImage = undefined; + return createImageWrapper( + editor, + { src: 'https://example.com/img.png', size: { width: 100, height: 100 } }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + // URL src without explicit size → INVALID_INPUT (cannot infer dimensions) + const editor = makeImageEditor(); + return createImageWrapper(editor, { src: 'https://example.com/img.png' }, { changeMode: 'direct' }); + }, + applyCase: () => { + return createImageWrapper( + makeImageEditor(), + { src: 'https://example.com/img.png', size: { width: 100, height: 100 } }, + { changeMode: 'direct' }, + ); + }, + }, + 'images.delete': { + throwCase: () => imagesDeleteWrapper(makeImageEditor(), { imageId: 'missing' }, { changeMode: 'direct' }), + failureCase: () => { + // Transaction produces no change → NO_OP + const editor = makeImageEditor(); + const tr = (editor.state as unknown as { tr: Record }).tr; + tr.docChanged = false; + tr.steps = []; + return imagesDeleteWrapper(editor, { imageId: 'img-1' }, { changeMode: 'direct' }); + }, + applyCase: () => imagesDeleteWrapper(makeImageEditor(), { imageId: 'img-1' }, { changeMode: 'direct' }), + }, + 'images.move': { + throwCase: () => + imagesMoveWrapper( + makeImageEditor(), + { imageId: 'missing', to: { kind: 'documentEnd' } }, + { changeMode: 'direct' }, + ), + failureCase: () => { + // Transaction produces no change → NO_OP + const editor = makeImageEditor(); + const tr = (editor.state as unknown as { tr: Record }).tr; + tr.docChanged = false; + tr.steps = []; + return imagesMoveWrapper(editor, { imageId: 'img-1', to: { kind: 'documentEnd' } }, { changeMode: 'direct' }); + }, + applyCase: () => + imagesMoveWrapper(makeImageEditor(), { imageId: 'img-1', to: { kind: 'documentEnd' } }, { changeMode: 'direct' }), + }, + 'images.convertToInline': { + throwCase: () => imagesConvertToInlineWrapper(makeImageEditor(), { imageId: 'missing' }, { changeMode: 'direct' }), + failureCase: () => { + // Already inline → NO_OP + const inlineImg = createNode('image', [], { + attrs: { + sdImageId: 'img-inline-noop', + src: 'test.png', + isAnchor: false, + wrap: { type: 'Inline' }, + anchorData: null, + marginOffset: null, + relativeHeight: null, + originalAttributes: {}, + }, + isInline: true, + isLeaf: true, + }); + const p = createNode('paragraph', [inlineImg], { + attrs: { sdBlockId: 'p-x' }, + isBlock: true, + inlineContent: true, + }); + const doc = createNode('doc', [p], { isBlock: false }); + const editor = { + state: { doc, tr: {}, schema: { nodes: {} } }, + dispatch: vi.fn(), + commands: { setImage: vi.fn(() => true) }, + schema: { marks: {} }, + options: {}, + on: () => {}, + } as unknown as Editor; + return imagesConvertToInlineWrapper(editor, { imageId: 'img-inline-noop' }, { changeMode: 'direct' }); + }, + applyCase: () => imagesConvertToInlineWrapper(makeImageEditor(), { imageId: 'img-1' }, { changeMode: 'direct' }), + }, + 'images.convertToFloating': { + throwCase: () => + imagesConvertToFloatingWrapper(makeImageEditor(), { imageId: 'missing' }, { changeMode: 'direct' }), + failureCase: () => { + // Already floating → NO_OP + return imagesConvertToFloatingWrapper(makeImageEditor(), { imageId: 'img-1' }, { changeMode: 'direct' }); + }, + applyCase: () => { + const inlineImg = createNode('image', [], { + attrs: { + sdImageId: 'img-for-float', + src: 'test.png', + isAnchor: false, + wrap: { type: 'Inline' }, + anchorData: null, + marginOffset: null, + relativeHeight: null, + originalAttributes: {}, + }, + isInline: true, + isLeaf: true, + }); + const p = createNode('paragraph', [inlineImg], { + attrs: { sdBlockId: 'p-f' }, + isBlock: true, + inlineContent: true, + }); + const doc = createNode('doc', [p], { isBlock: false }); + const tr = { + setNodeMarkup: vi.fn().mockReturnThis(), + setMeta: vi.fn().mockReturnThis(), + mapping: { map: (pos: number) => pos }, + docChanged: true, + steps: [{}], + doc, + }; + const editor = { + state: { doc, tr, schema: { nodes: {} } }, + dispatch: vi.fn(), + commands: { setImage: vi.fn(() => true) }, + schema: { marks: {} }, + options: {}, + on: () => {}, + } as unknown as Editor; + return imagesConvertToFloatingWrapper(editor, { imageId: 'img-for-float' }, { changeMode: 'direct' }); + }, + }, + 'images.setSize': { + throwCase: () => + imagesSetSizeWrapper( + makeImageEditor(), + { imageId: 'missing', size: { width: 220, height: 140 } }, + { changeMode: 'direct' }, + ), + failureCase: () => { + // Same size → NO_OP + return imagesSetSizeWrapper( + makeImageEditor(), + { imageId: 'img-1', size: { width: 100, height: 100 } }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => + imagesSetSizeWrapper( + makeImageEditor(), + { imageId: 'img-1', size: { width: 220, height: 140 } }, + { changeMode: 'direct' }, + ), + }, + 'images.setWrapType': { + throwCase: () => + imagesSetWrapTypeWrapper(makeImageEditor(), { imageId: 'missing', type: 'Tight' }, { changeMode: 'direct' }), + failureCase: () => { + // Same type → NO_OP + return imagesSetWrapTypeWrapper( + makeImageEditor(), + { imageId: 'img-1', type: 'Square' }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => + imagesSetWrapTypeWrapper(makeImageEditor(), { imageId: 'img-1', type: 'Tight' }, { changeMode: 'direct' }), + }, + 'images.setWrapSide': { + throwCase: () => + imagesSetWrapSideWrapper(makeImageEditor(), { imageId: 'missing', side: 'left' }, { changeMode: 'direct' }), + failureCase: () => { + // Same side → NO_OP + return imagesSetWrapSideWrapper( + makeImageEditor(), + { imageId: 'img-1', side: 'bothSides' }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => + imagesSetWrapSideWrapper(makeImageEditor(), { imageId: 'img-1', side: 'left' }, { changeMode: 'direct' }), + }, + 'images.setWrapDistances': { + throwCase: () => + imagesSetWrapDistancesWrapper( + makeImageEditor(), + { imageId: 'missing', distances: { distTop: 100 } }, + { changeMode: 'direct' }, + ), + failureCase: () => { + // Transaction produces no change → NO_OP + const editor = makeImageEditor(); + const tr = (editor.state as unknown as { tr: Record }).tr; + tr.docChanged = false; + tr.steps = []; + return imagesSetWrapDistancesWrapper( + editor, + { imageId: 'img-1', distances: { distTop: 100 } }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => + imagesSetWrapDistancesWrapper( + makeImageEditor(), + { imageId: 'img-1', distances: { distTop: 100 } }, + { changeMode: 'direct' }, + ), + }, + 'images.setPosition': { + throwCase: () => + imagesSetPositionWrapper( + makeImageEditor(), + { imageId: 'missing', position: { hRelativeFrom: 'page' } }, + { changeMode: 'direct' }, + ), + failureCase: () => { + // Transaction produces no change → NO_OP + const editor = makeImageEditor(); + const tr = (editor.state as unknown as { tr: Record }).tr; + tr.docChanged = false; + tr.steps = []; + return imagesSetPositionWrapper( + editor, + { imageId: 'img-1', position: { hRelativeFrom: 'page' } }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => + imagesSetPositionWrapper( + makeImageEditor(), + { imageId: 'img-1', position: { hRelativeFrom: 'page' } }, + { changeMode: 'direct' }, + ), + }, + 'images.setAnchorOptions': { + throwCase: () => + imagesSetAnchorOptionsWrapper( + makeImageEditor(), + { imageId: 'missing', options: { behindDoc: true } }, + { changeMode: 'direct' }, + ), + failureCase: () => { + // Transaction produces no change → NO_OP + const editor = makeImageEditor(); + const tr = (editor.state as unknown as { tr: Record }).tr; + tr.docChanged = false; + tr.steps = []; + return imagesSetAnchorOptionsWrapper( + editor, + { imageId: 'img-1', options: { behindDoc: true } }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => + imagesSetAnchorOptionsWrapper( + makeImageEditor(), + { imageId: 'img-1', options: { behindDoc: true } }, + { changeMode: 'direct' }, + ), + }, + 'images.setZOrder': { + throwCase: () => + imagesSetZOrderWrapper( + makeImageEditor(), + { imageId: 'missing', zOrder: { relativeHeight: 999 } }, + { changeMode: 'direct' }, + ), + failureCase: () => { + // Same relativeHeight → NO_OP + return imagesSetZOrderWrapper( + makeImageEditor(), + { imageId: 'img-1', zOrder: { relativeHeight: 251658240 } }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => + imagesSetZOrderWrapper( + makeImageEditor(), + { imageId: 'img-1', zOrder: { relativeHeight: 999999999 } }, + { changeMode: 'direct' }, + ), + }, }; const dryRunVectors: Partial unknown>> = { @@ -4968,6 +5414,170 @@ const dryRunVectors: Partial unknown>> = { expect(updateEntry).not.toHaveBeenCalled(); return result; }, + + // ------------------------------------------------------------------------- + // Image operations — dryRun vectors + // ------------------------------------------------------------------------- + 'create.image': () => { + const setImage = vi.fn(() => true); + const { editor } = makeTextEditor('Hello', { commands: { setImage } }); + const result = createImageWrapper( + editor, + { src: 'https://example.com/img.png', size: { width: 100, height: 100 } }, + { changeMode: 'direct', dryRun: true }, + ); + expect(setImage).not.toHaveBeenCalled(); + return result; + }, + 'images.delete': () => { + const editor = makeImageEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = imagesDeleteWrapper(editor, { imageId: 'img-1' }, { changeMode: 'direct', dryRun: true }); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'images.move': () => { + const editor = makeImageEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = imagesMoveWrapper( + editor, + { imageId: 'img-1', to: { kind: 'documentEnd' } }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'images.convertToInline': () => { + const editor = makeImageEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = imagesConvertToInlineWrapper(editor, { imageId: 'img-1' }, { changeMode: 'direct', dryRun: true }); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'images.convertToFloating': () => { + // Need an inline image for convertToFloating to be non-no-op + const inlineImageNode = createNode('image', [], { + attrs: { + sdImageId: 'img-inline', + src: 'https://example.com/test.png', + isAnchor: false, + wrap: { type: 'Inline' }, + anchorData: null, + marginOffset: null, + relativeHeight: null, + originalAttributes: {}, + }, + isInline: true, + isLeaf: true, + }); + const paragraph = createNode('paragraph', [inlineImageNode], { + attrs: { sdBlockId: 'p-img-inline' }, + isBlock: true, + inlineContent: true, + }); + const doc = createNode('doc', [paragraph], { isBlock: false }); + const dispatch = vi.fn(); + const tr = { + setNodeMarkup: vi.fn().mockReturnThis(), + setMeta: vi.fn().mockReturnThis(), + mapping: { map: (pos: number) => pos }, + docChanged: true, + steps: [{}], + doc, + }; + const editor = { + state: { doc, tr, schema: { nodes: {} } }, + dispatch, + commands: { setImage: vi.fn(() => true) }, + schema: { marks: {} }, + options: {}, + on: () => {}, + } as unknown as Editor; + const result = imagesConvertToFloatingWrapper( + editor, + { imageId: 'img-inline' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'images.setSize': () => { + const editor = makeImageEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = imagesSetSizeWrapper( + editor, + { imageId: 'img-1', size: { width: 220, height: 140 } }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'images.setWrapType': () => { + const editor = makeImageEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = imagesSetWrapTypeWrapper( + editor, + { imageId: 'img-1', type: 'Tight' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'images.setWrapSide': () => { + const editor = makeImageEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = imagesSetWrapSideWrapper( + editor, + { imageId: 'img-1', side: 'left' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'images.setWrapDistances': () => { + const editor = makeImageEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = imagesSetWrapDistancesWrapper( + editor, + { imageId: 'img-1', distances: { distTop: 100, distBottom: 100 } }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'images.setPosition': () => { + const editor = makeImageEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = imagesSetPositionWrapper( + editor, + { imageId: 'img-1', position: { hRelativeFrom: 'page' } }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'images.setAnchorOptions': () => { + const editor = makeImageEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = imagesSetAnchorOptionsWrapper( + editor, + { imageId: 'img-1', options: { behindDoc: true } }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'images.setZOrder': () => { + const editor = makeImageEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = imagesSetZOrderWrapper( + editor, + { imageId: 'img-1', zOrder: { relativeHeight: 999999999 } }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, }; beforeEach(() => { @@ -5674,4 +6284,289 @@ describe('document-api adapter conformance', () => { expect(receipt.steps[0].effect, `${op} outcome should be 'changed'`).toBe('changed'); }, ); + + // ------------------------------------------------------------------------- + // Location semantics — coverage for create.image at / images.move to + // ------------------------------------------------------------------------- + + describe('image location semantics', () => { + /** Editor with two paragraphs to make before/after positions meaningful. */ + function makeMultiBlockImageEditor() { + const imageNode = createNode('image', [], { + attrs: { + sdImageId: 'img-1', + src: 'https://example.com/test.png', + isAnchor: true, + wrap: { type: 'Square', attrs: { wrapText: 'bothSides' } }, + anchorData: { hRelativeFrom: 'column', vRelativeFrom: 'paragraph' }, + marginOffset: null, + relativeHeight: 251658240, + originalAttributes: {}, + size: { width: 100, height: 100 }, + }, + isInline: true, + isLeaf: true, + }); + // p1: pos=0, nodeSize=3 (1 inline image + 2 wrapper) + const p1 = createNode('paragraph', [imageNode], { + attrs: { sdBlockId: 'p-img' }, + isBlock: true, + inlineContent: true, + }); + const textNode = createNode('text', [], { text: 'Hello' }); + // p2: pos=3, nodeSize=7 (5 text chars + 2 wrapper) + const p2 = createNode('paragraph', [textNode], { + attrs: { sdBlockId: 'p-text' }, + isBlock: true, + inlineContent: true, + }); + const doc = createNode('doc', [p1, p2], { isBlock: false }); + // doc.content.size = 10 + + const dispatch = vi.fn(); + const tr = { + insertText: vi.fn().mockReturnThis(), + delete: vi.fn().mockReturnThis(), + insert: vi.fn().mockReturnThis(), + setNodeMarkup: vi.fn().mockReturnThis(), + replaceWith: vi.fn().mockReturnThis(), + setMeta: vi.fn().mockReturnThis(), + mapping: { map: (pos: number) => pos }, + docChanged: true, + steps: [{}], + doc, + }; + + return { + state: { + doc, + tr, + schema: { + nodes: { + image: { + create: vi.fn((attrs: Record) => + createNode('image', [], { attrs, isInline: true, isLeaf: true }), + ), + }, + }, + }, + }, + dispatch, + commands: { + setImage: vi.fn(() => true), + insertContentAt: vi.fn(() => true), + }, + schema: { marks: {} }, + options: {}, + on: () => {}, + } as unknown as Editor; + } + + it('create.image with at: documentStart uses insertContentAt at position 0', () => { + const editor = makeMultiBlockImageEditor(); + const result = createImageWrapper( + editor, + { src: 'https://example.com/new.png', size: { width: 100, height: 100 }, at: { kind: 'documentStart' } }, + { changeMode: 'direct' }, + ); + expect(result.success).toBe(true); + expect((editor.commands as any).insertContentAt).toHaveBeenCalledWith( + 0, + expect.objectContaining({ type: 'image' }), + ); + expect((editor.commands as any).setImage).not.toHaveBeenCalled(); + }); + + it('create.image with at: documentEnd uses insertContentAt at content size', () => { + const editor = makeMultiBlockImageEditor(); + const result = createImageWrapper( + editor, + { src: 'https://example.com/new.png', size: { width: 100, height: 100 }, at: { kind: 'documentEnd' } }, + { changeMode: 'direct' }, + ); + expect(result.success).toBe(true); + expect((editor.commands as any).insertContentAt).toHaveBeenCalledWith( + 10, // doc.content.size + expect.objectContaining({ type: 'image' }), + ); + expect((editor.commands as any).setImage).not.toHaveBeenCalled(); + }); + + it('create.image with at: before resolves block insertion position', () => { + const editor = makeMultiBlockImageEditor(); + const result = createImageWrapper( + editor, + { + src: 'https://example.com/new.png', + size: { width: 100, height: 100 }, + at: { kind: 'before', target: { kind: 'block', nodeType: 'paragraph', nodeId: 'p-text' } }, + }, + { changeMode: 'direct' }, + ); + expect(result.success).toBe(true); + expect((editor.commands as any).insertContentAt).toHaveBeenCalledWith( + 3, // p-text starts at pos 3 + expect.objectContaining({ type: 'image' }), + ); + }); + + it('create.image with at: after resolves block end position', () => { + const editor = makeMultiBlockImageEditor(); + const result = createImageWrapper( + editor, + { + src: 'https://example.com/new.png', + size: { width: 100, height: 100 }, + at: { kind: 'after', target: { kind: 'block', nodeType: 'paragraph', nodeId: 'p-img' } }, + }, + { changeMode: 'direct' }, + ); + expect(result.success).toBe(true); + expect((editor.commands as any).insertContentAt).toHaveBeenCalledWith( + 3, // p-img ends at pos 3 (pos=0 + nodeSize=3) + expect.objectContaining({ type: 'image' }), + ); + }); + + it('create.image with at: inParagraph resolves inline offset position', () => { + const editor = makeMultiBlockImageEditor(); + const result = createImageWrapper( + editor, + { + src: 'https://example.com/new.png', + size: { width: 100, height: 100 }, + at: { kind: 'inParagraph', target: { kind: 'block', nodeType: 'paragraph', nodeId: 'p-text' }, offset: 2 }, + }, + { changeMode: 'direct' }, + ); + expect(result.success).toBe(true); + // p-text starts at pos 3, +1 enters inline content, +2 offset = 6 + expect((editor.commands as any).insertContentAt).toHaveBeenCalledWith( + 6, + expect.objectContaining({ type: 'image' }), + ); + }); + + it('create.image without at uses setImage (selection-based)', () => { + const editor = makeMultiBlockImageEditor(); + const result = createImageWrapper( + editor, + { src: 'https://example.com/new.png', size: { width: 100, height: 100 } }, + { changeMode: 'direct' }, + ); + expect(result.success).toBe(true); + expect((editor.commands as any).setImage).toHaveBeenCalled(); + expect((editor.commands as any).insertContentAt).not.toHaveBeenCalled(); + }); + + it('images.move with to: documentStart inserts at position 0', () => { + const editor = makeMultiBlockImageEditor(); + const result = imagesMoveWrapper( + editor, + { imageId: 'img-1', to: { kind: 'documentStart' } }, + { changeMode: 'direct' }, + ); + expect(result.success).toBe(true); + const tr = (editor.state as unknown as { tr: { insert: ReturnType } }).tr; + expect(tr.insert).toHaveBeenCalledWith(0, expect.anything()); + }); + + it('images.move with to: before resolves block position', () => { + const editor = makeMultiBlockImageEditor(); + const result = imagesMoveWrapper( + editor, + { + imageId: 'img-1', + to: { kind: 'before', target: { kind: 'block', nodeType: 'paragraph', nodeId: 'p-text' } }, + }, + { changeMode: 'direct' }, + ); + expect(result.success).toBe(true); + const tr = (editor.state as unknown as { tr: { insert: ReturnType } }).tr; + // p-text starts at pos 3, mapping.map(3) → 3 + expect(tr.insert).toHaveBeenCalledWith(3, expect.anything()); + }); + + it('images.move with to: after resolves block end position', () => { + const editor = makeMultiBlockImageEditor(); + const result = imagesMoveWrapper( + editor, + { imageId: 'img-1', to: { kind: 'after', target: { kind: 'block', nodeType: 'paragraph', nodeId: 'p-text' } } }, + { changeMode: 'direct' }, + ); + expect(result.success).toBe(true); + const tr = (editor.state as unknown as { tr: { insert: ReturnType } }).tr; + // p-text ends at pos 10, mapping.map(10) → 10 + expect(tr.insert).toHaveBeenCalledWith(10, expect.anything()); + }); + }); + + // ------------------------------------------------------------------------- + // Image dimension resolution & unique drawing ID + // ------------------------------------------------------------------------- + + describe('image dimension resolution', () => { + /** Minimal 1x1 PNG as data URI (valid IHDR with width=1, height=1). */ + function makePngDataUri(width: number, height: number): string { + // Build a minimal PNG header with the given width/height in IHDR + const buf = new ArrayBuffer(33); + const view = new DataView(buf); + const bytes = new Uint8Array(buf); + bytes.set([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); // PNG signature + view.setUint32(8, 13); // IHDR length + bytes.set([0x49, 0x48, 0x44, 0x52], 12); // IHDR tag + view.setInt32(16, width); + view.setInt32(20, height); + let binary = ''; + for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]); + return `data:image/png;base64,${btoa(binary)}`; + } + + it('create.image resolves dimensions from a data URI when size is omitted', () => { + const editor = makeImageEditor(); + const pngUri = makePngDataUri(200, 150); + const result = createImageWrapper(editor, { src: pngUri }, { changeMode: 'direct' }); + expect(result.success).toBe(true); + // The setImage command should have been called with resolved size + const setImage = (editor.commands as any).setImage; + const attrs = setImage.mock.calls[0]?.[0]; + expect(attrs.size).toEqual({ width: 200, height: 150 }); + }); + + it('create.image returns INVALID_INPUT when URL src has no size', () => { + const editor = makeImageEditor(); + const result = createImageWrapper(editor, { src: 'https://example.com/image.png' }, { changeMode: 'direct' }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.failure.code).toBe('INVALID_INPUT'); + } + }); + + it('create.image returns INVALID_INPUT for data URI with unsupported format', () => { + const editor = makeImageEditor(); + // A data URI that doesn't match any known image format + const badUri = `data:application/octet-stream;base64,${btoa('not a real image')}`; + const result = createImageWrapper(editor, { src: badUri }, { changeMode: 'direct' }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.failure.code).toBe('INVALID_INPUT'); + } + }); + + it('create.image assigns a unique drawing ID (attrs.id)', () => { + const editor = makeImageEditor(); + const result = createImageWrapper( + editor, + { src: 'https://example.com/img.png', size: { width: 100, height: 100 } }, + { changeMode: 'direct' }, + ); + expect(result.success).toBe(true); + const setImage = (editor.commands as any).setImage; + const attrs = setImage.mock.calls[0]?.[0]; + // id should be a non-empty string (numeric string from generateUniqueDocPrId) + expect(attrs.id).toBeDefined(); + expect(typeof attrs.id).toBe('string'); + expect(attrs.id.length).toBeGreaterThan(0); + }); + }); }); diff --git a/packages/super-editor/src/document-api-adapters/assemble-adapters.ts b/packages/super-editor/src/document-api-adapters/assemble-adapters.ts index 68ffed189b..c10ee40d1f 100644 --- a/packages/super-editor/src/document-api-adapters/assemble-adapters.ts +++ b/packages/super-editor/src/document-api-adapters/assemble-adapters.ts @@ -149,6 +149,22 @@ import { tocUnmarkEntryWrapper, tocEditEntryWrapper, } from './plan-engine/toc-entry-wrappers.js'; +import { + createImageWrapper, + imagesListWrapper, + imagesGetWrapper, + imagesDeleteWrapper, + imagesMoveWrapper, + imagesConvertToInlineWrapper, + imagesConvertToFloatingWrapper, + imagesSetSizeWrapper, + imagesSetWrapTypeWrapper, + imagesSetWrapSideWrapper, + imagesSetWrapDistancesWrapper, + imagesSetPositionWrapper, + imagesSetAnchorOptionsWrapper, + imagesSetZOrderWrapper, +} from './plan-engine/images-wrappers.js'; /** * Assembles all document-api adapters for the given editor instance. @@ -233,6 +249,7 @@ export function assembleDocumentApiAdapters(editor: Editor): DocumentApiAdapters table: (input, options) => createTableWrapper(editor, input, options), sectionBreak: (input, options) => createSectionBreakAdapter(editor, input, options), tableOfContents: (input, options) => createTableOfContentsWrapper(editor, input, options), + image: (input, options) => createImageWrapper(editor, input, options), }, lists: { list: (query) => listsListWrapper(editor, query), @@ -329,6 +346,22 @@ export function assembleDocumentApiAdapters(editor: Editor): DocumentApiAdapters getEntry: (input) => tocGetEntryWrapper(editor, input), editEntry: (input, options) => tocEditEntryWrapper(editor, input, options), }, + images: { + image: (input, options) => createImageWrapper(editor, input, options), + list: (input) => imagesListWrapper(editor, input), + get: (input) => imagesGetWrapper(editor, input), + delete: (input, options) => imagesDeleteWrapper(editor, input, options), + move: (input, options) => imagesMoveWrapper(editor, input, options), + convertToInline: (input, options) => imagesConvertToInlineWrapper(editor, input, options), + convertToFloating: (input, options) => imagesConvertToFloatingWrapper(editor, input, options), + setSize: (input, options) => imagesSetSizeWrapper(editor, input, options), + setWrapType: (input, options) => imagesSetWrapTypeWrapper(editor, input, options), + setWrapSide: (input, options) => imagesSetWrapSideWrapper(editor, input, options), + setWrapDistances: (input, options) => imagesSetWrapDistancesWrapper(editor, input, options), + setPosition: (input, options) => imagesSetPositionWrapper(editor, input, options), + setAnchorOptions: (input, options) => imagesSetAnchorOptionsWrapper(editor, input, options), + setZOrder: (input, options) => imagesSetZOrderWrapper(editor, input, options), + }, query: { match: (input) => queryMatchAdapter(editor, input), }, diff --git a/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts b/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts index fd71740a3a..39f993a988 100644 --- a/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts +++ b/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts @@ -105,6 +105,19 @@ const REQUIRED_COMMANDS: Partial { + if (node.type.name !== 'image') return; + + const sdImageId = node.attrs.sdImageId; + if (typeof sdImageId !== 'string' || sdImageId.length === 0) return; + + results.push({ + pos, + node, + sdImageId, + placement: node.attrs.isAnchor ? 'floating' : 'inline', + }); + }); + + return results; +} + +// --------------------------------------------------------------------------- +// Lookup helpers +// --------------------------------------------------------------------------- + +/** + * Finds a single image by `sdImageId`. + * + * @throws {DocumentApiAdapterError} `TARGET_NOT_FOUND` when no image has the given ID. + * @throws {DocumentApiAdapterError} `AMBIGUOUS_TARGET` when multiple images share the same ID. + */ +export function findImageById(editor: Editor, sdImageId: string): ImageCandidate { + const candidates = collectImages(editor.state.doc); + const matches = candidates.filter((c) => c.sdImageId === sdImageId); + + if (matches.length === 0) { + throw new DocumentApiAdapterError('TARGET_NOT_FOUND', `Image with sdImageId "${sdImageId}" was not found.`, { + sdImageId, + }); + } + + if (matches.length > 1) { + throw new DocumentApiAdapterError( + 'AMBIGUOUS_TARGET', + `Multiple images share sdImageId "${sdImageId}" (${matches.length} found).`, + { sdImageId, count: matches.length }, + ); + } + + return matches[0]; +} + +/** + * Requires that the targeted image has floating placement. + * + * @throws {DocumentApiAdapterError} `INVALID_TARGET` when the image is inline. + */ +export function requireFloatingPlacement(image: ImageCandidate, operation: string): void { + if (image.placement === 'floating') return; + throw new DocumentApiAdapterError( + 'INVALID_TARGET', + `${operation} requires a floating image, but image "${image.sdImageId}" has inline placement.`, + { sdImageId: image.sdImageId, placement: image.placement }, + ); +} diff --git a/packages/super-editor/src/document-api-adapters/helpers/node-info-mapper.ts b/packages/super-editor/src/document-api-adapters/helpers/node-info-mapper.ts index 7b6d8e7fc7..7a9d3b0853 100644 --- a/packages/super-editor/src/document-api-adapters/helpers/node-info-mapper.ts +++ b/packages/super-editor/src/document-api-adapters/helpers/node-info-mapper.ts @@ -233,7 +233,10 @@ function mapTableOfContentsNode(candidate: BlockCandidate): TableOfContentsNodeI } function buildImageInfo(attrs: ImageAttrs | undefined, kind: 'block' | 'inline'): ImageNodeInfo { - const properties = { + const isFloating = Boolean(attrs?.isAnchor); + const wrapObj = attrs?.wrap; + + const properties: ImageNodeInfo['properties'] = { src: attrs?.src ?? undefined, alt: attrs?.alt ?? undefined, size: attrs?.size @@ -243,7 +246,14 @@ function buildImageInfo(attrs: ImageAttrs | undefined, kind: 'block' | 'inline') unit: undefined, } : undefined, - wrap: attrs?.wrap?.type ?? undefined, + placement: isFloating ? 'floating' : 'inline', + wrap: { + type: (wrapObj?.type as ImageNodeInfo['properties']['wrap']['type']) ?? 'Inline', + attrs: wrapObj?.attrs ?? undefined, + }, + anchorData: attrs?.anchorData ?? null, + marginOffset: attrs?.marginOffset ?? null, + relativeHeight: attrs?.relativeHeight ?? null, }; return { diff --git a/packages/super-editor/src/document-api-adapters/plan-engine/images-wrappers.ts b/packages/super-editor/src/document-api-adapters/plan-engine/images-wrappers.ts new file mode 100644 index 0000000000..af779e619c --- /dev/null +++ b/packages/super-editor/src/document-api-adapters/plan-engine/images-wrappers.ts @@ -0,0 +1,757 @@ +/** + * Plan-engine wrappers for all images.* operations. + * + * All image attribute mutations use `tr.setNodeMarkup` at the resolved image + * position — no dedicated editor commands exist for size, position, anchor options, or z-order. + */ + +import { v4 as uuidv4 } from 'uuid'; +import type { + MutationOptions, + CreateImageInput, + CreateImageResult, + ImagesListInput, + ImagesListResult, + ImagesGetInput, + ImageSummary, + ImagesDeleteInput, + ImagesMutationResult, + MoveImageInput, + ConvertToInlineInput, + ConvertToFloatingInput, + SetSizeInput, + SetWrapTypeInput, + SetWrapSideInput, + SetWrapDistancesInput, + SetPositionInput, + SetAnchorOptionsInput, + SetZOrderInput, + ImageAddress, + ImageWrapType, + ImageCreateLocation, +} from '@superdoc/document-api'; +import type { Editor } from '../../core/Editor.js'; +import { + collectImages, + findImageById, + requireFloatingPlacement, + type ImageCandidate, +} from '../helpers/image-resolver.js'; +import { DocumentApiAdapterError } from '../errors.js'; +import { rejectTrackedMode } from '../helpers/mutation-helpers.js'; +import { executeDomainCommand } from './plan-wrappers.js'; +import { resolveBlockInsertionPos } from './create-insertion.js'; +import { readImageDimensionsFromDataUri } from '../../core/super-converter/image-dimensions.js'; +import { generateUniqueDocPrId } from '../../extensions/image/imageHelpers/startImageUpload.js'; + +// --------------------------------------------------------------------------- +// Shared helpers +// --------------------------------------------------------------------------- + +const ALLOWED_WRAP_ATTRS: Record = { + None: ['behindDoc'], + Square: ['wrapText', 'distTop', 'distBottom', 'distLeft', 'distRight'], + Through: ['wrapText', 'distTop', 'distBottom', 'distLeft', 'distRight', 'polygon'], + Tight: ['wrapText', 'distTop', 'distBottom', 'distLeft', 'distRight', 'polygon'], + TopAndBottom: ['distTop', 'distBottom'], + Inline: [], +}; + +const WRAP_TYPES_SUPPORTING_SIDE = new Set(['Square', 'Tight', 'Through']); +const WRAP_TYPES_SUPPORTING_DISTANCES = new Set(['Square', 'Tight', 'Through', 'TopAndBottom']); + +function buildImageAddress(candidate: ImageCandidate): ImageAddress { + return { + kind: 'inline', + nodeType: 'image', + nodeId: candidate.sdImageId, + placement: candidate.placement, + }; +} + +function buildSuccessResult(candidate: ImageCandidate): ImagesMutationResult { + return { success: true, image: buildImageAddress(candidate) }; +} + +function buildNoOpResult(message: string): ImagesMutationResult { + return { success: false, failure: { code: 'NO_OP', message } }; +} + +function buildImageSummary(candidate: ImageCandidate): ImageSummary { + const attrs = candidate.node.attrs; + return { + sdImageId: candidate.sdImageId, + address: buildImageAddress(candidate), + properties: { + src: attrs.src ?? undefined, + alt: attrs.alt ?? undefined, + size: attrs.size ?? undefined, + placement: candidate.placement, + wrap: { + type: (attrs.wrap?.type as ImageWrapType) ?? 'Inline', + attrs: attrs.wrap?.attrs ?? undefined, + }, + anchorData: attrs.anchorData ?? null, + marginOffset: attrs.marginOffset ?? null, + relativeHeight: attrs.relativeHeight ?? null, + }, + }; +} + +/** + * Resolve an ImageCreateLocation to a numeric ProseMirror position. + * + * Reuses the same block-index infrastructure as create.paragraph / create.heading + * so that `before` / `after` / `inParagraph` semantics are consistent. + */ +function resolveImageInsertPosition(editor: Editor, location: ImageCreateLocation): number { + switch (location.kind) { + case 'documentStart': + return 0; + case 'documentEnd': + return editor.state.doc.content.size; + case 'before': + case 'after': + return resolveBlockInsertionPos(editor, location.target.nodeId, location.kind); + case 'inParagraph': { + const pos = resolveBlockInsertionPos(editor, location.target.nodeId, 'before'); + // pos points to the start of the paragraph node; +1 enters the inline content. + // Add any caller-supplied character offset within the paragraph text. + return pos + 1 + (location.offset ?? 0); + } + default: { + const _exhaustive: never = location; + throw new DocumentApiAdapterError( + 'INVALID_TARGET', + `Unknown image location kind: "${(location as { kind: string }).kind}".`, + ); + } + } +} + +/** Strip wrap.attrs to only the keys allowed for the given wrap type. */ +function filterWrapAttrs(type: string, attrs: Record): Record { + const allowed = ALLOWED_WRAP_ATTRS[type] ?? []; + const result: Record = {}; + for (const key of allowed) { + if (key in attrs) result[key] = attrs[key]; + } + return result; +} + +// --------------------------------------------------------------------------- +// Read operations +// --------------------------------------------------------------------------- + +export function imagesListWrapper(editor: Editor, input: ImagesListInput): ImagesListResult { + const allImages = collectImages(editor.state.doc); + const offset = input.offset ?? 0; + const limit = input.limit ?? allImages.length; + const items = allImages.slice(offset, offset + limit).map(buildImageSummary); + return { total: allImages.length, items }; +} + +export function imagesGetWrapper(editor: Editor, input: ImagesGetInput): ImageSummary { + const image = findImageById(editor, input.imageId); + return buildImageSummary(image); +} + +// --------------------------------------------------------------------------- +// Create image +// --------------------------------------------------------------------------- + +export function createImageWrapper( + editor: Editor, + input: CreateImageInput, + options?: MutationOptions, +): CreateImageResult { + rejectTrackedMode('create.image', options); + + if (typeof editor.commands.setImage !== 'function') { + throw new DocumentApiAdapterError( + 'CAPABILITY_UNAVAILABLE', + 'create.image requires the image extension (setImage command).', + ); + } + + // -- Resolve image dimensions ------------------------------------------------- + let resolvedSize = input.size; + + if (isFinitePositive(resolvedSize?.width) && isFinitePositive(resolvedSize?.height)) { + // Caller provided valid dimensions — use as-is. + } else if (input.src?.startsWith('data:')) { + const dims = readImageDimensionsFromDataUri(input.src); + if (dims) { + resolvedSize = dims; + } else { + return { + success: false, + failure: { + code: 'INVALID_INPUT', + message: + 'Image dimensions could not be determined. Provide explicit size.width and size.height, or use a data URI with a supported format (PNG, JPEG, GIF, BMP, WEBP).', + }, + }; + } + } else { + return { + success: false, + failure: { + code: 'INVALID_INPUT', + message: + 'Image dimensions are required. Provide size.width and size.height (finite positive numbers), or use a data URI src so dimensions can be inferred.', + }, + }; + } + + // -- Assign unique drawing ID ------------------------------------------------- + const drawingId = generateUniqueDocPrId(editor); + + const sdImageId = uuidv4(); + const insertPos = input.at ? resolveImageInsertPosition(editor, input.at) : null; + + if (options?.dryRun) { + return { + success: true, + image: { kind: 'inline', nodeType: 'image', nodeId: sdImageId, placement: 'inline' }, + }; + } + + const receipt = executeDomainCommand(editor, () => { + const attrs = { + src: input.src, + alt: input.alt, + title: input.title, + size: resolvedSize, + sdImageId, + id: drawingId, + }; + + if (insertPos !== null) { + // Targeted insertion — insert at the resolved position. + return Boolean(editor.commands.insertContentAt(insertPos, { type: 'image', attrs })); + } + + // No location specified — insert at current selection via setImage. + return Boolean(editor.commands.setImage(attrs)); + }); + + const commandSucceeded = receipt.steps[0]?.effect === 'changed'; + if (!commandSucceeded) { + return { success: false, failure: { code: 'INVALID_TARGET', message: 'Image could not be created.' } }; + } + + return { + success: true, + image: { kind: 'inline', nodeType: 'image', nodeId: sdImageId, placement: 'inline' }, + }; +} + +function isFinitePositive(value: unknown): value is number { + return typeof value === 'number' && Number.isFinite(value) && value > 0; +} + +// --------------------------------------------------------------------------- +// Delete image +// --------------------------------------------------------------------------- + +export function imagesDeleteWrapper( + editor: Editor, + input: ImagesDeleteInput, + options?: MutationOptions, +): ImagesMutationResult { + rejectTrackedMode('images.delete', options); + + const image = findImageById(editor, input.imageId); + + if (options?.dryRun) { + return buildSuccessResult(image); + } + + const receipt = executeDomainCommand(editor, () => { + const { pos, node } = image; + const tr = editor.state.tr; + tr.delete(pos, pos + node.nodeSize); + if (!tr.docChanged) return false; + editor.dispatch(tr); + return true; + }); + + const changed = receipt.steps[0]?.effect === 'changed'; + if (!changed) { + return buildNoOpResult('Image deletion produced no change.'); + } + + return buildSuccessResult(image); +} + +// --------------------------------------------------------------------------- +// Move image +// --------------------------------------------------------------------------- + +export function imagesMoveWrapper( + editor: Editor, + input: MoveImageInput, + options?: MutationOptions, +): ImagesMutationResult { + rejectTrackedMode('images.move', options); + + const image = findImageById(editor, input.imageId); + + // Resolve target position BEFORE the mutation (and before dry-run bail-out) + // so that invalid destinations are caught even in dry-run mode. + const targetPos = resolveImageInsertPosition(editor, input.to); + + if (options?.dryRun) { + return buildSuccessResult(image); + } + + const receipt = executeDomainCommand(editor, () => { + const { pos, node } = image; + const attrs = { ...node.attrs }; + const tr = editor.state.tr; + + // Delete the source image first. + tr.delete(pos, pos + node.nodeSize); + + // Map the pre-resolved target through the delete mapping so it remains + // accurate after the deletion step shifts positions. + const mappedPos = tr.mapping.map(targetPos); + + const imageNode = editor.state.schema.nodes.image.create(attrs); + tr.insert(mappedPos, imageNode); + + if (!tr.docChanged) return false; + editor.dispatch(tr); + return true; + }); + + const changed = receipt.steps[0]?.effect === 'changed'; + if (!changed) { + return { success: false, failure: { code: 'INVALID_TARGET', message: 'Image move produced no change.' } }; + } + + // Re-resolve after move + const updated = findImageById(editor, input.imageId); + return buildSuccessResult(updated); +} + +// --------------------------------------------------------------------------- +// Convert placement +// --------------------------------------------------------------------------- + +export function imagesConvertToInlineWrapper( + editor: Editor, + input: ConvertToInlineInput, + options?: MutationOptions, +): ImagesMutationResult { + rejectTrackedMode('images.convertToInline', options); + + const image = findImageById(editor, input.imageId); + + if (image.placement === 'inline') { + return buildNoOpResult('Image is already inline.'); + } + + if (options?.dryRun) { + return buildSuccessResult(image); + } + + const receipt = executeDomainCommand(editor, () => { + const { pos, node } = image; + const tr = editor.state.tr; + tr.setNodeMarkup(pos, undefined, { + ...node.attrs, + isAnchor: false, + wrap: { type: 'Inline' }, + anchorData: null, + marginOffset: null, + relativeHeight: null, + }); + editor.dispatch(tr); + return true; + }); + + const changed = receipt.steps[0]?.effect === 'changed'; + if (!changed) return buildNoOpResult('Convert to inline produced no change.'); + + const updated = findImageById(editor, input.imageId); + return buildSuccessResult(updated); +} + +export function imagesConvertToFloatingWrapper( + editor: Editor, + input: ConvertToFloatingInput, + options?: MutationOptions, +): ImagesMutationResult { + rejectTrackedMode('images.convertToFloating', options); + + const image = findImageById(editor, input.imageId); + + if (image.placement === 'floating') { + return buildNoOpResult('Image is already floating.'); + } + + if (options?.dryRun) { + return buildSuccessResult(image); + } + + const receipt = executeDomainCommand(editor, () => { + const { pos, node } = image; + const tr = editor.state.tr; + tr.setNodeMarkup(pos, undefined, { + ...node.attrs, + isAnchor: true, + wrap: { type: 'Square', attrs: {} }, + anchorData: { + hRelativeFrom: 'column', + vRelativeFrom: 'paragraph', + }, + }); + editor.dispatch(tr); + return true; + }); + + const changed = receipt.steps[0]?.effect === 'changed'; + if (!changed) return buildNoOpResult('Convert to floating produced no change.'); + + const updated = findImageById(editor, input.imageId); + return buildSuccessResult(updated); +} + +// --------------------------------------------------------------------------- +// Size +// --------------------------------------------------------------------------- + +export function imagesSetSizeWrapper( + editor: Editor, + input: SetSizeInput, + options?: MutationOptions, +): ImagesMutationResult { + rejectTrackedMode('images.setSize', options); + + if (!isFinitePositive(input.size?.width) || !isFinitePositive(input.size?.height)) { + throw new DocumentApiAdapterError( + 'INVALID_INPUT', + 'images.setSize requires size.width and size.height as finite positive numbers.', + ); + } + + const image = findImageById(editor, input.imageId); + const currentSize = image.node.attrs.size ?? {}; + const nextSize = { + width: input.size.width, + height: input.size.height, + ...(input.size.unit !== undefined ? { unit: input.size.unit } : {}), + }; + + if ( + currentSize.width === nextSize.width && + currentSize.height === nextSize.height && + currentSize.unit === nextSize.unit + ) { + return buildNoOpResult(`Image size is already ${nextSize.width}x${nextSize.height}.`); + } + + if (options?.dryRun) return buildSuccessResult(image); + + const receipt = executeDomainCommand(editor, () => { + const { pos, node } = image; + const tr = editor.state.tr; + tr.setNodeMarkup(pos, undefined, { + ...node.attrs, + size: nextSize, + }); + if (!tr.docChanged) return false; + editor.dispatch(tr); + return true; + }); + + const changed = receipt.steps[0]?.effect === 'changed'; + if (!changed) return buildNoOpResult('Set image size produced no change.'); + + const updated = findImageById(editor, input.imageId); + return buildSuccessResult(updated); +} + +// --------------------------------------------------------------------------- +// Wrap type +// --------------------------------------------------------------------------- + +export function imagesSetWrapTypeWrapper( + editor: Editor, + input: SetWrapTypeInput, + options?: MutationOptions, +): ImagesMutationResult { + rejectTrackedMode('images.setWrapType', options); + + const image = findImageById(editor, input.imageId); + requireFloatingPlacement(image, 'images.setWrapType'); + + const currentType = image.node.attrs.wrap?.type; + if (currentType === input.type) { + return buildNoOpResult(`Wrap type is already "${input.type}".`); + } + + if (options?.dryRun) return buildSuccessResult(image); + + const receipt = executeDomainCommand(editor, () => { + const { pos, node } = image; + const tr = editor.state.tr; + const existingAttrs = node.attrs.wrap?.attrs ?? {}; + const filteredAttrs = filterWrapAttrs(input.type, existingAttrs); + const becomingInline = input.type === 'Inline'; + + tr.setNodeMarkup(pos, undefined, { + ...node.attrs, + wrap: { type: input.type, attrs: filteredAttrs }, + isAnchor: !becomingInline, + // When transitioning to Inline, clear floating-only fields to stay + // consistent with convertToInline and prevent stale anchor data. + ...(becomingInline ? { anchorData: null, marginOffset: null, relativeHeight: null } : {}), + }); + editor.dispatch(tr); + return true; + }); + + const changed = receipt.steps[0]?.effect === 'changed'; + if (!changed) return buildNoOpResult('Set wrap type produced no change.'); + + const updated = findImageById(editor, input.imageId); + return buildSuccessResult(updated); +} + +// --------------------------------------------------------------------------- +// Wrap side +// --------------------------------------------------------------------------- + +export function imagesSetWrapSideWrapper( + editor: Editor, + input: SetWrapSideInput, + options?: MutationOptions, +): ImagesMutationResult { + rejectTrackedMode('images.setWrapSide', options); + + const image = findImageById(editor, input.imageId); + requireFloatingPlacement(image, 'images.setWrapSide'); + + const currentWrapType = image.node.attrs.wrap?.type; + if (!WRAP_TYPES_SUPPORTING_SIDE.has(currentWrapType)) { + throw new DocumentApiAdapterError( + 'INVALID_TARGET', + `images.setWrapSide is not valid for wrap type "${currentWrapType}".`, + { wrapType: currentWrapType }, + ); + } + + const currentSide = image.node.attrs.wrap?.attrs?.wrapText; + if (currentSide === input.side) { + return buildNoOpResult(`Wrap side is already "${input.side}".`); + } + + if (options?.dryRun) return buildSuccessResult(image); + + const receipt = executeDomainCommand(editor, () => { + const { pos, node } = image; + const tr = editor.state.tr; + tr.setNodeMarkup(pos, undefined, { + ...node.attrs, + wrap: { + ...node.attrs.wrap, + attrs: { ...(node.attrs.wrap?.attrs ?? {}), wrapText: input.side }, + }, + }); + editor.dispatch(tr); + return true; + }); + + const changed = receipt.steps[0]?.effect === 'changed'; + if (!changed) return buildNoOpResult('Set wrap side produced no change.'); + + const updated = findImageById(editor, input.imageId); + return buildSuccessResult(updated); +} + +// --------------------------------------------------------------------------- +// Wrap distances +// --------------------------------------------------------------------------- + +export function imagesSetWrapDistancesWrapper( + editor: Editor, + input: SetWrapDistancesInput, + options?: MutationOptions, +): ImagesMutationResult { + rejectTrackedMode('images.setWrapDistances', options); + + const image = findImageById(editor, input.imageId); + requireFloatingPlacement(image, 'images.setWrapDistances'); + + const currentWrapType = image.node.attrs.wrap?.type; + if (!WRAP_TYPES_SUPPORTING_DISTANCES.has(currentWrapType)) { + throw new DocumentApiAdapterError( + 'INVALID_TARGET', + `images.setWrapDistances is not valid for wrap type "${currentWrapType}".`, + { wrapType: currentWrapType }, + ); + } + + if (options?.dryRun) return buildSuccessResult(image); + + const receipt = executeDomainCommand(editor, () => { + const { pos, node } = image; + const tr = editor.state.tr; + const currentAttrs = node.attrs.wrap?.attrs ?? {}; + tr.setNodeMarkup(pos, undefined, { + ...node.attrs, + wrap: { + ...node.attrs.wrap, + attrs: { ...currentAttrs, ...input.distances }, + }, + }); + if (!tr.docChanged) return false; + editor.dispatch(tr); + return true; + }); + + const changed = receipt.steps[0]?.effect === 'changed'; + if (!changed) return buildNoOpResult('Set wrap distances produced no change.'); + + const updated = findImageById(editor, input.imageId); + return buildSuccessResult(updated); +} + +// --------------------------------------------------------------------------- +// Position +// --------------------------------------------------------------------------- + +export function imagesSetPositionWrapper( + editor: Editor, + input: SetPositionInput, + options?: MutationOptions, +): ImagesMutationResult { + rejectTrackedMode('images.setPosition', options); + + const image = findImageById(editor, input.imageId); + requireFloatingPlacement(image, 'images.setPosition'); + + if (options?.dryRun) return buildSuccessResult(image); + + const receipt = executeDomainCommand(editor, () => { + const { pos, node } = image; + const tr = editor.state.tr; + const { position } = input; + + const newAnchorData = { + ...(node.attrs.anchorData ?? {}), + ...(position.hRelativeFrom !== undefined ? { hRelativeFrom: position.hRelativeFrom } : {}), + ...(position.vRelativeFrom !== undefined ? { vRelativeFrom: position.vRelativeFrom } : {}), + ...(position.alignH !== undefined ? { alignH: position.alignH } : {}), + ...(position.alignV !== undefined ? { alignV: position.alignV } : {}), + }; + + const newMarginOffset = position.marginOffset + ? { ...(node.attrs.marginOffset ?? {}), ...position.marginOffset } + : node.attrs.marginOffset; + + tr.setNodeMarkup(pos, undefined, { + ...node.attrs, + anchorData: newAnchorData, + marginOffset: newMarginOffset, + }); + if (!tr.docChanged) return false; + editor.dispatch(tr); + return true; + }); + + const changed = receipt.steps[0]?.effect === 'changed'; + if (!changed) return buildNoOpResult('Set position produced no change.'); + + const updated = findImageById(editor, input.imageId); + return buildSuccessResult(updated); +} + +// --------------------------------------------------------------------------- +// Anchor options +// --------------------------------------------------------------------------- + +export function imagesSetAnchorOptionsWrapper( + editor: Editor, + input: SetAnchorOptionsInput, + options?: MutationOptions, +): ImagesMutationResult { + rejectTrackedMode('images.setAnchorOptions', options); + + const image = findImageById(editor, input.imageId); + requireFloatingPlacement(image, 'images.setAnchorOptions'); + + if (options?.dryRun) return buildSuccessResult(image); + + const receipt = executeDomainCommand(editor, () => { + const { pos, node } = image; + const tr = editor.state.tr; + const { options: anchorOpts } = input; + + const currentOrigAttrs = node.attrs.originalAttributes ?? {}; + const updatedOrigAttrs = { + ...currentOrigAttrs, + ...(anchorOpts.behindDoc !== undefined ? { behindDoc: anchorOpts.behindDoc ? '1' : '0' } : {}), + ...(anchorOpts.allowOverlap !== undefined ? { allowOverlap: anchorOpts.allowOverlap ? '1' : '0' } : {}), + ...(anchorOpts.layoutInCell !== undefined ? { layoutInCell: anchorOpts.layoutInCell ? '1' : '0' } : {}), + ...(anchorOpts.lockAnchor !== undefined ? { locked: anchorOpts.lockAnchor ? '1' : '0' } : {}), + }; + + tr.setNodeMarkup(pos, undefined, { + ...node.attrs, + originalAttributes: updatedOrigAttrs, + ...(anchorOpts.simplePos !== undefined ? { simplePos: anchorOpts.simplePos } : {}), + }); + if (!tr.docChanged) return false; + editor.dispatch(tr); + return true; + }); + + const changed = receipt.steps[0]?.effect === 'changed'; + if (!changed) return buildNoOpResult('Set anchor options produced no change.'); + + const updated = findImageById(editor, input.imageId); + return buildSuccessResult(updated); +} + +// --------------------------------------------------------------------------- +// Z-order +// --------------------------------------------------------------------------- + +export function imagesSetZOrderWrapper( + editor: Editor, + input: SetZOrderInput, + options?: MutationOptions, +): ImagesMutationResult { + rejectTrackedMode('images.setZOrder', options); + + const image = findImageById(editor, input.imageId); + requireFloatingPlacement(image, 'images.setZOrder'); + + const currentHeight = image.node.attrs.relativeHeight; + if (currentHeight === input.zOrder.relativeHeight) { + return buildNoOpResult(`relativeHeight is already ${input.zOrder.relativeHeight}.`); + } + + if (options?.dryRun) return buildSuccessResult(image); + + const receipt = executeDomainCommand(editor, () => { + const { pos, node } = image; + const tr = editor.state.tr; + tr.setNodeMarkup(pos, undefined, { + ...node.attrs, + relativeHeight: input.zOrder.relativeHeight, + }); + editor.dispatch(tr); + return true; + }); + + const changed = receipt.steps[0]?.effect === 'changed'; + if (!changed) return buildNoOpResult('Set z-order produced no change.'); + + const updated = findImageById(editor, input.imageId); + return buildSuccessResult(updated); +} diff --git a/packages/super-editor/src/extensions/content-block/content-block.js b/packages/super-editor/src/extensions/content-block/content-block.js index 7e94fbed14..3a127a2cd4 100644 --- a/packages/super-editor/src/extensions/content-block/content-block.js +++ b/packages/super-editor/src/extensions/content-block/content-block.js @@ -1,6 +1,7 @@ // @ts-nocheck import { Node, Attribute } from '@core/index.js'; +import { OOXML_Z_INDEX_BASE } from '@extensions/shared/constants.js'; /** * Size configuration for content blocks @@ -115,9 +116,7 @@ export const ContentBlock = Node.create({ // Use relativeHeight from OOXML for proper z-ordering of overlapping elements const relativeHeight = attrs.originalAttributes?.relativeHeight; if (relativeHeight != null) { - // Scale down the relativeHeight value to a reasonable CSS z-index range - // OOXML uses large numbers (e.g., 251659318), we normalize to a smaller range - const zIndex = Math.floor(relativeHeight / 1000000); + const zIndex = Math.max(0, relativeHeight - OOXML_Z_INDEX_BASE); style += `z-index: ${zIndex}; `; } else { style += 'z-index: 1; '; diff --git a/packages/super-editor/src/extensions/image/image.js b/packages/super-editor/src/extensions/image/image.js index d14d5a7589..f180065cc8 100644 --- a/packages/super-editor/src/extensions/image/image.js +++ b/packages/super-editor/src/extensions/image/image.js @@ -1,3 +1,4 @@ +import { v4 as uuidv4 } from 'uuid'; import { Attribute, Node } from '@core/index.js'; import { formatInsetClipPathTransform } from '@superdoc/contracts'; import { ImageRegistrationPlugin } from './imageHelpers/imageRegistrationPlugin.js'; @@ -88,6 +89,18 @@ export const Image = Node.create({ addAttributes() { return { + /** Stable, session-scoped image identity. Assigned on import and create. */ + sdImageId: { + default: null, + rendered: false, + }, + + /** Raw OOXML relativeHeight for z-ordering. Only meaningful for floating images. */ + relativeHeight: { + default: null, + rendered: false, + }, + src: { default: null, renderDOM: ({ src }) => { @@ -375,12 +388,10 @@ export const Image = Node.create({ switch (type) { case 'None': style += 'position: absolute;'; - // Use relativeHeight from OOXML for proper z-ordering of overlapping elements - const relativeHeight = node.attrs.originalAttributes?.relativeHeight; + // Use first-class relativeHeight attr, falling back to originalAttributes for legacy docs + const relativeHeight = node.attrs.relativeHeight ?? node.attrs.originalAttributes?.relativeHeight; if (relativeHeight != null) { - // Scale down the relativeHeight value to a reasonable CSS z-index range - // OOXML uses large numbers (e.g., 251659318), we normalize to a smaller range - const zIndex = Math.floor(relativeHeight / 1000000); + const zIndex = Math.max(0, relativeHeight - OOXML_Z_INDEX_BASE); style += `z-index: ${zIndex};`; } else if (attrs.behindDoc) { style += 'z-index: -1;'; @@ -676,7 +687,7 @@ export const Image = Node.create({ ({ commands }) => { return commands.insertContent({ type: this.name, - attrs: options, + attrs: { ...options, sdImageId: options.sdImageId ?? uuidv4() }, }); }, diff --git a/packages/super-editor/src/extensions/shape-group/ShapeGroupView.js b/packages/super-editor/src/extensions/shape-group/ShapeGroupView.js index 8a774f4155..0b85757946 100644 --- a/packages/super-editor/src/extensions/shape-group/ShapeGroupView.js +++ b/packages/super-editor/src/extensions/shape-group/ShapeGroupView.js @@ -1,6 +1,7 @@ // @ts-expect-error - preset-geometry package may not have type definitions import { getPresetShapeSvg } from '@superdoc/preset-geometry'; import { createGradient, createTextElement } from '../shared/svg-utils.js'; +import { OOXML_Z_INDEX_BASE } from '@extensions/shared/constants.js'; export class ShapeGroupView { node; @@ -128,7 +129,7 @@ export class ShapeGroupView { // Use relativeHeight from OOXML for proper z-ordering of overlapping elements const relativeHeight = originalAttributes?.relativeHeight; if (relativeHeight != null) { - const zIndex = Math.floor(relativeHeight / 1000000); + const zIndex = Math.max(0, relativeHeight - OOXML_Z_INDEX_BASE); container.style.zIndex = zIndex.toString(); } else { container.style.zIndex = '1'; diff --git a/packages/super-editor/src/extensions/types/node-attributes.ts b/packages/super-editor/src/extensions/types/node-attributes.ts index 1779cef7b1..bd6d69a848 100644 --- a/packages/super-editor/src/extensions/types/node-attributes.ts +++ b/packages/super-editor/src/extensions/types/node-attributes.ts @@ -451,6 +451,10 @@ export interface ImageTransformData { /** Image node attributes */ export interface ImageAttrs extends ShapeNodeAttributes { + /** Stable, session-scoped image identity (UUID assigned on import / create). */ + sdImageId?: string | null; + /** Raw OOXML relativeHeight for z-ordering. Only meaningful for floating images. */ + relativeHeight?: number | null; /** Image source URL or base64 data */ src: string | null; /** Alternative text for accessibility */ diff --git a/packages/super-editor/src/extensions/vector-shape/VectorShapeView.js b/packages/super-editor/src/extensions/vector-shape/VectorShapeView.js index 2cec0d41f9..417c98a514 100644 --- a/packages/super-editor/src/extensions/vector-shape/VectorShapeView.js +++ b/packages/super-editor/src/extensions/vector-shape/VectorShapeView.js @@ -1,6 +1,7 @@ // @ts-expect-error - preset-geometry package may not have type definitions import { getPresetShapeSvg } from '@superdoc/preset-geometry'; import { inchesToPixels } from '@converter/helpers.js'; +import { OOXML_Z_INDEX_BASE } from '@extensions/shared/constants.js'; import { createGradient, createTextElement, @@ -9,13 +10,6 @@ import { generateTransforms, } from '../shared/svg-utils.js'; -/** - * Scaling factor to convert OOXML relativeHeight values to CSS z-index range. - * OOXML uses large numbers (e.g., 251659318), so we scale down by dividing by this factor. - * This ensures proper z-ordering of overlapping elements while staying within reasonable CSS limits. - */ -const Z_INDEX_SCALE_FACTOR = 1000000; - export class VectorShapeView { node; @@ -204,9 +198,7 @@ export class VectorShapeView { // Use relativeHeight from OOXML for proper z-ordering of overlapping elements const relativeHeight = originalAttributes?.relativeHeight; if (relativeHeight != null) { - // Scale down the relativeHeight value to a reasonable CSS z-index range - // OOXML uses large numbers (e.g., 251659318), we normalize to a smaller range - const zIndex = Math.floor(relativeHeight / Z_INDEX_SCALE_FACTOR); + const zIndex = Math.max(0, relativeHeight - OOXML_Z_INDEX_BASE); style += `z-index: ${zIndex};`; } else if (wrap?.attrs?.behindDoc) { style += 'z-index: -1;'; diff --git a/tests/doc-api-stories/tests/images/all-commands.ts b/tests/doc-api-stories/tests/images/all-commands.ts new file mode 100644 index 0000000000..7343131a99 --- /dev/null +++ b/tests/doc-api-stories/tests/images/all-commands.ts @@ -0,0 +1,548 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { execFileSync } from 'node:child_process'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { corpusDoc, unwrap, useStoryHarness } from '../harness'; + +// --------------------------------------------------------------------------- +// Test image — read from local assets as a data URI +// --------------------------------------------------------------------------- + +const TEST_IMAGE_PATH = path.resolve(import.meta.dirname, 'assets/test-image.webp'); +const SET_SIZE_WIDTH_PX = 321; +const SET_SIZE_HEIGHT_PX = 123; +const PX_TO_EMU = 9_525; + +async function imageDataUri(): Promise { + const buf = await readFile(TEST_IMAGE_PATH); + return `data:image/webp;base64,${buf.toString('base64')}`; +} + +/** + * Corpus document with images already embedded. The converter assigns `sdImageId` + * on import, so `images.list` / `images.get` / etc. can resolve them immediately. + */ +const IMAGE_CORPUS_DOC = corpusDoc('basic/image-wrapping.docx'); + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +type ImageFixture = { + imageId: string; +}; + +const ALL_IMAGE_COMMAND_IDS = [ + 'create.image', + 'images.list', + 'images.get', + 'images.delete', + 'images.move', + 'images.convertToInline', + 'images.convertToFloating', + 'images.setSize', + 'images.setWrapType', + 'images.setWrapSide', + 'images.setWrapDistances', + 'images.setPosition', + 'images.setAnchorOptions', + 'images.setZOrder', +] as const; + +type ImageCommandId = (typeof ALL_IMAGE_COMMAND_IDS)[number]; + +type SetupKind = 'blank' | 'inlineImage' | 'floatingImage'; + +type Scenario = { + operationId: ImageCommandId; + setup: SetupKind; + prepare?: (sessionId: string, fixture: ImageFixture | null) => Promise; + run: (sessionId: string, fixture: ImageFixture | null) => Promise; +}; + +// --------------------------------------------------------------------------- +// Test suite +// --------------------------------------------------------------------------- + +describe('document-api story: all image commands', () => { + const { client, outPath } = useStoryHarness('images/all-commands', { + preserveResults: true, + }); + + const api = client as any; + const readOperationIds = new Set(['images.list', 'images.get']); + + // -- helpers --------------------------------------------------------------- + + function makeSessionId(prefix: string): string { + return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + } + + function docNameFor(operationId: ImageCommandId): string { + return `${operationId.replace(/\./g, '-')}.docx`; + } + + function sourceDocNameFor(operationId: ImageCommandId): string { + return `${operationId.replace(/\./g, '-')}-source.docx`; + } + + function readOutputNameFor(operationId: ImageCommandId): string { + return `${operationId.replace(/\./g, '-')}-read-output.json`; + } + + async function saveSource(sessionId: string, operationId: ImageCommandId) { + await api.doc.save({ + sessionId, + out: outPath(sourceDocNameFor(operationId)), + force: true, + }); + } + + async function saveResult(sessionId: string, operationId: ImageCommandId) { + await api.doc.save({ + sessionId, + out: outPath(docNameFor(operationId)), + force: true, + }); + } + + async function saveReadOutput(operationId: ImageCommandId, result: any) { + const payload = { operationId, output: result }; + await writeFile(outPath(readOutputNameFor(operationId)), `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); + } + + function assertMutationSuccess(operationId: ImageCommandId, result: any) { + if (result?.success === true || result?.receipt?.success === true) return; + const code = result?.failure?.code ?? result?.receipt?.failure?.code ?? 'UNKNOWN'; + throw new Error(`${operationId} did not report success (code: ${code}).`); + } + + function assertReadOutput(operationId: ImageCommandId, result: any) { + if (operationId === 'images.list') { + expect(typeof result?.total).toBe('number'); + expect(Array.isArray(result?.items)).toBe(true); + expect(result.items.length).toBeGreaterThan(0); + expect(typeof result.items[0]?.sdImageId).toBe('string'); + return; + } + + if (operationId === 'images.get') { + expect(typeof result?.sdImageId).toBe('string'); + expect(result?.address).toBeDefined(); + expect(result?.properties).toBeDefined(); + return; + } + + throw new Error(`Unexpected read assertion branch for ${operationId}.`); + } + + function requireFixture(operationId: ImageCommandId, fixture: ImageFixture | null): ImageFixture { + if (!fixture) throw new Error(`${operationId} requires an image fixture.`); + return fixture; + } + + // -- fixture setup --------------------------------------------------------- + + /** + * Resolve an image by placement from a session that already has images. + * The corpus doc has both inline and floating images so we can pick the + * right one for each test scenario. + */ + async function resolveImageByPlacement(sessionId: string, placement: 'inline' | 'floating'): Promise { + const listResult = unwrap(await api.doc.images.list({ sessionId })); + const items: any[] = listResult?.items ?? []; + const match = items.find((it) => it?.address?.placement === placement); + if (match) return match.sdImageId; + + // Fallback: return first image regardless of placement + const imageId = items[0]?.sdImageId; + if (!imageId) { + throw new Error(`resolveImageByPlacement: images.list returned no images (wanted ${placement}).`); + } + return imageId; + } + + /** Open the corpus doc that has images, return the first inline image's id. */ + async function setupInlineImageFixture(sessionId: string): Promise { + await api.doc.open({ sessionId, doc: IMAGE_CORPUS_DOC }); + const imageId = await resolveImageByPlacement(sessionId, 'inline'); + return { imageId }; + } + + /** Open the corpus doc, return the first floating image's id. */ + async function setupFloatingImageFixture(sessionId: string): Promise { + await api.doc.open({ sessionId, doc: IMAGE_CORPUS_DOC }); + const imageId = await resolveImageByPlacement(sessionId, 'floating'); + return { imageId }; + } + + // -- scenarios ------------------------------------------------------------- + + const scenarios: Scenario[] = [ + { + operationId: 'create.image', + setup: 'blank', + run: async (sessionId) => { + const src = await imageDataUri(); + return unwrap( + await api.doc.create.image({ + sessionId, + src, + alt: 'butterfly logo', + at: { kind: 'documentEnd' }, + }), + ); + }, + }, + { + operationId: 'images.list', + setup: 'inlineImage', + run: async (sessionId) => { + return unwrap(await api.doc.images.list({ sessionId })); + }, + }, + { + operationId: 'images.get', + setup: 'inlineImage', + run: async (sessionId, fixture) => { + const f = requireFixture('images.get', fixture); + return unwrap(await api.doc.images.get({ sessionId, imageId: f.imageId })); + }, + }, + { + operationId: 'images.delete', + setup: 'inlineImage', + run: async (sessionId, fixture) => { + const f = requireFixture('images.delete', fixture); + return unwrap(await api.doc.images.delete({ sessionId, imageId: f.imageId })); + }, + }, + { + operationId: 'images.move', + setup: 'inlineImage', + prepare: async (sessionId) => { + const result = unwrap( + await api.doc.create.paragraph({ + sessionId, + at: { kind: 'documentEnd' }, + text: 'Paragraph below the image.', + }), + ); + if (result?.success !== true) { + throw new Error('images.move prepare: failed to create target paragraph.'); + } + }, + run: async (sessionId, fixture) => { + const f = requireFixture('images.move', fixture); + return unwrap( + await api.doc.images.move({ + sessionId, + imageId: f.imageId, + to: { kind: 'documentStart' }, + }), + ); + }, + }, + { + operationId: 'images.convertToFloating', + setup: 'inlineImage', + run: async (sessionId, fixture) => { + const f = requireFixture('images.convertToFloating', fixture); + return unwrap( + await api.doc.images.convertToFloating({ + sessionId, + imageId: f.imageId, + }), + ); + }, + }, + { + operationId: 'images.convertToInline', + setup: 'floatingImage', + run: async (sessionId, fixture) => { + const f = requireFixture('images.convertToInline', fixture); + return unwrap( + await api.doc.images.convertToInline({ + sessionId, + imageId: f.imageId, + }), + ); + }, + }, + { + operationId: 'images.setSize', + setup: 'blank', + run: async (sessionId) => { + const src = await imageDataUri(); + const createdResult = unwrap( + await api.doc.create.image({ + sessionId, + src, + alt: 'resizable image', + at: { kind: 'documentEnd' }, + }), + ); + if (createdResult?.success !== true) { + throw new Error('images.setSize setup: create.image did not succeed.'); + } + + const listResult = unwrap(await api.doc.images.list({ sessionId })); + const imageId = listResult?.items?.[0]?.sdImageId; + if (typeof imageId !== 'string' || imageId.length === 0) { + throw new Error('images.setSize setup: images.list did not return an image id.'); + } + return unwrap( + await api.doc.images.setSize({ + sessionId, + imageId, + size: { width: SET_SIZE_WIDTH_PX, height: SET_SIZE_HEIGHT_PX }, + }), + ); + }, + }, + { + operationId: 'images.setWrapType', + setup: 'floatingImage', + run: async (sessionId, fixture) => { + const f = requireFixture('images.setWrapType', fixture); + return unwrap( + await api.doc.images.setWrapType({ + sessionId, + imageId: f.imageId, + type: 'Tight', + }), + ); + }, + }, + { + operationId: 'images.setWrapSide', + setup: 'floatingImage', + prepare: async (sessionId, fixture) => { + const f = requireFixture('images.setWrapSide', fixture); + // setWrapSide requires a wrap type that supports side (Square/Tight/Through). + // The corpus image may already have Square — ignore no-op errors. + try { + unwrap( + await api.doc.images.setWrapType({ + sessionId, + imageId: f.imageId, + type: 'Square', + }), + ); + } catch { + /* already Square — fine */ + } + }, + run: async (sessionId, fixture) => { + const f = requireFixture('images.setWrapSide', fixture); + return unwrap( + await api.doc.images.setWrapSide({ + sessionId, + imageId: f.imageId, + side: 'left', + }), + ); + }, + }, + { + operationId: 'images.setWrapDistances', + setup: 'floatingImage', + prepare: async (sessionId, fixture) => { + const f = requireFixture('images.setWrapDistances', fixture); + // Ensure wrap type supports distances — ignore no-op errors. + try { + unwrap( + await api.doc.images.setWrapType({ + sessionId, + imageId: f.imageId, + type: 'Square', + }), + ); + } catch { + /* already Square — fine */ + } + }, + run: async (sessionId, fixture) => { + const f = requireFixture('images.setWrapDistances', fixture); + return unwrap( + await api.doc.images.setWrapDistances({ + sessionId, + imageId: f.imageId, + distances: { distTop: 100, distBottom: 100 }, + }), + ); + }, + }, + { + operationId: 'images.setPosition', + setup: 'floatingImage', + run: async (sessionId, fixture) => { + const f = requireFixture('images.setPosition', fixture); + return unwrap( + await api.doc.images.setPosition({ + sessionId, + imageId: f.imageId, + position: { hRelativeFrom: 'column', alignH: 'center' }, + }), + ); + }, + }, + { + operationId: 'images.setAnchorOptions', + setup: 'floatingImage', + run: async (sessionId, fixture) => { + const f = requireFixture('images.setAnchorOptions', fixture); + return unwrap( + await api.doc.images.setAnchorOptions({ + sessionId, + imageId: f.imageId, + options: { behindDoc: true, allowOverlap: false }, + }), + ); + }, + }, + { + operationId: 'images.setZOrder', + setup: 'floatingImage', + run: async (sessionId, fixture) => { + const f = requireFixture('images.setZOrder', fixture); + return unwrap( + await api.doc.images.setZOrder({ + sessionId, + imageId: f.imageId, + zOrder: { relativeHeight: 500 }, + }), + ); + }, + }, + ]; + + // -- coverage check -------------------------------------------------------- + + it('covers every image command currently defined on this branch', () => { + const scenarioIds = scenarios.map((scenario) => scenario.operationId); + expect(new Set(scenarioIds).size).toBe(scenarioIds.length); + expect(new Set(scenarioIds)).toEqual(new Set(ALL_IMAGE_COMMAND_IDS)); + }); + + // -- test runner ----------------------------------------------------------- + + for (const scenario of scenarios) { + it(`${scenario.operationId}: executes and saves source/result docs`, async () => { + const sessionId = makeSessionId(scenario.operationId.replace(/\./g, '-')); + + let fixture: ImageFixture | null = null; + if (scenario.setup === 'inlineImage') { + fixture = await setupInlineImageFixture(sessionId); + } else if (scenario.setup === 'floatingImage') { + fixture = await setupFloatingImageFixture(sessionId); + } else { + // blank — just open an empty doc and seed a paragraph + await api.doc.open({ sessionId }); + await api.doc.insert({ sessionId, value: 'Blank document for image test.' }); + } + + if (scenario.prepare) { + await scenario.prepare(sessionId, fixture); + } + + await saveSource(sessionId, scenario.operationId); + + const result = await scenario.run(sessionId, fixture); + + if (readOperationIds.has(scenario.operationId)) { + assertReadOutput(scenario.operationId, result); + await saveReadOutput(scenario.operationId, result); + } else { + assertMutationSuccess(scenario.operationId, result); + } + + await saveResult(sessionId, scenario.operationId); + }); + } + + // -- OOXML validity invariants ----------------------------------------------- + + it('create.image output has valid wp:extent, a:ext, and non-zero docPr IDs', async () => { + const docxPath = outPath(docNameFor('create.image')); + + // Extract word/document.xml from the saved docx + const xml = execFileSync('unzip', ['-p', docxPath, 'word/document.xml'], { + encoding: 'utf8', + maxBuffer: 10 * 1024 * 1024, + }); + + // --- wp:extent must have positive integer cx/cy --- + const extentMatches = [...xml.matchAll(/]*)\/?>/g)]; + expect(extentMatches.length).toBeGreaterThan(0); + for (const m of extentMatches) { + const attrs = m[1]; + const cx = Number(attrs.match(/cx="(\d+)"/)?.[1]); + const cy = Number(attrs.match(/cy="(\d+)"/)?.[1]); + expect(cx).toBeGreaterThan(0); + expect(cy).toBeGreaterThan(0); + expect(Number.isNaN(cx)).toBe(false); + expect(Number.isNaN(cy)).toBe(false); + } + + // --- a:ext must have positive integer cx/cy --- + const aExtMatches = [...xml.matchAll(/]*)\/?>/g)]; + expect(aExtMatches.length).toBeGreaterThan(0); + for (const m of aExtMatches) { + const attrs = m[1]; + const cx = Number(attrs.match(/cx="(\d+)"/)?.[1]); + const cy = Number(attrs.match(/cy="(\d+)"/)?.[1]); + expect(cx).toBeGreaterThan(0); + expect(cy).toBeGreaterThan(0); + expect(Number.isNaN(cx)).toBe(false); + expect(Number.isNaN(cy)).toBe(false); + } + + // --- wp:docPr must have non-zero id --- + const docPrMatches = [...xml.matchAll(/]*)\/?>/g)]; + expect(docPrMatches.length).toBeGreaterThan(0); + for (const m of docPrMatches) { + const id = Number(m[1].match(/id="(\d+)"/)?.[1]); + expect(id).toBeGreaterThan(0); + } + + // --- pic:cNvPr must have non-zero id --- + const cNvPrMatches = [...xml.matchAll(/]*)\/?>/g)]; + expect(cNvPrMatches.length).toBeGreaterThan(0); + for (const m of cNvPrMatches) { + const id = Number(m[1].match(/id="(\d+)"/)?.[1]); + expect(id).toBeGreaterThan(0); + } + }); + + it('images.setSize output contains the requested extent values in OOXML', async () => { + const docxPath = outPath(docNameFor('images.setSize')); + const xml = execFileSync('unzip', ['-p', docxPath, 'word/document.xml'], { + encoding: 'utf8', + maxBuffer: 10 * 1024 * 1024, + }); + + const expectedCx = SET_SIZE_WIDTH_PX * PX_TO_EMU; + const expectedCy = SET_SIZE_HEIGHT_PX * PX_TO_EMU; + + const wpExtents = [...xml.matchAll(/]*)\/?>/g)]; + expect(wpExtents.length).toBeGreaterThan(0); + const hasExpectedWpExtent = wpExtents.some((m) => { + const attrs = m[1]; + const cx = Number(attrs.match(/cx="(\d+)"/)?.[1]); + const cy = Number(attrs.match(/cy="(\d+)"/)?.[1]); + return cx === expectedCx && cy === expectedCy; + }); + expect(hasExpectedWpExtent).toBe(true); + + const aExtents = [...xml.matchAll(/]*)\/?>/g)]; + expect(aExtents.length).toBeGreaterThan(0); + const hasValidAExtent = aExtents.some((m) => { + const attrs = m[1]; + const cx = Number(attrs.match(/cx="(\d+)"/)?.[1]); + const cy = Number(attrs.match(/cy="(\d+)"/)?.[1]); + return cx > 0 && cy > 0; + }); + expect(hasValidAExtent).toBe(true); + }); +}); diff --git a/tests/doc-api-stories/tests/images/assets/test-image.webp b/tests/doc-api-stories/tests/images/assets/test-image.webp new file mode 100644 index 0000000000000000000000000000000000000000..c1733596093303f8cb22d1d49f09129436a38319 GIT binary patch literal 234646 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 Ay#N3J literal 0 HcmV?d00001 From 9714e6bdbd75b2c179dd3be733dbceb416271558 Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Wed, 4 Mar 2026 13:39:48 -0800 Subject: [PATCH 2/2] chore: fix review comments, ooxml spec --- .../reference/_generated-manifest.json | 2 +- .../reference/images/set-z-order.mdx | 8 +- .../src/contract/contract.test.ts | 23 +++++ packages/document-api/src/contract/schemas.ts | 12 ++- .../document-api/src/images/images.test.ts | 92 +++++++++++++++++++ packages/document-api/src/images/images.ts | 27 ++++-- .../document-api/src/images/images.types.ts | 2 +- packages/document-api/src/images/z-order.ts | 18 ++++ .../anchor/helpers/translate-anchor-node.js | 11 +-- .../helpers/translate-anchor-node.test.js | 53 ++++++++++- .../wp/helpers/encode-image-node-helpers.js | 7 +- .../helpers/encode-image-node-helpers.test.js | 36 ++++++++ .../v3/handlers/wp/helpers/relative-height.js | 45 +++++++++ .../plan-engine/images-wrappers.ts | 15 +++ 14 files changed, 329 insertions(+), 22 deletions(-) create mode 100644 packages/document-api/src/images/images.test.ts create mode 100644 packages/document-api/src/images/z-order.ts create mode 100644 packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/relative-height.js diff --git a/apps/docs/document-api/reference/_generated-manifest.json b/apps/docs/document-api/reference/_generated-manifest.json index bbf5d03ea1..419419296d 100644 --- a/apps/docs/document-api/reference/_generated-manifest.json +++ b/apps/docs/document-api/reference/_generated-manifest.json @@ -533,5 +533,5 @@ } ], "marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}", - "sourceHash": "b0802fa2163aafb18783cc4e673a0857aaff5a3254e95c1f727ca13cb96119b8" + "sourceHash": "4a1730dcf3d8aecc2706b7507381afedffd8fd4bbf5e930d9afe3c6f79403c3a" } diff --git a/apps/docs/document-api/reference/images/set-z-order.mdx b/apps/docs/document-api/reference/images/set-z-order.mdx index 5c073522e2..48f70431d8 100644 --- a/apps/docs/document-api/reference/images/set-z-order.mdx +++ b/apps/docs/document-api/reference/images/set-z-order.mdx @@ -30,7 +30,7 @@ Returns an ImagesMutationResult. | --- | --- | --- | --- | | `imageId` | string | yes | | | `zOrder` | object | yes | | -| `zOrder.relativeHeight` | number | yes | | +| `zOrder.relativeHeight` | integer | yes | | ### Example request @@ -38,7 +38,7 @@ Returns an ImagesMutationResult. { "imageId": "example", "zOrder": { - "relativeHeight": 12.5 + "relativeHeight": 1 } } ``` @@ -85,7 +85,9 @@ Returns an ImagesMutationResult. "additionalProperties": false, "properties": { "relativeHeight": { - "type": "number" + "maximum": 4294967295, + "minimum": 0, + "type": "integer" } }, "required": [ diff --git a/packages/document-api/src/contract/contract.test.ts b/packages/document-api/src/contract/contract.test.ts index 2088f07643..7c211e7492 100644 --- a/packages/document-api/src/contract/contract.test.ts +++ b/packages/document-api/src/contract/contract.test.ts @@ -6,6 +6,7 @@ import { OPERATION_REFERENCE_DOC_PATH_MAP, REFERENCE_OPERATION_GROUPS } from './ import { buildInternalContractSchemas } from './schemas.js'; import { PUBLIC_MUTATION_STEP_OP_IDS, STEP_OP_CATALOG } from './step-op-catalog.js'; import { OPERATION_IDS, PRE_APPLY_THROW_CODES, isValidOperationIdFormat } from './types.js'; +import { Z_ORDER_RELATIVE_HEIGHT_MAX, Z_ORDER_RELATIVE_HEIGHT_MIN } from '../images/z-order.js'; describe('document-api contract catalog', () => { it('keeps operation ids explicit and format-valid', () => { @@ -125,6 +126,28 @@ describe('document-api contract catalog', () => { expect(capabilitiesOutput.properties?.global?.required).toContain('history'); }); + it('declares images.setZOrder.relativeHeight as unsigned 32-bit integer', () => { + const schemas = buildInternalContractSchemas(); + const inputSchema = schemas.operations['images.setZOrder'].input as { + properties?: { + zOrder?: { + properties?: { + relativeHeight?: { + type?: string; + minimum?: number; + maximum?: number; + }; + }; + }; + }; + }; + + const relativeHeightSchema = inputSchema.properties?.zOrder?.properties?.relativeHeight; + expect(relativeHeightSchema?.type).toBe('integer'); + expect(relativeHeightSchema?.minimum).toBe(Z_ORDER_RELATIVE_HEIGHT_MIN); + expect(relativeHeightSchema?.maximum).toBe(Z_ORDER_RELATIVE_HEIGHT_MAX); + }); + it('derives OPERATION_IDS from OPERATION_DEFINITIONS keys', () => { const definitionKeys = Object.keys(OPERATION_DEFINITIONS).sort(); const operationIds = [...OPERATION_IDS].sort(); diff --git a/packages/document-api/src/contract/schemas.ts b/packages/document-api/src/contract/schemas.ts index 7f18990f53..7af7d8168b 100644 --- a/packages/document-api/src/contract/schemas.ts +++ b/packages/document-api/src/contract/schemas.ts @@ -12,6 +12,7 @@ import { LINE_RULES, } from '../paragraphs/paragraphs.js'; import { buildPatchSchema, buildStateSchema } from '../styles/index.js'; +import { Z_ORDER_RELATIVE_HEIGHT_MAX, Z_ORDER_RELATIVE_HEIGHT_MIN } from '../images/z-order.js'; type JsonSchema = Record; @@ -3842,7 +3843,16 @@ const operationSchemas: Record = { input: objectSchema( { imageId: { type: 'string' }, - zOrder: objectSchema({ relativeHeight: { type: 'number' } }, ['relativeHeight']), + zOrder: objectSchema( + { + relativeHeight: { + type: 'integer', + minimum: Z_ORDER_RELATIVE_HEIGHT_MIN, + maximum: Z_ORDER_RELATIVE_HEIGHT_MAX, + }, + }, + ['relativeHeight'], + ), }, ['imageId', 'zOrder'], ), diff --git a/packages/document-api/src/images/images.test.ts b/packages/document-api/src/images/images.test.ts new file mode 100644 index 0000000000..6867cf61b5 --- /dev/null +++ b/packages/document-api/src/images/images.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it, vi } from 'vitest'; +import { DocumentApiValidationError } from '../errors.js'; +import { executeImagesSetZOrder, type ImagesAdapter } from './images.js'; +import { Z_ORDER_RELATIVE_HEIGHT_MAX, Z_ORDER_RELATIVE_HEIGHT_MIN } from './z-order.js'; + +function makeSetZOrderAdapter() { + const setZOrder = vi.fn(() => ({ + success: true as const, + image: { + kind: 'inline' as const, + nodeType: 'image' as const, + nodeId: 'img-1', + placement: 'floating' as const, + }, + })); + + const adapter = { setZOrder } as unknown as ImagesAdapter; + return { adapter, setZOrder }; +} + +describe('executeImagesSetZOrder', () => { + it('accepts minimum valid relativeHeight (0)', () => { + const { adapter, setZOrder } = makeSetZOrderAdapter(); + + executeImagesSetZOrder(adapter, { + imageId: 'img-1', + zOrder: { relativeHeight: Z_ORDER_RELATIVE_HEIGHT_MIN }, + }); + + expect(setZOrder).toHaveBeenCalledWith( + { + imageId: 'img-1', + zOrder: { relativeHeight: Z_ORDER_RELATIVE_HEIGHT_MIN }, + }, + undefined, + ); + }); + + it('accepts maximum valid relativeHeight (4294967295)', () => { + const { adapter, setZOrder } = makeSetZOrderAdapter(); + + executeImagesSetZOrder(adapter, { + imageId: 'img-1', + zOrder: { relativeHeight: Z_ORDER_RELATIVE_HEIGHT_MAX }, + }); + + expect(setZOrder).toHaveBeenCalledWith( + { + imageId: 'img-1', + zOrder: { relativeHeight: Z_ORDER_RELATIVE_HEIGHT_MAX }, + }, + undefined, + ); + }); + + it.each([ + { label: 'fractional number', value: 1.5 }, + { label: 'negative integer', value: -1 }, + { label: 'overflow integer', value: Z_ORDER_RELATIVE_HEIGHT_MAX + 1 }, + { label: 'NaN', value: Number.NaN }, + { label: 'Infinity', value: Number.POSITIVE_INFINITY }, + ])('rejects invalid relativeHeight: $label', ({ value }) => { + const { adapter, setZOrder } = makeSetZOrderAdapter(); + + let thrown: unknown; + try { + executeImagesSetZOrder(adapter, { + imageId: 'img-1', + zOrder: { relativeHeight: value }, + }); + } catch (error) { + thrown = error; + } + + expect(thrown).toBeInstanceOf(DocumentApiValidationError); + expect((thrown as Error).message).toContain('unsigned 32-bit integer'); + expect(setZOrder).not.toHaveBeenCalled(); + }); + + it('rejects missing zOrder object', () => { + const { adapter, setZOrder } = makeSetZOrderAdapter(); + + expect(() => + executeImagesSetZOrder(adapter, { + imageId: 'img-1', + zOrder: undefined as unknown as { relativeHeight: number }, + }), + ).toThrow('requires a "zOrder" object'); + + expect(setZOrder).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/document-api/src/images/images.ts b/packages/document-api/src/images/images.ts index 90544cb512..bb8460a01a 100644 --- a/packages/document-api/src/images/images.ts +++ b/packages/document-api/src/images/images.ts @@ -20,6 +20,7 @@ import type { SetAnchorOptionsInput, SetZOrderInput, } from './images.types.js'; +import { isUnsignedInt32, Z_ORDER_RELATIVE_HEIGHT_MAX, Z_ORDER_RELATIVE_HEIGHT_MIN } from './z-order.js'; // --------------------------------------------------------------------------- // Valid value sets @@ -74,6 +75,21 @@ function requireFinitePositiveNumber(value: unknown, field: string): asserts val } } +function requireUnsignedInt32(value: unknown, field: string): asserts value is number { + if (!isUnsignedInt32(value)) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${field} must be an unsigned 32-bit integer (${Z_ORDER_RELATIVE_HEIGHT_MIN}..${Z_ORDER_RELATIVE_HEIGHT_MAX}).`, + { + field, + value, + minimum: Z_ORDER_RELATIVE_HEIGHT_MIN, + maximum: Z_ORDER_RELATIVE_HEIGHT_MAX, + }, + ); + } +} + // --------------------------------------------------------------------------- // Execute functions // --------------------------------------------------------------------------- @@ -229,13 +245,12 @@ export function executeImagesSetZOrder( options?: MutationOptions, ): ImagesMutationResult { requireImageId(input); - if (!input.zOrder || !Number.isFinite(input.zOrder.relativeHeight)) { - throw new DocumentApiValidationError( - 'INVALID_INPUT', - 'images.setZOrder requires zOrder.relativeHeight as a number.', - { field: 'zOrder.relativeHeight' }, - ); + if (!input.zOrder || typeof input.zOrder !== 'object') { + throw new DocumentApiValidationError('INVALID_INPUT', 'images.setZOrder requires a "zOrder" object.', { + field: 'zOrder', + }); } + requireUnsignedInt32(input.zOrder.relativeHeight, 'zOrder.relativeHeight'); return adapter.setZOrder(input, options); } diff --git a/packages/document-api/src/images/images.types.ts b/packages/document-api/src/images/images.types.ts index ac7e4f217c..ff61cf00d7 100644 --- a/packages/document-api/src/images/images.types.ts +++ b/packages/document-api/src/images/images.types.ts @@ -82,7 +82,7 @@ export interface ImageAnchorOptionsInput { // --------------------------------------------------------------------------- export interface ImageZOrderInput { - /** Raw OOXML relativeHeight integer. */ + /** Raw OOXML relativeHeight unsigned 32-bit integer (0..4294967295). */ relativeHeight: number; } diff --git a/packages/document-api/src/images/z-order.ts b/packages/document-api/src/images/z-order.ts new file mode 100644 index 0000000000..72baeba444 --- /dev/null +++ b/packages/document-api/src/images/z-order.ts @@ -0,0 +1,18 @@ +/** + * OOXML unsignedInt bounds for wp:anchor@relativeHeight. + * ECMA-376 defines unsignedInt as 0..4294967295. + */ +export const Z_ORDER_RELATIVE_HEIGHT_MIN = 0; +export const Z_ORDER_RELATIVE_HEIGHT_MAX = 4_294_967_295; + +/** + * Returns true when the value is an unsigned 32-bit integer. + */ +export function isUnsignedInt32(value: unknown): value is number { + return ( + typeof value === 'number' && + Number.isInteger(value) && + value >= Z_ORDER_RELATIVE_HEIGHT_MIN && + value <= Z_ORDER_RELATIVE_HEIGHT_MAX + ); +} diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/wp/anchor/helpers/translate-anchor-node.js b/packages/super-editor/src/core/super-converter/v3/handlers/wp/anchor/helpers/translate-anchor-node.js index 6235fed7aa..0a73accecf 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/wp/anchor/helpers/translate-anchor-node.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/wp/anchor/helpers/translate-anchor-node.js @@ -1,6 +1,7 @@ import { translateImageNode } from '@converter/v3/handlers/wp/helpers/decode-image-node-helpers.js'; import { pixelsToEmu, objToPolygon } from '@converter/helpers.js'; import { mergeDrawingChildren } from '@converter/v3/handlers/wp/helpers/merge-drawing-children.js'; +import { parseRelativeHeight } from '@converter/v3/handlers/wp/helpers/relative-height.js'; /** * Translates anchor image @@ -73,12 +74,10 @@ export function translateAnchorNode(params) { }; // Prefer the live top-level relativeHeight (updated by images.setZOrder) - // over the stale value in originalAttributes. - if (attrs.relativeHeight != null) { - inlineAttrs.relativeHeight = attrs.relativeHeight; - } else if (inlineAttrs.relativeHeight == null) { - inlineAttrs.relativeHeight = 1; - } + // over the stale value in originalAttributes. Always serialize as unsignedInt. + const liveRelativeHeight = parseRelativeHeight(attrs.relativeHeight); + const originalRelativeHeight = parseRelativeHeight(inlineAttrs.relativeHeight); + inlineAttrs.relativeHeight = liveRelativeHeight ?? originalRelativeHeight ?? 1; if (attrs.originalAttributes?.simplePos === undefined && hasSimplePos) { inlineAttrs.simplePos = '1'; diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/wp/anchor/helpers/translate-anchor-node.test.js b/packages/super-editor/src/core/super-converter/v3/handlers/wp/anchor/helpers/translate-anchor-node.test.js index b953c65360..a854861166 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/wp/anchor/helpers/translate-anchor-node.test.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/wp/anchor/helpers/translate-anchor-node.test.js @@ -630,7 +630,58 @@ describe('translateAnchorNode', () => { expect(result.attributes['wp14:anchorId']).toBe('52C3A784'); expect(result.attributes['wp14:editId']).toBe('36FE4467'); - expect(result.attributes.relativeHeight).toBe('251651584'); + expect(result.attributes.relativeHeight).toBe(251651584); + }); + + it('prefers live relativeHeight when it is a valid unsigned integer', () => { + const params = { + node: { + attrs: { + relativeHeight: 500, + originalAttributes: { + relativeHeight: '251651584', + }, + }, + }, + }; + + const result = translateAnchorNode(params); + + expect(result.attributes.relativeHeight).toBe(500); + }); + + it('falls back to original relativeHeight when live value is invalid', () => { + const params = { + node: { + attrs: { + relativeHeight: 1.5, + originalAttributes: { + relativeHeight: '251651584', + }, + }, + }, + }; + + const result = translateAnchorNode(params); + + expect(result.attributes.relativeHeight).toBe(251651584); + }); + + it('falls back to default relativeHeight=1 when both values are invalid', () => { + const params = { + node: { + attrs: { + relativeHeight: -1, + originalAttributes: { + relativeHeight: 'not-an-int', + }, + }, + }, + }; + + const result = translateAnchorNode(params); + + expect(result.attributes.relativeHeight).toBe(1); }); it('should apply polygonEdited value when provided', () => { diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.js b/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.js index ea85a0d247..c0f645fbbf 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.js @@ -18,6 +18,7 @@ import { extractParagraphAlignment, extractBodyPrProperties, } from './textbox-content-helpers.js'; +import { parseRelativeHeight } from './relative-height.js'; const DRAWING_XML_TAG = 'w:drawing'; const SHAPE_URI = 'http://schemas.microsoft.com/office/word/2010/wordprocessingShape'; @@ -437,9 +438,9 @@ export function handleImageNode(node, params, isAnchor) { // which is not what we want for placeholder images that should maintain their original layout. const wrapValue = wrap; - // Extract relativeHeight from anchor attributes for first-class z-order support - const rawRelativeHeight = isAnchor ? Number(attributes['relativeHeight']) : null; - const relativeHeight = rawRelativeHeight != null && Number.isFinite(rawRelativeHeight) ? rawRelativeHeight : null; + // Extract relativeHeight from anchor attributes for first-class z-order support. + // We only accept OOXML-conformant unsignedInt values. + const relativeHeight = isAnchor ? parseRelativeHeight(attributes['relativeHeight']) : null; // Derive a deterministic sdImageId from the drawing's docPr id, the rEmbed, // and the document-part filename so the same image always receives the same diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.test.js b/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.test.js index 599598594a..335adb47a6 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.test.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.test.js @@ -230,6 +230,42 @@ describe('handleImageNode', () => { expect(result.attrs.size).toEqual({ width: 5, height: 6 }); // emuToPixels mocked }); + it('parses valid anchor relativeHeight as unsigned integer', () => { + const node = makeNode({ + attributes: { + relativeHeight: '251651584', + }, + }); + + const result = handleImageNode(node, makeParams(), true); + + expect(result.attrs.relativeHeight).toBe(251651584); + }); + + it('drops fractional anchor relativeHeight values', () => { + const node = makeNode({ + attributes: { + relativeHeight: '1.5', + }, + }); + + const result = handleImageNode(node, makeParams(), true); + + expect(result.attrs.relativeHeight).toBeNull(); + }); + + it('drops out-of-range anchor relativeHeight values', () => { + const node = makeNode({ + attributes: { + relativeHeight: '4294967296', + }, + }); + + const result = handleImageNode(node, makeParams(), true); + + expect(result.attrs.relativeHeight).toBeNull(); + }); + it('calls convertTiffToPng for .tif images', () => { convertTiffToPng.mockReturnValue({ dataUri: 'data:image/png;base64,fake', format: 'png' }); const node = makeNode(); diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/relative-height.js b/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/relative-height.js new file mode 100644 index 0000000000..68c37d851e --- /dev/null +++ b/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/relative-height.js @@ -0,0 +1,45 @@ +/** + * OOXML unsignedInt bounds for wp:anchor@relativeHeight. + * ECMA-376 defines unsignedInt as 0..4294967295. + */ +export const RELATIVE_HEIGHT_MIN = 0; +export const RELATIVE_HEIGHT_MAX = 4_294_967_295; + +/** + * Check if a value is a valid OOXML unsignedInt (32-bit). + * + * @param {unknown} value + * @returns {value is number} + */ +export function isValidRelativeHeight(value) { + return ( + typeof value === 'number' && Number.isInteger(value) && value >= RELATIVE_HEIGHT_MIN && value <= RELATIVE_HEIGHT_MAX + ); +} + +/** + * Parse and normalize an OOXML relativeHeight value. + * + * Accepts: + * - numbers that are already valid unsignedInt values + * - digit-only strings (e.g. "251651584") + * + * Returns null for malformed, fractional, negative, or out-of-range values. + * + * @param {unknown} value + * @returns {number|null} + */ +export function parseRelativeHeight(value) { + if (isValidRelativeHeight(value)) return value; + + if (typeof value !== 'string') return null; + + const trimmed = value.trim(); + if (!/^\d+$/.test(trimmed)) return null; + + const parsed = Number.parseInt(trimmed, 10); + if (!Number.isFinite(parsed)) return null; + if (!Number.isSafeInteger(parsed)) return null; + + return isValidRelativeHeight(parsed) ? parsed : null; +} diff --git a/packages/super-editor/src/document-api-adapters/plan-engine/images-wrappers.ts b/packages/super-editor/src/document-api-adapters/plan-engine/images-wrappers.ts index af779e619c..dc76b9a282 100644 --- a/packages/super-editor/src/document-api-adapters/plan-engine/images-wrappers.ts +++ b/packages/super-editor/src/document-api-adapters/plan-engine/images-wrappers.ts @@ -59,6 +59,8 @@ const ALLOWED_WRAP_ATTRS: Record = { const WRAP_TYPES_SUPPORTING_SIDE = new Set(['Square', 'Tight', 'Through']); const WRAP_TYPES_SUPPORTING_DISTANCES = new Set(['Square', 'Tight', 'Through', 'TopAndBottom']); +const RELATIVE_HEIGHT_MIN = 0; +const RELATIVE_HEIGHT_MAX = 4_294_967_295; function buildImageAddress(candidate: ImageCandidate): ImageAddress { return { @@ -98,6 +100,12 @@ function buildImageSummary(candidate: ImageCandidate): ImageSummary { }; } +function isUnsignedInt32(value: unknown): value is number { + return ( + typeof value === 'number' && Number.isInteger(value) && value >= RELATIVE_HEIGHT_MIN && value <= RELATIVE_HEIGHT_MAX + ); +} + /** * Resolve an ImageCreateLocation to a numeric ProseMirror position. * @@ -728,6 +736,13 @@ export function imagesSetZOrderWrapper( ): ImagesMutationResult { rejectTrackedMode('images.setZOrder', options); + if (!isUnsignedInt32(input.zOrder?.relativeHeight)) { + throw new DocumentApiAdapterError( + 'INVALID_INPUT', + `images.setZOrder requires zOrder.relativeHeight as an unsigned 32-bit integer (${RELATIVE_HEIGHT_MIN}..${RELATIVE_HEIGHT_MAX}).`, + ); + } + const image = findImageById(editor, input.imageId); requireFloatingPlacement(image, 'images.setZOrder');