diff --git a/apps/cli/package.json b/apps/cli/package.json index b32af2e57a..ebe12fbc87 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -37,6 +37,7 @@ }, "devDependencies": { "@superdoc/document-api": "workspace:*", + "@superdoc/pm-adapter": "workspace:*", "@superdoc/super-editor": "workspace:*", "@types/bun": "catalog:", "@types/node": "catalog:", diff --git a/apps/cli/scripts/export-sdk-contract.ts b/apps/cli/scripts/export-sdk-contract.ts index bbd7447b82..e8c15a0430 100644 --- a/apps/cli/scripts/export-sdk-contract.ts +++ b/apps/cli/scripts/export-sdk-contract.ts @@ -67,6 +67,25 @@ const INTENT_NAMES = { 'doc.styles.apply': 'styles_apply', 'doc.create.paragraph': 'create_paragraph', 'doc.create.heading': 'create_heading', + 'doc.create.sectionBreak': 'create_section_break', + 'doc.sections.list': 'list_sections', + 'doc.sections.get': 'get_section', + 'doc.sections.setBreakType': 'set_section_break_type', + 'doc.sections.setPageMargins': 'set_section_page_margins', + 'doc.sections.setHeaderFooterMargins': 'set_section_header_footer_margins', + 'doc.sections.setPageSetup': 'set_section_page_setup', + 'doc.sections.setColumns': 'set_section_columns', + 'doc.sections.setLineNumbering': 'set_section_line_numbering', + 'doc.sections.setPageNumbering': 'set_section_page_numbering', + 'doc.sections.setTitlePage': 'set_section_title_page', + 'doc.sections.setOddEvenHeadersFooters': 'set_section_odd_even_headers_footers', + 'doc.sections.setVerticalAlign': 'set_section_vertical_align', + 'doc.sections.setSectionDirection': 'set_section_direction', + 'doc.sections.setHeaderFooterRef': 'set_section_header_footer_reference', + 'doc.sections.clearHeaderFooterRef': 'clear_section_header_footer_reference', + 'doc.sections.setLinkToPrevious': 'set_section_link_to_previous', + 'doc.sections.setPageBorders': 'set_section_page_borders', + 'doc.sections.clearPageBorders': 'clear_section_page_borders', 'doc.lists.list': 'list_lists', 'doc.lists.get': 'get_list', 'doc.lists.insert': 'insert_list', diff --git a/apps/cli/src/__tests__/conformance/scenarios.ts b/apps/cli/src/__tests__/conformance/scenarios.ts index 5990dc1307..c6993055b7 100644 --- a/apps/cli/src/__tests__/conformance/scenarios.ts +++ b/apps/cli/src/__tests__/conformance/scenarios.ts @@ -27,6 +27,117 @@ function genericInvalidArgumentFailure(operationId: CliOperationId) { }); } +function extractDiscoveryItems(data: unknown): Record[] { + if (!data || typeof data !== 'object') return []; + + for (const value of Object.values(data as Record)) { + if (!value || typeof value !== 'object') continue; + + const asContainer = value as { + items?: unknown; + result?: { + items?: unknown; + }; + }; + const maybeItems = Array.isArray(asContainer.items) + ? asContainer.items + : Array.isArray(asContainer.result?.items) + ? asContainer.result.items + : null; + + if (Array.isArray(maybeItems)) { + return maybeItems.filter((entry): entry is Record => !!entry && typeof entry === 'object'); + } + } + + return []; +} + +function requireSectionAddress(item: Record, context: string): Record { + const address = item.address; + if (!address || typeof address !== 'object') { + throw new Error(`Missing section address for ${context}.`); + } + return address as Record; +} + +async function resolveFirstSection( + harness: ConformanceHarness, + stateDir: string, + docPath: string, + context: string, +): Promise<{ item: Record; address: Record }> { + const listed = await harness.runCli([...commandTokens('doc.sections.list'), docPath, '--limit', '10'], stateDir); + if (listed.result.code !== 0 || listed.envelope.ok !== true) { + throw new Error(`Failed to list sections for ${context}.`); + } + + const items = extractDiscoveryItems(listed.envelope.data); + const first = items[0]; + if (!first) { + throw new Error(`No sections available for ${context}.`); + } + + return { + item: first, + address: requireSectionAddress(first, context), + }; +} + +async function createDocWithSecondSection( + harness: ConformanceHarness, + stateDir: string, + label: string, +): Promise<{ docPath: string; first: Record; second: Record }> { + const sourceDoc = await harness.copyFixtureDoc(`${label}-source`); + const withBreakDoc = harness.createOutputPath(`${label}-with-break`); + const created = await harness.runCli( + [...commandTokens('doc.create.sectionBreak'), sourceDoc, '--break-type', 'nextPage', '--out', withBreakDoc], + stateDir, + ); + if (created.result.code !== 0 || created.envelope.ok !== true) { + throw new Error(`Failed to create second section for ${label}.`); + } + + const listed = await harness.runCli([...commandTokens('doc.sections.list'), withBreakDoc, '--limit', '10'], stateDir); + if (listed.result.code !== 0 || listed.envelope.ok !== true) { + throw new Error(`Failed to list sections after break creation for ${label}.`); + } + + const items = extractDiscoveryItems(listed.envelope.data); + const first = items[0]; + const second = items[1]; + if (!first || !second) { + throw new Error(`Expected at least 2 sections for ${label}.`); + } + + return { docPath: withBreakDoc, first, second }; +} + +function sectionMutationScenario( + operationId: CliOperationId, + label: string, + extraArgs: string[], +): (harness: ConformanceHarness) => Promise { + return async (harness) => { + const stateDir = await harness.createStateDir(`${label}-success`); + const docPath = await harness.copyFixtureDoc(`${label}-source`); + const { address } = await resolveFirstSection(harness, stateDir, docPath, label); + return { + stateDir, + args: [ + ...commandTokens(operationId), + docPath, + '--target-json', + JSON.stringify(address), + ...extraArgs, + '--out', + harness.createOutputPath(`${label}-output`), + ], + }; + }; +} + // --------------------------------------------------------------------------- // Table scenario helpers (DRY builders for the 40 table operations) // --------------------------------------------------------------------------- @@ -393,6 +504,249 @@ export const SUCCESS_SCENARIOS = { ], }; }, + 'doc.create.sectionBreak': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('doc-create-section-break-success'); + const docPath = await harness.copyFixtureDoc('doc-create-section-break'); + return { + stateDir, + args: [ + ...commandTokens('doc.create.sectionBreak'), + docPath, + '--break-type', + 'nextPage', + '--out', + harness.createOutputPath('doc-create-section-break-output'), + ], + }; + }, + 'doc.sections.list': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('doc-sections-list-success'); + const docPath = await harness.copyFixtureDoc('doc-sections-list'); + return { + stateDir, + args: [...commandTokens('doc.sections.list'), docPath, '--limit', '10'], + }; + }, + 'doc.sections.get': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('doc-sections-get-success'); + const docPath = await harness.copyFixtureDoc('doc-sections-get'); + const { address } = await resolveFirstSection(harness, stateDir, docPath, 'doc.sections.get'); + return { + stateDir, + args: [...commandTokens('doc.sections.get'), docPath, '--address-json', JSON.stringify(address)], + }; + }, + 'doc.sections.setBreakType': sectionMutationScenario('doc.sections.setBreakType', 'doc-sections-set-break-type', [ + '--break-type', + 'continuous', + ]), + 'doc.sections.setPageMargins': sectionMutationScenario( + 'doc.sections.setPageMargins', + 'doc-sections-set-page-margins', + ['--top', '1.1', '--right', '1.2', '--bottom', '1.3', '--left', '1.4'], + ), + 'doc.sections.setHeaderFooterMargins': sectionMutationScenario( + 'doc.sections.setHeaderFooterMargins', + 'doc-sections-set-header-footer-margins', + ['--header', '0.6', '--footer', '0.8'], + ), + 'doc.sections.setPageSetup': sectionMutationScenario('doc.sections.setPageSetup', 'doc-sections-set-page-setup', [ + '--orientation', + 'landscape', + ]), + 'doc.sections.setColumns': sectionMutationScenario('doc.sections.setColumns', 'doc-sections-set-columns', [ + '--count', + '2', + '--gap', + '0.8', + '--equal-width', + 'true', + ]), + 'doc.sections.setLineNumbering': sectionMutationScenario( + 'doc.sections.setLineNumbering', + 'doc-sections-set-line-numbering', + ['--enabled', 'true', '--count-by', '2', '--start', '1', '--distance', '0.25', '--restart', 'newSection'], + ), + 'doc.sections.setPageNumbering': sectionMutationScenario( + 'doc.sections.setPageNumbering', + 'doc-sections-set-page-numbering', + ['--start', '5', '--format', 'decimal'], + ), + 'doc.sections.setTitlePage': sectionMutationScenario('doc.sections.setTitlePage', 'doc-sections-set-title-page', [ + '--enabled', + 'true', + ]), + 'doc.sections.setOddEvenHeadersFooters': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('doc-sections-set-odd-even-success'); + const docPath = await harness.copyFixtureDoc('doc-sections-set-odd-even'); + return { + stateDir, + args: [ + ...commandTokens('doc.sections.setOddEvenHeadersFooters'), + docPath, + '--enabled', + 'true', + '--out', + harness.createOutputPath('doc-sections-set-odd-even-output'), + ], + }; + }, + 'doc.sections.setVerticalAlign': sectionMutationScenario( + 'doc.sections.setVerticalAlign', + 'doc-sections-set-vertical-align', + ['--value', 'center'], + ), + 'doc.sections.setSectionDirection': sectionMutationScenario( + 'doc.sections.setSectionDirection', + 'doc-sections-set-direction', + ['--direction', 'rtl'], + ), + 'doc.sections.setHeaderFooterRef': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('doc-sections-set-header-footer-ref-success'); + const docPath = await harness.copyFixtureDoc('doc-sections-set-header-footer-ref'); + const { item, address } = await resolveFirstSection(harness, stateDir, docPath, 'doc.sections.setHeaderFooterRef'); + const footerRefs = item.footerRefs as Record | undefined; + const refId = + (typeof footerRefs?.default === 'string' ? footerRefs.default : undefined) ?? + (typeof footerRefs?.even === 'string' ? footerRefs.even : undefined); + if (!refId) { + throw new Error('No footer relationship id available for doc.sections.setHeaderFooterRef.'); + } + return { + stateDir, + args: [ + ...commandTokens('doc.sections.setHeaderFooterRef'), + docPath, + '--target-json', + JSON.stringify(address), + '--kind', + 'footer', + '--variant', + 'first', + '--ref-id', + refId, + '--out', + harness.createOutputPath('doc-sections-set-header-footer-ref-output'), + ], + }; + }, + 'doc.sections.clearHeaderFooterRef': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('doc-sections-clear-header-footer-ref-success'); + const sourceDoc = await harness.copyFixtureDoc('doc-sections-clear-header-footer-ref'); + const { item, address } = await resolveFirstSection( + harness, + stateDir, + sourceDoc, + 'doc.sections.clearHeaderFooterRef:prepare', + ); + const footerRefs = item.footerRefs as Record | undefined; + const refId = + (typeof footerRefs?.default === 'string' ? footerRefs.default : undefined) ?? + (typeof footerRefs?.even === 'string' ? footerRefs.even : undefined); + if (!refId) { + throw new Error('No footer relationship id available for doc.sections.clearHeaderFooterRef.'); + } + + const preparedDoc = harness.createOutputPath('doc-sections-clear-header-footer-ref-prepared'); + const prepared = await harness.runCli( + [ + ...commandTokens('doc.sections.setHeaderFooterRef'), + sourceDoc, + '--target-json', + JSON.stringify(address), + '--kind', + 'footer', + '--variant', + 'first', + '--ref-id', + refId, + '--out', + preparedDoc, + ], + stateDir, + ); + if (prepared.result.code !== 0 || prepared.envelope.ok !== true) { + throw new Error('Failed to prepare explicit header/footer ref for clear scenario.'); + } + + return { + stateDir, + args: [ + ...commandTokens('doc.sections.clearHeaderFooterRef'), + preparedDoc, + '--target-json', + JSON.stringify(address), + '--kind', + 'footer', + '--variant', + 'first', + '--out', + harness.createOutputPath('doc-sections-clear-header-footer-ref-output'), + ], + }; + }, + 'doc.sections.setLinkToPrevious': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('doc-sections-set-link-to-previous-success'); + const fixture = await createDocWithSecondSection(harness, stateDir, 'doc-sections-set-link-to-previous'); + const secondAddress = requireSectionAddress(fixture.second, 'doc.sections.setLinkToPrevious'); + return { + stateDir, + args: [ + ...commandTokens('doc.sections.setLinkToPrevious'), + fixture.docPath, + '--target-json', + JSON.stringify(secondAddress), + '--kind', + 'header', + '--variant', + 'default', + '--linked', + 'false', + '--out', + harness.createOutputPath('doc-sections-set-link-to-previous-output'), + ], + }; + }, + 'doc.sections.setPageBorders': sectionMutationScenario( + 'doc.sections.setPageBorders', + 'doc-sections-set-page-borders', + ['--borders-json', JSON.stringify({ top: { style: 'single', size: 8, color: '000000' } })], + ), + 'doc.sections.clearPageBorders': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('doc-sections-clear-page-borders-success'); + const sourceDoc = await harness.copyFixtureDoc('doc-sections-clear-page-borders'); + const { address } = await resolveFirstSection(harness, stateDir, sourceDoc, 'doc.sections.clearPageBorders'); + + const withBordersDoc = harness.createOutputPath('doc-sections-clear-page-borders-prepared'); + const prepared = await harness.runCli( + [ + ...commandTokens('doc.sections.setPageBorders'), + sourceDoc, + '--target-json', + JSON.stringify(address), + '--borders-json', + JSON.stringify({ top: { style: 'single', size: 8, color: '000000' } }), + '--out', + withBordersDoc, + ], + stateDir, + ); + if (prepared.result.code !== 0 || prepared.envelope.ok !== true) { + throw new Error('Failed to prepare page borders for clear-page-borders scenario.'); + } + + return { + stateDir, + args: [ + ...commandTokens('doc.sections.clearPageBorders'), + withBordersDoc, + '--target-json', + JSON.stringify(address), + '--out', + harness.createOutputPath('doc-sections-clear-page-borders-output'), + ], + }; + }, 'doc.blocks.delete': async (harness: ConformanceHarness): Promise => { const stateDir = await harness.createStateDir('doc-blocks-delete-success'); const docPath = await harness.copyFixtureDoc('doc-blocks-delete'); diff --git a/apps/docs/document-api/available-operations.mdx b/apps/docs/document-api/available-operations.mdx index 7b51646ea9..8cf00b3f6b 100644 --- a/apps/docs/document-api/available-operations.mdx +++ b/apps/docs/document-api/available-operations.mdx @@ -18,11 +18,12 @@ 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 | 8 | 0 | 8 | [Reference](/document-api/reference/core/index) | -| Create | 3 | 0 | 3 | [Reference](/document-api/reference/create/index) | +| Create | 4 | 0 | 4 | [Reference](/document-api/reference/create/index) | | Format | 5 | 4 | 9 | [Reference](/document-api/reference/format/index) | | Lists | 8 | 0 | 8 | [Reference](/document-api/reference/lists/index) | | Mutations | 2 | 0 | 2 | [Reference](/document-api/reference/mutations/index) | | Query | 1 | 0 | 1 | [Reference](/document-api/reference/query/index) | +| Sections | 18 | 0 | 18 | [Reference](/document-api/reference/sections/index) | | Styles | 1 | 0 | 1 | [Reference](/document-api/reference/styles/index) | | Tables | 39 | 0 | 39 | [Reference](/document-api/reference/tables/index) | | Track Changes | 3 | 0 | 3 | [Reference](/document-api/reference/track-changes/index) | @@ -46,6 +47,7 @@ Use the tables below to see what operations are available and where each one is | editor.doc.delete(...) | [`delete`](/document-api/reference/delete) | | editor.doc.create.paragraph(...) | [`create.paragraph`](/document-api/reference/create/paragraph) | | editor.doc.create.heading(...) | [`create.heading`](/document-api/reference/create/heading) | +| 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.format.apply(...) | [`format.apply`](/document-api/reference/format/apply) | | editor.doc.format.fontSize(...) | [`format.fontSize`](/document-api/reference/format/font-size) | @@ -67,6 +69,24 @@ Use the tables below to see what operations are available and where each one is | editor.doc.mutations.preview(...) | [`mutations.preview`](/document-api/reference/mutations/preview) | | editor.doc.mutations.apply(...) | [`mutations.apply`](/document-api/reference/mutations/apply) | | editor.doc.query.match(...) | [`query.match`](/document-api/reference/query/match) | +| editor.doc.sections.list(...) | [`sections.list`](/document-api/reference/sections/list) | +| editor.doc.sections.get(...) | [`sections.get`](/document-api/reference/sections/get) | +| editor.doc.sections.setBreakType(...) | [`sections.setBreakType`](/document-api/reference/sections/set-break-type) | +| editor.doc.sections.setPageMargins(...) | [`sections.setPageMargins`](/document-api/reference/sections/set-page-margins) | +| editor.doc.sections.setHeaderFooterMargins(...) | [`sections.setHeaderFooterMargins`](/document-api/reference/sections/set-header-footer-margins) | +| editor.doc.sections.setPageSetup(...) | [`sections.setPageSetup`](/document-api/reference/sections/set-page-setup) | +| editor.doc.sections.setColumns(...) | [`sections.setColumns`](/document-api/reference/sections/set-columns) | +| editor.doc.sections.setLineNumbering(...) | [`sections.setLineNumbering`](/document-api/reference/sections/set-line-numbering) | +| editor.doc.sections.setPageNumbering(...) | [`sections.setPageNumbering`](/document-api/reference/sections/set-page-numbering) | +| editor.doc.sections.setTitlePage(...) | [`sections.setTitlePage`](/document-api/reference/sections/set-title-page) | +| editor.doc.sections.setOddEvenHeadersFooters(...) | [`sections.setOddEvenHeadersFooters`](/document-api/reference/sections/set-odd-even-headers-footers) | +| editor.doc.sections.setVerticalAlign(...) | [`sections.setVerticalAlign`](/document-api/reference/sections/set-vertical-align) | +| editor.doc.sections.setSectionDirection(...) | [`sections.setSectionDirection`](/document-api/reference/sections/set-section-direction) | +| editor.doc.sections.setHeaderFooterRef(...) | [`sections.setHeaderFooterRef`](/document-api/reference/sections/set-header-footer-ref) | +| editor.doc.sections.clearHeaderFooterRef(...) | [`sections.clearHeaderFooterRef`](/document-api/reference/sections/clear-header-footer-ref) | +| editor.doc.sections.setLinkToPrevious(...) | [`sections.setLinkToPrevious`](/document-api/reference/sections/set-link-to-previous) | +| editor.doc.sections.setPageBorders(...) | [`sections.setPageBorders`](/document-api/reference/sections/set-page-borders) | +| editor.doc.sections.clearPageBorders(...) | [`sections.clearPageBorders`](/document-api/reference/sections/clear-page-borders) | | editor.doc.styles.apply(...) | [`styles.apply`](/document-api/reference/styles/apply) | | editor.doc.tables.convertFromText(...) | [`tables.convertFromText`](/document-api/reference/tables/convert-from-text) | | editor.doc.tables.delete(...) | [`tables.delete`](/document-api/reference/tables/delete) | diff --git a/apps/docs/document-api/reference/_generated-manifest.json b/apps/docs/document-api/reference/_generated-manifest.json index 1712cbd7c6..c7f7325269 100644 --- a/apps/docs/document-api/reference/_generated-manifest.json +++ b/apps/docs/document-api/reference/_generated-manifest.json @@ -15,6 +15,7 @@ "apps/docs/document-api/reference/create/heading.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", "apps/docs/document-api/reference/create/table.mdx", "apps/docs/document-api/reference/delete.mdx", "apps/docs/document-api/reference/find.mdx", @@ -45,6 +46,25 @@ "apps/docs/document-api/reference/query/index.mdx", "apps/docs/document-api/reference/query/match.mdx", "apps/docs/document-api/reference/replace.mdx", + "apps/docs/document-api/reference/sections/clear-header-footer-ref.mdx", + "apps/docs/document-api/reference/sections/clear-page-borders.mdx", + "apps/docs/document-api/reference/sections/get.mdx", + "apps/docs/document-api/reference/sections/index.mdx", + "apps/docs/document-api/reference/sections/list.mdx", + "apps/docs/document-api/reference/sections/set-break-type.mdx", + "apps/docs/document-api/reference/sections/set-columns.mdx", + "apps/docs/document-api/reference/sections/set-header-footer-margins.mdx", + "apps/docs/document-api/reference/sections/set-header-footer-ref.mdx", + "apps/docs/document-api/reference/sections/set-line-numbering.mdx", + "apps/docs/document-api/reference/sections/set-link-to-previous.mdx", + "apps/docs/document-api/reference/sections/set-odd-even-headers-footers.mdx", + "apps/docs/document-api/reference/sections/set-page-borders.mdx", + "apps/docs/document-api/reference/sections/set-page-margins.mdx", + "apps/docs/document-api/reference/sections/set-page-numbering.mdx", + "apps/docs/document-api/reference/sections/set-page-setup.mdx", + "apps/docs/document-api/reference/sections/set-section-direction.mdx", + "apps/docs/document-api/reference/sections/set-title-page.mdx", + "apps/docs/document-api/reference/sections/set-vertical-align.mdx", "apps/docs/document-api/reference/styles/apply.mdx", "apps/docs/document-api/reference/styles/index.mdx", "apps/docs/document-api/reference/tables/apply-border-preset.mdx", @@ -118,10 +138,36 @@ { "aliasMemberPaths": [], "key": "create", - "operationIds": ["create.paragraph", "create.heading", "create.table"], + "operationIds": ["create.paragraph", "create.heading", "create.sectionBreak", "create.table"], "pagePath": "apps/docs/document-api/reference/create/index.mdx", "title": "Create" }, + { + "aliasMemberPaths": [], + "key": "sections", + "operationIds": [ + "sections.list", + "sections.get", + "sections.setBreakType", + "sections.setPageMargins", + "sections.setHeaderFooterMargins", + "sections.setPageSetup", + "sections.setColumns", + "sections.setLineNumbering", + "sections.setPageNumbering", + "sections.setTitlePage", + "sections.setOddEvenHeadersFooters", + "sections.setVerticalAlign", + "sections.setSectionDirection", + "sections.setHeaderFooterRef", + "sections.clearHeaderFooterRef", + "sections.setLinkToPrevious", + "sections.setPageBorders", + "sections.clearPageBorders" + ], + "pagePath": "apps/docs/document-api/reference/sections/index.mdx", + "title": "Sections" + }, { "aliasMemberPaths": ["format.bold", "format.italic", "format.underline", "format.strikethrough"], "key": "format", @@ -229,5 +275,5 @@ } ], "marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}", - "sourceHash": "5454f771b591f831325cc10903af71f01a064277b44a8c252b71855ff2b98a7d" + "sourceHash": "8b0c27a3eff9d8548a4de7b931f4a577774027635048dc42492f0500d00963b1" } diff --git a/apps/docs/document-api/reference/blocks/delete.mdx b/apps/docs/document-api/reference/blocks/delete.mdx index 60ff945606..79a5103387 100644 --- a/apps/docs/document-api/reference/blocks/delete.mdx +++ b/apps/docs/document-api/reference/blocks/delete.mdx @@ -1,7 +1,7 @@ --- title: blocks.delete sidebarTitle: blocks.delete -description: Reference for blocks.delete +description: Delete an entire block node (paragraph, heading, list item, table, image, or sdt) deterministically. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for blocks.delete ## Summary +Delete an entire block node (paragraph, heading, list item, table, image, or sdt) deterministically. + - Operation ID: `blocks.delete` - API member path: `editor.doc.blocks.delete(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for blocks.delete - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a BlocksDeleteResult receipt confirming the block was removed from the document. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/capabilities/get.mdx b/apps/docs/document-api/reference/capabilities/get.mdx index b5c5b11af3..f705fe7e39 100644 --- a/apps/docs/document-api/reference/capabilities/get.mdx +++ b/apps/docs/document-api/reference/capabilities/get.mdx @@ -1,7 +1,7 @@ --- title: capabilities.get sidebarTitle: capabilities.get -description: Reference for capabilities.get +description: Query runtime capabilities supported by the current document engine. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for capabilities.get ## Summary +Query runtime capabilities supported by the current document engine. + - Operation ID: `capabilities.get` - API member path: `editor.doc.capabilities()` - Mutates document: `no` @@ -18,6 +20,10 @@ description: Reference for capabilities.get - Supports dry run: `no` - Deterministic target resolution: `yes` +## Expected result + +Returns a DocumentApiCapabilities object describing supported features of the current engine. + ## Input fields _No fields._ @@ -156,6 +162,14 @@ _No fields._ ], "tracked": true }, + "create.sectionBreak": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, "create.table": { "available": true, "dryRun": true, @@ -356,6 +370,150 @@ _No fields._ ], "tracked": true }, + "sections.clearHeaderFooterRef": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "sections.clearPageBorders": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "sections.get": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "sections.list": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "sections.setBreakType": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "sections.setColumns": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "sections.setHeaderFooterMargins": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "sections.setHeaderFooterRef": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "sections.setLineNumbering": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "sections.setLinkToPrevious": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "sections.setOddEvenHeadersFooters": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "sections.setPageBorders": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "sections.setPageMargins": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "sections.setPageNumbering": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "sections.setPageSetup": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "sections.setSectionDirection": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "sections.setTitlePage": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "sections.setVerticalAlign": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, "styles.apply": { "available": true, "dryRun": true, @@ -1272,6 +1430,41 @@ _No fields._ ], "type": "object" }, + "create.sectionBreak": { + "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.table": { "additionalProperties": false, "properties": { @@ -2147,6 +2340,636 @@ _No fields._ ], "type": "object" }, + "sections.clearHeaderFooterRef": { + "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" + }, + "sections.clearPageBorders": { + "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" + }, + "sections.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" + }, + "sections.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" + }, + "sections.setBreakType": { + "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" + }, + "sections.setColumns": { + "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" + }, + "sections.setHeaderFooterMargins": { + "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" + }, + "sections.setHeaderFooterRef": { + "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" + }, + "sections.setLineNumbering": { + "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" + }, + "sections.setLinkToPrevious": { + "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" + }, + "sections.setOddEvenHeadersFooters": { + "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" + }, + "sections.setPageBorders": { + "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" + }, + "sections.setPageMargins": { + "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" + }, + "sections.setPageNumbering": { + "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" + }, + "sections.setPageSetup": { + "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" + }, + "sections.setSectionDirection": { + "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" + }, + "sections.setTitlePage": { + "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" + }, + "sections.setVerticalAlign": { + "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" + }, "styles.apply": { "additionalProperties": false, "properties": { @@ -3671,6 +4494,25 @@ _No fields._ "styles.apply", "create.paragraph", "create.heading", + "create.sectionBreak", + "sections.list", + "sections.get", + "sections.setBreakType", + "sections.setPageMargins", + "sections.setHeaderFooterMargins", + "sections.setPageSetup", + "sections.setColumns", + "sections.setLineNumbering", + "sections.setPageNumbering", + "sections.setTitlePage", + "sections.setOddEvenHeadersFooters", + "sections.setVerticalAlign", + "sections.setSectionDirection", + "sections.setHeaderFooterRef", + "sections.clearHeaderFooterRef", + "sections.setLinkToPrevious", + "sections.setPageBorders", + "sections.clearPageBorders", "lists.list", "lists.get", "lists.insert", diff --git a/apps/docs/document-api/reference/comments/create.mdx b/apps/docs/document-api/reference/comments/create.mdx index b57693dfcf..c63d1d0727 100644 --- a/apps/docs/document-api/reference/comments/create.mdx +++ b/apps/docs/document-api/reference/comments/create.mdx @@ -1,7 +1,7 @@ --- title: comments.create sidebarTitle: comments.create -description: Reference for comments.create +description: Create a new comment thread (or reply when parentCommentId is given). --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for comments.create ## Summary +Create a new comment thread (or reply when parentCommentId is given). + - Operation ID: `comments.create` - API member path: `editor.doc.comments.create(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for comments.create - Supports dry run: `no` - Deterministic target resolution: `yes` +## Expected result + +Returns a Receipt confirming the comment was created; reports NO_OP if the anchor target is invalid. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/comments/delete.mdx b/apps/docs/document-api/reference/comments/delete.mdx index 1d8353e0e0..ee7e442944 100644 --- a/apps/docs/document-api/reference/comments/delete.mdx +++ b/apps/docs/document-api/reference/comments/delete.mdx @@ -1,7 +1,7 @@ --- title: comments.delete sidebarTitle: comments.delete -description: Reference for comments.delete +description: Remove a comment or reply by ID. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for comments.delete ## Summary +Remove a comment or reply by ID. + - Operation ID: `comments.delete` - API member path: `editor.doc.comments.delete(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for comments.delete - Supports dry run: `no` - Deterministic target resolution: `yes` +## Expected result + +Returns a Receipt confirming the comment was removed; reports NO_OP if the comment was already deleted. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/comments/get.mdx b/apps/docs/document-api/reference/comments/get.mdx index 10f6d07809..c96d77c89e 100644 --- a/apps/docs/document-api/reference/comments/get.mdx +++ b/apps/docs/document-api/reference/comments/get.mdx @@ -1,7 +1,7 @@ --- title: comments.get sidebarTitle: comments.get -description: Reference for comments.get +description: Retrieve a single comment thread by ID. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for comments.get ## Summary +Retrieve a single comment thread by ID. + - Operation ID: `comments.get` - API member path: `editor.doc.comments.get(...)` - Mutates document: `no` @@ -18,6 +20,10 @@ description: Reference for comments.get - Supports dry run: `no` - Deterministic target resolution: `yes` +## Expected result + +Returns a CommentInfo object with the comment text, author, date, and thread metadata. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/comments/list.mdx b/apps/docs/document-api/reference/comments/list.mdx index 0837e2daac..c544c1f3c1 100644 --- a/apps/docs/document-api/reference/comments/list.mdx +++ b/apps/docs/document-api/reference/comments/list.mdx @@ -1,7 +1,7 @@ --- title: comments.list sidebarTitle: comments.list -description: Reference for comments.list +description: List all comment threads in the document. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for comments.list ## Summary +List all comment threads in the document. + - Operation ID: `comments.list` - API member path: `editor.doc.comments.list(...)` - Mutates document: `no` @@ -18,6 +20,10 @@ description: Reference for comments.list - Supports dry run: `no` - Deterministic target resolution: `yes` +## Expected result + +Returns a CommentsListResult with an array of comment threads and total count. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/comments/patch.mdx b/apps/docs/document-api/reference/comments/patch.mdx index 81eb1cbf14..f8e943a191 100644 --- a/apps/docs/document-api/reference/comments/patch.mdx +++ b/apps/docs/document-api/reference/comments/patch.mdx @@ -1,7 +1,7 @@ --- title: comments.patch sidebarTitle: comments.patch -description: Reference for comments.patch +description: Patch fields on an existing comment (text, target, status, or isInternal). --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for comments.patch ## Summary +Patch fields on an existing comment (text, target, status, or isInternal). + - Operation ID: `comments.patch` - API member path: `editor.doc.comments.patch(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for comments.patch - Supports dry run: `no` - Deterministic target resolution: `yes` +## Expected result + +Returns a Receipt confirming the comment was updated; reports NO_OP if no fields changed. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/create/heading.mdx b/apps/docs/document-api/reference/create/heading.mdx index d9f5168102..8c5d0f5675 100644 --- a/apps/docs/document-api/reference/create/heading.mdx +++ b/apps/docs/document-api/reference/create/heading.mdx @@ -1,7 +1,7 @@ --- title: create.heading sidebarTitle: create.heading -description: Reference for create.heading +description: Create a new heading at the target position. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for create.heading ## Summary +Create a new heading at the target position. + - Operation ID: `create.heading` - API member path: `editor.doc.create.heading(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for create.heading - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a CreateHeadingResult with the new heading block ID and address. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/create/index.mdx b/apps/docs/document-api/reference/create/index.mdx index 8ac342f422..b5f2a38252 100644 --- a/apps/docs/document-api/reference/create/index.mdx +++ b/apps/docs/document-api/reference/create/index.mdx @@ -16,5 +16,6 @@ Structured creation helpers. | --- | --- | --- | --- | --- | --- | | create.paragraph | `create.paragraph` | Yes | `non-idempotent` | Yes | Yes | | create.heading | `create.heading` | Yes | `non-idempotent` | Yes | Yes | +| create.sectionBreak | `create.sectionBreak` | Yes | `non-idempotent` | No | Yes | | create.table | `create.table` | Yes | `non-idempotent` | Yes | Yes | diff --git a/apps/docs/document-api/reference/create/paragraph.mdx b/apps/docs/document-api/reference/create/paragraph.mdx index d92f76e5a4..43e195adff 100644 --- a/apps/docs/document-api/reference/create/paragraph.mdx +++ b/apps/docs/document-api/reference/create/paragraph.mdx @@ -1,7 +1,7 @@ --- title: create.paragraph sidebarTitle: create.paragraph -description: Reference for create.paragraph +description: Create a new paragraph at the target position. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for create.paragraph ## Summary +Create a new paragraph at the target position. + - Operation ID: `create.paragraph` - API member path: `editor.doc.create.paragraph(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for create.paragraph - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a CreateParagraphResult with the new paragraph block ID and address. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/create/section-break.mdx b/apps/docs/document-api/reference/create/section-break.mdx new file mode 100644 index 0000000000..37d20e2a7a --- /dev/null +++ b/apps/docs/document-api/reference/create/section-break.mdx @@ -0,0 +1,324 @@ +--- +title: create.sectionBreak +sidebarTitle: create.sectionBreak +description: Create a section break at the target location with optional initial section properties. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Create a section break at the target location with optional initial section properties. + +- Operation ID: `create.sectionBreak` +- API member path: `editor.doc.create.sectionBreak(...)` +- Mutates document: `yes` +- Idempotency: `non-idempotent` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a CreateSectionBreakResult with the new section break position and section address. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `at` | object(kind="documentStart") \\| object(kind="documentEnd") \\| object(kind="before") \\| object(kind="after") | no | One of: object(kind="documentStart"), object(kind="documentEnd"), object(kind="before"), object(kind="after") | +| `breakType` | enum | no | `"continuous"`, `"nextPage"`, `"evenPage"`, `"oddPage"` | +| `headerFooterMargins` | object | no | | +| `pageMargins` | object | no | | + +### Example request + +```json +{ + "at": { + "kind": "documentStart" + }, + "breakType": "continuous" +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "breakParagraph": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + }, + "section": { + "kind": "section", + "sectionId": "example" + }, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `AMBIGUOUS_TARGET` +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` +- `INTERNAL_ERROR` + +## Non-applied failure codes + +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "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" + } + ] + }, + "breakType": { + "enum": [ + "continuous", + "nextPage", + "evenPage", + "oddPage" + ] + }, + "headerFooterMargins": { + "additionalProperties": false, + "properties": { + "footer": { + "minimum": 0, + "type": "number" + }, + "header": { + "minimum": 0, + "type": "number" + } + }, + "type": "object" + }, + "pageMargins": { + "additionalProperties": false, + "properties": { + "bottom": { + "minimum": 0, + "type": "number" + }, + "gutter": { + "minimum": 0, + "type": "number" + }, + "left": { + "minimum": 0, + "type": "number" + }, + "right": { + "minimum": 0, + "type": "number" + }, + "top": { + "minimum": 0, + "type": "number" + } + }, + "type": "object" + } + }, + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "breakParagraph": { + "$ref": "#/$defs/BlockNodeAddress" + }, + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "breakParagraph": { + "$ref": "#/$defs/BlockNodeAddress" + }, + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "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/table.mdx b/apps/docs/document-api/reference/create/table.mdx index b81ea102b0..fc0ce3a0b2 100644 --- a/apps/docs/document-api/reference/create/table.mdx +++ b/apps/docs/document-api/reference/create/table.mdx @@ -1,7 +1,7 @@ --- title: create.table sidebarTitle: create.table -description: Reference for create.table +description: Create a new table at the target position. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for create.table ## Summary +Create a new table at the target position. + - Operation ID: `create.table` - API member path: `editor.doc.create.table(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for create.table - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a CreateTableResult with the new table block ID and address. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/delete.mdx b/apps/docs/document-api/reference/delete.mdx index 4b6e50fa7d..ce4550ce25 100644 --- a/apps/docs/document-api/reference/delete.mdx +++ b/apps/docs/document-api/reference/delete.mdx @@ -1,7 +1,7 @@ --- title: delete sidebarTitle: delete -description: Reference for delete +description: Delete content at a target position. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for delete ## Summary +Delete content at a target position. + - Operation ID: `delete` - API member path: `editor.doc.delete(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for delete - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TextMutationReceipt with applied status; receipt reports NO_OP if the target range is already empty. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/find.mdx b/apps/docs/document-api/reference/find.mdx index 2cb6564ffd..514c797656 100644 --- a/apps/docs/document-api/reference/find.mdx +++ b/apps/docs/document-api/reference/find.mdx @@ -1,7 +1,7 @@ --- title: find sidebarTitle: find -description: Reference for find +description: Search the document for nodes matching type, text, or attribute criteria. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for find ## Summary +Search the document for nodes matching type, text, or attribute criteria. + - Operation ID: `find` - API member path: `editor.doc.find(...)` - Mutates document: `no` @@ -18,6 +20,10 @@ description: Reference for find - Supports dry run: `no` - Deterministic target resolution: `no` +## Expected result + +Returns a FindOutput with matched items array and total count, or an empty items array if no nodes match. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/format/align.mdx b/apps/docs/document-api/reference/format/align.mdx index 242f01e3fa..b347b66417 100644 --- a/apps/docs/document-api/reference/format/align.mdx +++ b/apps/docs/document-api/reference/format/align.mdx @@ -1,7 +1,7 @@ --- title: format.align sidebarTitle: format.align -description: Reference for format.align +description: Set or unset paragraph alignment on the block containing the target. Pass null to reset to default. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for format.align ## Summary +Set or unset paragraph alignment on the block containing the target. Pass null to reset to default. + - Operation ID: `format.align` - API member path: `editor.doc.format.align(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for format.align - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TextMutationReceipt; receipt reports NO_OP if the block already has the requested alignment. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/format/apply.mdx b/apps/docs/document-api/reference/format/apply.mdx index bf1aba37f7..89611ab9f3 100644 --- a/apps/docs/document-api/reference/format/apply.mdx +++ b/apps/docs/document-api/reference/format/apply.mdx @@ -1,7 +1,7 @@ --- title: format.apply sidebarTitle: format.apply -description: Reference for format.apply +description: "Apply explicit inline style changes (bold, italic, underline, strike) to the target range using directive semantics ('on', 'off', 'clear')." --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for format.apply ## Summary +Apply explicit inline style changes (bold, italic, underline, strike) to the target range using directive semantics ('on', 'off', 'clear'). + - Operation ID: `format.apply` - API member path: `editor.doc.format.apply(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for format.apply - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TextMutationReceipt confirming inline styles were applied to the target range. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/format/color.mdx b/apps/docs/document-api/reference/format/color.mdx index 279b08234c..d3a1d75c69 100644 --- a/apps/docs/document-api/reference/format/color.mdx +++ b/apps/docs/document-api/reference/format/color.mdx @@ -1,7 +1,7 @@ --- title: format.color sidebarTitle: format.color -description: Reference for format.color +description: Set or unset the text color on the target text range. Pass null to remove. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for format.color ## Summary +Set or unset the text color on the target text range. Pass null to remove. + - Operation ID: `format.color` - API member path: `editor.doc.format.color(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for format.color - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TextMutationReceipt; receipt reports NO_OP if the target already has the requested color. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/format/font-family.mdx b/apps/docs/document-api/reference/format/font-family.mdx index ace8d2108d..339c0d9657 100644 --- a/apps/docs/document-api/reference/format/font-family.mdx +++ b/apps/docs/document-api/reference/format/font-family.mdx @@ -1,7 +1,7 @@ --- title: format.fontFamily sidebarTitle: format.fontFamily -description: Reference for format.fontFamily +description: Set or unset the font family on the target text range. Pass null to remove. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for format.fontFamily ## Summary +Set or unset the font family on the target text range. Pass null to remove. + - Operation ID: `format.fontFamily` - API member path: `editor.doc.format.fontFamily(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for format.fontFamily - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TextMutationReceipt; receipt reports NO_OP if the target already has the requested font family. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/format/font-size.mdx b/apps/docs/document-api/reference/format/font-size.mdx index 8f35ec3f58..3d17bde201 100644 --- a/apps/docs/document-api/reference/format/font-size.mdx +++ b/apps/docs/document-api/reference/format/font-size.mdx @@ -1,7 +1,7 @@ --- title: format.fontSize sidebarTitle: format.fontSize -description: Reference for format.fontSize +description: Set or unset the font size on the target text range. Pass null to remove. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for format.fontSize ## Summary +Set or unset the font size on the target text range. Pass null to remove. + - Operation ID: `format.fontSize` - API member path: `editor.doc.format.fontSize(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for format.fontSize - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TextMutationReceipt; receipt reports NO_OP if the target already has the requested font size. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/get-node-by-id.mdx b/apps/docs/document-api/reference/get-node-by-id.mdx index 026b59338c..bc34da201f 100644 --- a/apps/docs/document-api/reference/get-node-by-id.mdx +++ b/apps/docs/document-api/reference/get-node-by-id.mdx @@ -1,7 +1,7 @@ --- title: getNodeById sidebarTitle: getNodeById -description: Reference for getNodeById +description: Retrieve a single node by its unique ID. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for getNodeById ## Summary +Retrieve a single node by its unique ID. + - Operation ID: `getNodeById` - API member path: `editor.doc.getNodeById(...)` - Mutates document: `no` @@ -18,6 +20,10 @@ description: Reference for getNodeById - Supports dry run: `no` - Deterministic target resolution: `yes` +## Expected result + +Returns a NodeInfo object with the node type, address, content, and typed properties. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/get-node.mdx b/apps/docs/document-api/reference/get-node.mdx index 3c83af062a..983003e3ca 100644 --- a/apps/docs/document-api/reference/get-node.mdx +++ b/apps/docs/document-api/reference/get-node.mdx @@ -1,7 +1,7 @@ --- title: getNode sidebarTitle: getNode -description: Reference for getNode +description: Retrieve a single node by target position. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for getNode ## Summary +Retrieve a single node by target position. + - Operation ID: `getNode` - API member path: `editor.doc.getNode(...)` - Mutates document: `no` @@ -18,6 +20,10 @@ description: Reference for getNode - Supports dry run: `no` - Deterministic target resolution: `yes` +## Expected result + +Returns a NodeInfo object with the node type, address, content, and typed properties. + ## Input fields _No fields._ diff --git a/apps/docs/document-api/reference/get-text.mdx b/apps/docs/document-api/reference/get-text.mdx index 0abdf2f0cc..93812fe05f 100644 --- a/apps/docs/document-api/reference/get-text.mdx +++ b/apps/docs/document-api/reference/get-text.mdx @@ -1,7 +1,7 @@ --- title: getText sidebarTitle: getText -description: Reference for getText +description: Extract the plain-text content of the document. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for getText ## Summary +Extract the plain-text content of the document. + - Operation ID: `getText` - API member path: `editor.doc.getText(...)` - Mutates document: `no` @@ -18,6 +20,10 @@ description: Reference for getText - Supports dry run: `no` - Deterministic target resolution: `yes` +## Expected result + +Returns the full plain-text content of the document as a string. + ## Input fields _No fields._ diff --git a/apps/docs/document-api/reference/index.mdx b/apps/docs/document-api/reference/index.mdx index d633f5e7a0..ae1ef0a061 100644 --- a/apps/docs/document-api/reference/index.mdx +++ b/apps/docs/document-api/reference/index.mdx @@ -23,7 +23,8 @@ Document API is currently alpha and subject to breaking changes. | Core | 8 | 0 | 8 | [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 | 3 | 0 | 3 | [Open](/document-api/reference/create/index) | +| Create | 4 | 0 | 4 | [Open](/document-api/reference/create/index) | +| Sections | 18 | 0 | 18 | [Open](/document-api/reference/sections/index) | | Format | 5 | 4 | 9 | [Open](/document-api/reference/format/index) | | Styles | 1 | 0 | 1 | [Open](/document-api/reference/styles/index) | | Lists | 8 | 0 | 8 | [Open](/document-api/reference/lists/index) | @@ -68,8 +69,32 @@ The tables below are grouped by namespace. | --- | --- | --- | | create.paragraph | editor.doc.create.paragraph(...) | Create a new paragraph at the target position. | | create.heading | editor.doc.create.heading(...) | Create a new heading at the target position. | +| 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. | +#### Sections + +| Operation | API member path | Description | +| --- | --- | --- | +| sections.list | editor.doc.sections.list(...) | List sections in deterministic order with section-target handles. | +| sections.get | editor.doc.sections.get(...) | Retrieve full section information by section address. | +| sections.setBreakType | editor.doc.sections.setBreakType(...) | Set the section break type. | +| sections.setPageMargins | editor.doc.sections.setPageMargins(...) | Set page-edge margins for a section. | +| sections.setHeaderFooterMargins | editor.doc.sections.setHeaderFooterMargins(...) | Set header/footer margin distances for a section. | +| sections.setPageSetup | editor.doc.sections.setPageSetup(...) | Set page size/orientation properties for a section. | +| sections.setColumns | editor.doc.sections.setColumns(...) | Set column configuration for a section. | +| sections.setLineNumbering | editor.doc.sections.setLineNumbering(...) | Enable or configure line numbering for a section. | +| sections.setPageNumbering | editor.doc.sections.setPageNumbering(...) | Set page numbering format/start for a section. | +| sections.setTitlePage | editor.doc.sections.setTitlePage(...) | Enable or disable title-page behavior for a section. | +| sections.setOddEvenHeadersFooters | editor.doc.sections.setOddEvenHeadersFooters(...) | Enable or disable odd/even header-footer mode in document settings. | +| sections.setVerticalAlign | editor.doc.sections.setVerticalAlign(...) | Set vertical page alignment for a section. | +| sections.setSectionDirection | editor.doc.sections.setSectionDirection(...) | Set section text flow direction (LTR/RTL). | +| sections.setHeaderFooterRef | editor.doc.sections.setHeaderFooterRef(...) | Set or replace a section header/footer reference for a variant. | +| sections.clearHeaderFooterRef | editor.doc.sections.clearHeaderFooterRef(...) | Clear a section header/footer reference for a specific variant. | +| sections.setLinkToPrevious | editor.doc.sections.setLinkToPrevious(...) | Set or clear link-to-previous behavior for a header/footer variant. | +| sections.setPageBorders | editor.doc.sections.setPageBorders(...) | Set page border configuration for a section. | +| sections.clearPageBorders | editor.doc.sections.clearPageBorders(...) | Clear page border configuration for a section. | + #### Format | Operation | API member path | Description | diff --git a/apps/docs/document-api/reference/info.mdx b/apps/docs/document-api/reference/info.mdx index 689c62c5be..88c672ee03 100644 --- a/apps/docs/document-api/reference/info.mdx +++ b/apps/docs/document-api/reference/info.mdx @@ -1,7 +1,7 @@ --- title: info sidebarTitle: info -description: Reference for info +description: Return document metadata including revision, node count, and capabilities. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for info ## Summary +Return document metadata including revision, node count, and capabilities. + - Operation ID: `info` - API member path: `editor.doc.info(...)` - Mutates document: `no` @@ -18,6 +20,10 @@ description: Reference for info - Supports dry run: `no` - Deterministic target resolution: `yes` +## Expected result + +Returns a DocumentInfo object with revision, word/paragraph/heading counts, and capability flags. + ## Input fields _No fields._ diff --git a/apps/docs/document-api/reference/insert.mdx b/apps/docs/document-api/reference/insert.mdx index e70e395eb9..f3c4f23d55 100644 --- a/apps/docs/document-api/reference/insert.mdx +++ b/apps/docs/document-api/reference/insert.mdx @@ -1,7 +1,7 @@ --- title: insert sidebarTitle: insert -description: Reference for insert +description: "Insert content at a target position. Supports text (default), markdown, and html content types via the `type` field." --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for insert ## Summary +Insert content at a target position. Supports text (default), markdown, and html content types via the `type` field. + - Operation ID: `insert` - API member path: `editor.doc.insert(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for insert - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TextMutationReceipt with applied status; receipt reports NO_OP if the insertion point is invalid or content is empty. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/lists/exit.mdx b/apps/docs/document-api/reference/lists/exit.mdx index d006bb940b..028dc7fc9d 100644 --- a/apps/docs/document-api/reference/lists/exit.mdx +++ b/apps/docs/document-api/reference/lists/exit.mdx @@ -1,7 +1,7 @@ --- title: lists.exit sidebarTitle: lists.exit -description: Reference for lists.exit +description: Exit a list context, converting the target item to a paragraph. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for lists.exit ## Summary +Exit a list context, converting the target item to a paragraph. + - Operation ID: `lists.exit` - API member path: `editor.doc.lists.exit(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for lists.exit - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a ListsExitResult confirming the item was converted to a plain paragraph. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/lists/get.mdx b/apps/docs/document-api/reference/lists/get.mdx index bc4c0fb9ee..a8743398fc 100644 --- a/apps/docs/document-api/reference/lists/get.mdx +++ b/apps/docs/document-api/reference/lists/get.mdx @@ -1,7 +1,7 @@ --- title: lists.get sidebarTitle: lists.get -description: Reference for lists.get +description: Retrieve a specific list node by target. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for lists.get ## Summary +Retrieve a specific list node by target. + - Operation ID: `lists.get` - API member path: `editor.doc.lists.get(...)` - Mutates document: `no` @@ -18,6 +20,10 @@ description: Reference for lists.get - Supports dry run: `no` - Deterministic target resolution: `yes` +## Expected result + +Returns a ListItemInfo object with the item kind, level, marker, and address. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/lists/indent.mdx b/apps/docs/document-api/reference/lists/indent.mdx index 98fb66511a..a2786cdcc4 100644 --- a/apps/docs/document-api/reference/lists/indent.mdx +++ b/apps/docs/document-api/reference/lists/indent.mdx @@ -1,7 +1,7 @@ --- title: lists.indent sidebarTitle: lists.indent -description: Reference for lists.indent +description: Increase the indentation level of a list item. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for lists.indent ## Summary +Increase the indentation level of a list item. + - Operation ID: `lists.indent` - API member path: `editor.doc.lists.indent(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for lists.indent - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a ListsMutateItemResult receipt; reports NO_OP if the item is already at maximum indent level. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/lists/insert.mdx b/apps/docs/document-api/reference/lists/insert.mdx index 2aa475918b..efcfe3269f 100644 --- a/apps/docs/document-api/reference/lists/insert.mdx +++ b/apps/docs/document-api/reference/lists/insert.mdx @@ -1,7 +1,7 @@ --- title: lists.insert sidebarTitle: lists.insert -description: Reference for lists.insert +description: Insert a new list at the target position. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for lists.insert ## Summary +Insert a new list at the target position. + - Operation ID: `lists.insert` - API member path: `editor.doc.lists.insert(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for lists.insert - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a ListsInsertResult with the new list item address and block ID. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/lists/list.mdx b/apps/docs/document-api/reference/lists/list.mdx index df6644a2e7..6389dfc452 100644 --- a/apps/docs/document-api/reference/lists/list.mdx +++ b/apps/docs/document-api/reference/lists/list.mdx @@ -1,7 +1,7 @@ --- title: lists.list sidebarTitle: lists.list -description: Reference for lists.list +description: List all list nodes in the document, optionally filtered by scope. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for lists.list ## Summary +List all list nodes in the document, optionally filtered by scope. + - Operation ID: `lists.list` - API member path: `editor.doc.lists.list(...)` - Mutates document: `no` @@ -18,6 +20,10 @@ description: Reference for lists.list - Supports dry run: `no` - Deterministic target resolution: `yes` +## Expected result + +Returns a ListsListResult with an array of list item summaries and total count. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/lists/outdent.mdx b/apps/docs/document-api/reference/lists/outdent.mdx index 1a3e89dea8..4b9f7faae6 100644 --- a/apps/docs/document-api/reference/lists/outdent.mdx +++ b/apps/docs/document-api/reference/lists/outdent.mdx @@ -1,7 +1,7 @@ --- title: lists.outdent sidebarTitle: lists.outdent -description: Reference for lists.outdent +description: Decrease the indentation level of a list item. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for lists.outdent ## Summary +Decrease the indentation level of a list item. + - Operation ID: `lists.outdent` - API member path: `editor.doc.lists.outdent(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for lists.outdent - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a ListsMutateItemResult receipt; reports NO_OP if the item is already at the root level. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/lists/restart.mdx b/apps/docs/document-api/reference/lists/restart.mdx index 9dd5b39073..33955478b2 100644 --- a/apps/docs/document-api/reference/lists/restart.mdx +++ b/apps/docs/document-api/reference/lists/restart.mdx @@ -1,7 +1,7 @@ --- title: lists.restart sidebarTitle: lists.restart -description: Reference for lists.restart +description: Restart numbering of an ordered list at the target item. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for lists.restart ## Summary +Restart numbering of an ordered list at the target item. + - Operation ID: `lists.restart` - API member path: `editor.doc.lists.restart(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for lists.restart - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a ListsMutateItemResult receipt; reports NO_OP if numbering already restarts at the target item. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/lists/set-type.mdx b/apps/docs/document-api/reference/lists/set-type.mdx index ee67114987..c16e730f9c 100644 --- a/apps/docs/document-api/reference/lists/set-type.mdx +++ b/apps/docs/document-api/reference/lists/set-type.mdx @@ -1,7 +1,7 @@ --- title: lists.setType sidebarTitle: lists.setType -description: Reference for lists.setType +description: Change the list type (ordered, unordered) of a target list. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for lists.setType ## Summary +Change the list type (ordered, unordered) of a target list. + - Operation ID: `lists.setType` - API member path: `editor.doc.lists.setType(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for lists.setType - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a ListsMutateItemResult receipt; reports NO_OP if the list already has the requested type. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/mutations/apply.mdx b/apps/docs/document-api/reference/mutations/apply.mdx index c0c542a119..c1b1f42816 100644 --- a/apps/docs/document-api/reference/mutations/apply.mdx +++ b/apps/docs/document-api/reference/mutations/apply.mdx @@ -1,7 +1,7 @@ --- title: mutations.apply sidebarTitle: mutations.apply -description: Reference for mutations.apply +description: Execute a mutation plan atomically against the document. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for mutations.apply ## Summary +Execute a mutation plan atomically against the document. + - Operation ID: `mutations.apply` - API member path: `editor.doc.mutations.apply(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for mutations.apply - Supports dry run: `no` - Deterministic target resolution: `yes` +## Expected result + +Returns a PlanReceipt with per-step results for the atomically applied mutation plan. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/mutations/preview.mdx b/apps/docs/document-api/reference/mutations/preview.mdx index a4734b9cf5..74108cb731 100644 --- a/apps/docs/document-api/reference/mutations/preview.mdx +++ b/apps/docs/document-api/reference/mutations/preview.mdx @@ -1,7 +1,7 @@ --- title: mutations.preview sidebarTitle: mutations.preview -description: Reference for mutations.preview +description: Dry-run a mutation plan, returning resolved targets without applying changes. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for mutations.preview ## Summary +Dry-run a mutation plan, returning resolved targets without applying changes. + - Operation ID: `mutations.preview` - API member path: `editor.doc.mutations.preview(...)` - Mutates document: `no` @@ -18,6 +20,10 @@ description: Reference for mutations.preview - Supports dry run: `no` - Deterministic target resolution: `yes` +## Expected result + +Returns a MutationsPreviewOutput with resolved targets and step details without applying changes. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/query/match.mdx b/apps/docs/document-api/reference/query/match.mdx index 25a1b85fbd..3dc181f0e4 100644 --- a/apps/docs/document-api/reference/query/match.mdx +++ b/apps/docs/document-api/reference/query/match.mdx @@ -1,7 +1,7 @@ --- title: query.match sidebarTitle: query.match -description: Reference for query.match +description: Deterministic selector-based search with cardinality contracts for mutation targeting. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for query.match ## Summary +Deterministic selector-based search with cardinality contracts for mutation targeting. + - Operation ID: `query.match` - API member path: `editor.doc.query.match(...)` - Mutates document: `no` @@ -18,6 +20,10 @@ description: Reference for query.match - Supports dry run: `no` - Deterministic target resolution: `yes` +## Expected result + +Returns a QueryMatchOutput with the resolved target address and cardinality metadata. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/replace.mdx b/apps/docs/document-api/reference/replace.mdx index 40a97a558c..24219c2552 100644 --- a/apps/docs/document-api/reference/replace.mdx +++ b/apps/docs/document-api/reference/replace.mdx @@ -1,7 +1,7 @@ --- title: replace sidebarTitle: replace -description: Reference for replace +description: Replace content at a target position with new text or inline content. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for replace ## Summary +Replace content at a target position with new text or inline content. + - Operation ID: `replace` - API member path: `editor.doc.replace(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for replace - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TextMutationReceipt with applied status; receipt reports NO_OP if the target range already contains identical content. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/sections/clear-header-footer-ref.mdx b/apps/docs/document-api/reference/sections/clear-header-footer-ref.mdx new file mode 100644 index 0000000000..85d708cc00 --- /dev/null +++ b/apps/docs/document-api/reference/sections/clear-header-footer-ref.mdx @@ -0,0 +1,229 @@ +--- +title: sections.clearHeaderFooterRef +sidebarTitle: sections.clearHeaderFooterRef +description: Clear a section header/footer reference for a specific variant. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Clear a section header/footer reference for a specific variant. + +- Operation ID: `sections.clearHeaderFooterRef` +- API member path: `editor.doc.sections.clearHeaderFooterRef(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a SectionMutationResult receipt; reports NO_OP if no reference exists for the specified variant. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `kind` | enum | yes | `"header"`, `"footer"` | +| `target` | SectionAddress | yes | SectionAddress | +| `variant` | enum | yes | `"default"`, `"first"`, `"even"` | + +### Example request + +```json +{ + "kind": "header", + "target": { + "kind": "section", + "sectionId": "example" + }, + "variant": "default" +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "section": { + "kind": "section", + "sectionId": "example" + }, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` +- `INTERNAL_ERROR` + +## Non-applied failure codes + +- `NO_OP` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "kind": { + "enum": [ + "header", + "footer" + ] + }, + "target": { + "$ref": "#/$defs/SectionAddress" + }, + "variant": { + "enum": [ + "default", + "first", + "even" + ] + } + }, + "required": [ + "target", + "kind", + "variant" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/sections/clear-page-borders.mdx b/apps/docs/document-api/reference/sections/clear-page-borders.mdx new file mode 100644 index 0000000000..d02cbb15f9 --- /dev/null +++ b/apps/docs/document-api/reference/sections/clear-page-borders.mdx @@ -0,0 +1,210 @@ +--- +title: sections.clearPageBorders +sidebarTitle: sections.clearPageBorders +description: Clear page border configuration for a section. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Clear page border configuration for a section. + +- Operation ID: `sections.clearPageBorders` +- API member path: `editor.doc.sections.clearPageBorders(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a SectionMutationResult receipt; reports NO_OP if no page borders are configured on the section. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | SectionAddress | yes | SectionAddress | + +### Example request + +```json +{ + "target": { + "kind": "section", + "sectionId": "example" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "section": { + "kind": "section", + "sectionId": "example" + }, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` +- `INTERNAL_ERROR` + +## Non-applied failure codes + +- `NO_OP` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/SectionAddress" + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/sections/get.mdx b/apps/docs/document-api/reference/sections/get.mdx new file mode 100644 index 0000000000..18982c7426 --- /dev/null +++ b/apps/docs/document-api/reference/sections/get.mdx @@ -0,0 +1,639 @@ +--- +title: sections.get +sidebarTitle: sections.get +description: Retrieve full section information by section address. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Retrieve full section information by section address. + +- Operation ID: `sections.get` +- API member path: `editor.doc.sections.get(...)` +- Mutates document: `no` +- Idempotency: `idempotent` +- Supports tracked mode: `no` +- Supports dry run: `no` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a SectionInfo object with full section properties including margins, columns, and header/footer refs. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `address` | SectionAddress | yes | SectionAddress | + +### Example request + +```json +{ + "address": { + "kind": "section", + "sectionId": "example" + } +} +``` + +## Output fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `address` | SectionAddress | yes | SectionAddress | +| `breakType` | enum | no | `"continuous"`, `"nextPage"`, `"evenPage"`, `"oddPage"` | +| `columns` | object | no | | +| `footerRefs` | object | no | | +| `headerFooterMargins` | object | no | | +| `headerRefs` | object | no | | +| `index` | integer | yes | | +| `lineNumbering` | object | no | | +| `margins` | object | no | | +| `oddEvenHeadersFooters` | boolean | no | | +| `pageBorders` | any \\| any \\| any \\| any \\| any \\| any \\| any | no | One of: any, any, any, any, any, any, any | +| `pageNumbering` | object | no | | +| `pageSetup` | object | no | | +| `range` | object | yes | | +| `sectionDirection` | enum | no | `"ltr"`, `"rtl"` | +| `titlePage` | boolean | no | | +| `verticalAlign` | enum | no | `"top"`, `"center"`, `"bottom"`, `"both"` | + +### Example response + +```json +{ + "address": { + "kind": "section", + "sectionId": "example" + }, + "breakType": "continuous", + "index": 1, + "pageSetup": { + "height": 12.5, + "width": 12.5 + }, + "range": { + "endParagraphIndex": 1, + "startParagraphIndex": 1 + } +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- None + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "address": { + "$ref": "#/$defs/SectionAddress" + } + }, + "required": [ + "address" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "address": { + "$ref": "#/$defs/SectionAddress" + }, + "breakType": { + "enum": [ + "continuous", + "nextPage", + "evenPage", + "oddPage" + ] + }, + "columns": { + "additionalProperties": false, + "properties": { + "count": { + "minimum": 1, + "type": "integer" + }, + "equalWidth": { + "type": "boolean" + }, + "gap": { + "minimum": 0, + "type": "number" + } + }, + "type": "object" + }, + "footerRefs": { + "additionalProperties": false, + "properties": { + "default": { + "type": "string" + }, + "even": { + "type": "string" + }, + "first": { + "type": "string" + } + }, + "type": "object" + }, + "headerFooterMargins": { + "additionalProperties": false, + "properties": { + "footer": { + "minimum": 0, + "type": "number" + }, + "header": { + "minimum": 0, + "type": "number" + } + }, + "type": "object" + }, + "headerRefs": { + "additionalProperties": false, + "properties": { + "default": { + "type": "string" + }, + "even": { + "type": "string" + }, + "first": { + "type": "string" + } + }, + "type": "object" + }, + "index": { + "minimum": 0, + "type": "integer" + }, + "lineNumbering": { + "additionalProperties": false, + "properties": { + "countBy": { + "minimum": 1, + "type": "integer" + }, + "distance": { + "minimum": 0, + "type": "number" + }, + "enabled": { + "type": "boolean" + }, + "restart": { + "enum": [ + "continuous", + "newPage", + "newSection" + ] + }, + "start": { + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "margins": { + "additionalProperties": false, + "properties": { + "bottom": { + "minimum": 0, + "type": "number" + }, + "gutter": { + "minimum": 0, + "type": "number" + }, + "left": { + "minimum": 0, + "type": "number" + }, + "right": { + "minimum": 0, + "type": "number" + }, + "top": { + "minimum": 0, + "type": "number" + } + }, + "type": "object" + }, + "oddEvenHeadersFooters": { + "type": "boolean" + }, + "pageBorders": { + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "display" + ] + }, + { + "required": [ + "offsetFrom" + ] + }, + { + "required": [ + "zOrder" + ] + }, + { + "required": [ + "top" + ] + }, + { + "required": [ + "right" + ] + }, + { + "required": [ + "bottom" + ] + }, + { + "required": [ + "left" + ] + } + ], + "properties": { + "bottom": { + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "style" + ] + }, + { + "required": [ + "size" + ] + }, + { + "required": [ + "space" + ] + }, + { + "required": [ + "color" + ] + }, + { + "required": [ + "shadow" + ] + }, + { + "required": [ + "frame" + ] + } + ], + "properties": { + "color": { + "type": "string" + }, + "frame": { + "type": "boolean" + }, + "shadow": { + "type": "boolean" + }, + "size": { + "minimum": 0, + "type": "number" + }, + "space": { + "minimum": 0, + "type": "number" + }, + "style": { + "type": "string" + } + }, + "type": "object" + }, + "display": { + "enum": [ + "allPages", + "firstPage", + "notFirstPage" + ] + }, + "left": { + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "style" + ] + }, + { + "required": [ + "size" + ] + }, + { + "required": [ + "space" + ] + }, + { + "required": [ + "color" + ] + }, + { + "required": [ + "shadow" + ] + }, + { + "required": [ + "frame" + ] + } + ], + "properties": { + "color": { + "type": "string" + }, + "frame": { + "type": "boolean" + }, + "shadow": { + "type": "boolean" + }, + "size": { + "minimum": 0, + "type": "number" + }, + "space": { + "minimum": 0, + "type": "number" + }, + "style": { + "type": "string" + } + }, + "type": "object" + }, + "offsetFrom": { + "enum": [ + "page", + "text" + ] + }, + "right": { + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "style" + ] + }, + { + "required": [ + "size" + ] + }, + { + "required": [ + "space" + ] + }, + { + "required": [ + "color" + ] + }, + { + "required": [ + "shadow" + ] + }, + { + "required": [ + "frame" + ] + } + ], + "properties": { + "color": { + "type": "string" + }, + "frame": { + "type": "boolean" + }, + "shadow": { + "type": "boolean" + }, + "size": { + "minimum": 0, + "type": "number" + }, + "space": { + "minimum": 0, + "type": "number" + }, + "style": { + "type": "string" + } + }, + "type": "object" + }, + "top": { + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "style" + ] + }, + { + "required": [ + "size" + ] + }, + { + "required": [ + "space" + ] + }, + { + "required": [ + "color" + ] + }, + { + "required": [ + "shadow" + ] + }, + { + "required": [ + "frame" + ] + } + ], + "properties": { + "color": { + "type": "string" + }, + "frame": { + "type": "boolean" + }, + "shadow": { + "type": "boolean" + }, + "size": { + "minimum": 0, + "type": "number" + }, + "space": { + "minimum": 0, + "type": "number" + }, + "style": { + "type": "string" + } + }, + "type": "object" + }, + "zOrder": { + "enum": [ + "front", + "back" + ] + } + }, + "type": "object" + }, + "pageNumbering": { + "additionalProperties": false, + "properties": { + "format": { + "enum": [ + "decimal", + "lowerLetter", + "upperLetter", + "lowerRoman", + "upperRoman", + "numberInDash" + ] + }, + "start": { + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "pageSetup": { + "additionalProperties": false, + "properties": { + "height": { + "minimum": 0, + "type": "number" + }, + "orientation": { + "enum": [ + "portrait", + "landscape" + ] + }, + "paperSize": { + "type": "string" + }, + "width": { + "minimum": 0, + "type": "number" + } + }, + "type": "object" + }, + "range": { + "additionalProperties": false, + "properties": { + "endParagraphIndex": { + "minimum": 0, + "type": "integer" + }, + "startParagraphIndex": { + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "startParagraphIndex", + "endParagraphIndex" + ], + "type": "object" + }, + "sectionDirection": { + "enum": [ + "ltr", + "rtl" + ] + }, + "titlePage": { + "type": "boolean" + }, + "verticalAlign": { + "enum": [ + "top", + "center", + "bottom", + "both" + ] + } + }, + "required": [ + "address", + "index", + "range" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/sections/index.mdx b/apps/docs/document-api/reference/sections/index.mdx new file mode 100644 index 0000000000..2c7a0638b8 --- /dev/null +++ b/apps/docs/document-api/reference/sections/index.mdx @@ -0,0 +1,35 @@ +--- +title: Sections operations +sidebarTitle: Sections +description: Sections 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) + +Section structure and page-setup operations. + +| Operation | Member path | Mutates | Idempotency | Tracked | Dry run | +| --- | --- | --- | --- | --- | --- | +| sections.list | `sections.list` | No | `idempotent` | No | No | +| sections.get | `sections.get` | No | `idempotent` | No | No | +| sections.setBreakType | `sections.setBreakType` | Yes | `conditional` | No | Yes | +| sections.setPageMargins | `sections.setPageMargins` | Yes | `conditional` | No | Yes | +| sections.setHeaderFooterMargins | `sections.setHeaderFooterMargins` | Yes | `conditional` | No | Yes | +| sections.setPageSetup | `sections.setPageSetup` | Yes | `conditional` | No | Yes | +| sections.setColumns | `sections.setColumns` | Yes | `conditional` | No | Yes | +| sections.setLineNumbering | `sections.setLineNumbering` | Yes | `conditional` | No | Yes | +| sections.setPageNumbering | `sections.setPageNumbering` | Yes | `conditional` | No | Yes | +| sections.setTitlePage | `sections.setTitlePage` | Yes | `conditional` | No | Yes | +| sections.setOddEvenHeadersFooters | `sections.setOddEvenHeadersFooters` | Yes | `conditional` | No | Yes | +| sections.setVerticalAlign | `sections.setVerticalAlign` | Yes | `conditional` | No | Yes | +| sections.setSectionDirection | `sections.setSectionDirection` | Yes | `conditional` | No | Yes | +| sections.setHeaderFooterRef | `sections.setHeaderFooterRef` | Yes | `conditional` | No | Yes | +| sections.clearHeaderFooterRef | `sections.clearHeaderFooterRef` | Yes | `conditional` | No | Yes | +| sections.setLinkToPrevious | `sections.setLinkToPrevious` | Yes | `conditional` | No | Yes | +| sections.setPageBorders | `sections.setPageBorders` | Yes | `conditional` | No | Yes | +| sections.clearPageBorders | `sections.clearPageBorders` | Yes | `conditional` | No | Yes | + diff --git a/apps/docs/document-api/reference/sections/list.mdx b/apps/docs/document-api/reference/sections/list.mdx new file mode 100644 index 0000000000..e5bdc99d60 --- /dev/null +++ b/apps/docs/document-api/reference/sections/list.mdx @@ -0,0 +1,692 @@ +--- +title: sections.list +sidebarTitle: sections.list +description: List sections in deterministic order with section-target handles. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +List sections in deterministic order with section-target handles. + +- Operation ID: `sections.list` +- API member path: `editor.doc.sections.list(...)` +- Mutates document: `no` +- Idempotency: `idempotent` +- Supports tracked mode: `no` +- Supports dry run: `no` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a SectionsListResult with an ordered array of section summaries and their target handles. + +## 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 | +| --- | --- | --- | --- | +| `evaluatedRevision` | string | yes | | +| `items` | object[] | yes | | +| `page` | PageInfo | yes | PageInfo | +| `total` | integer | yes | | + +### Example response + +```json +{ + "evaluatedRevision": "rev-001", + "items": [ + { + "address": { + "kind": "section", + "sectionId": "example" + }, + "breakType": "continuous", + "handle": { + "ref": "handle:abc123", + "refStability": "ephemeral", + "targetKind": "section" + }, + "id": "id-001", + "index": 1, + "pageSetup": { + "height": 12.5, + "width": 12.5 + }, + "range": { + "endParagraphIndex": 1, + "startParagraphIndex": 1 + } + } + ], + "page": { + "limit": 50, + "offset": 0, + "returned": 1 + }, + "total": 1 +} +``` + +## Pre-apply throws + +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- None + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "limit": { + "minimum": 1, + "type": "integer" + }, + "offset": { + "minimum": 0, + "type": "integer" + } + }, + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "evaluatedRevision": { + "type": "string" + }, + "items": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "$ref": "#/$defs/SectionAddress" + }, + "breakType": { + "enum": [ + "continuous", + "nextPage", + "evenPage", + "oddPage" + ] + }, + "columns": { + "additionalProperties": false, + "properties": { + "count": { + "minimum": 1, + "type": "integer" + }, + "equalWidth": { + "type": "boolean" + }, + "gap": { + "minimum": 0, + "type": "number" + } + }, + "type": "object" + }, + "footerRefs": { + "additionalProperties": false, + "properties": { + "default": { + "type": "string" + }, + "even": { + "type": "string" + }, + "first": { + "type": "string" + } + }, + "type": "object" + }, + "handle": { + "additionalProperties": false, + "properties": { + "ref": { + "type": "string" + }, + "refStability": { + "const": "ephemeral" + }, + "targetKind": { + "const": "section" + } + }, + "required": [ + "ref", + "refStability", + "targetKind" + ], + "type": "object" + }, + "headerFooterMargins": { + "additionalProperties": false, + "properties": { + "footer": { + "minimum": 0, + "type": "number" + }, + "header": { + "minimum": 0, + "type": "number" + } + }, + "type": "object" + }, + "headerRefs": { + "additionalProperties": false, + "properties": { + "default": { + "type": "string" + }, + "even": { + "type": "string" + }, + "first": { + "type": "string" + } + }, + "type": "object" + }, + "id": { + "type": "string" + }, + "index": { + "minimum": 0, + "type": "integer" + }, + "lineNumbering": { + "additionalProperties": false, + "properties": { + "countBy": { + "minimum": 1, + "type": "integer" + }, + "distance": { + "minimum": 0, + "type": "number" + }, + "enabled": { + "type": "boolean" + }, + "restart": { + "enum": [ + "continuous", + "newPage", + "newSection" + ] + }, + "start": { + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "margins": { + "additionalProperties": false, + "properties": { + "bottom": { + "minimum": 0, + "type": "number" + }, + "gutter": { + "minimum": 0, + "type": "number" + }, + "left": { + "minimum": 0, + "type": "number" + }, + "right": { + "minimum": 0, + "type": "number" + }, + "top": { + "minimum": 0, + "type": "number" + } + }, + "type": "object" + }, + "oddEvenHeadersFooters": { + "type": "boolean" + }, + "pageBorders": { + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "display" + ] + }, + { + "required": [ + "offsetFrom" + ] + }, + { + "required": [ + "zOrder" + ] + }, + { + "required": [ + "top" + ] + }, + { + "required": [ + "right" + ] + }, + { + "required": [ + "bottom" + ] + }, + { + "required": [ + "left" + ] + } + ], + "properties": { + "bottom": { + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "style" + ] + }, + { + "required": [ + "size" + ] + }, + { + "required": [ + "space" + ] + }, + { + "required": [ + "color" + ] + }, + { + "required": [ + "shadow" + ] + }, + { + "required": [ + "frame" + ] + } + ], + "properties": { + "color": { + "type": "string" + }, + "frame": { + "type": "boolean" + }, + "shadow": { + "type": "boolean" + }, + "size": { + "minimum": 0, + "type": "number" + }, + "space": { + "minimum": 0, + "type": "number" + }, + "style": { + "type": "string" + } + }, + "type": "object" + }, + "display": { + "enum": [ + "allPages", + "firstPage", + "notFirstPage" + ] + }, + "left": { + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "style" + ] + }, + { + "required": [ + "size" + ] + }, + { + "required": [ + "space" + ] + }, + { + "required": [ + "color" + ] + }, + { + "required": [ + "shadow" + ] + }, + { + "required": [ + "frame" + ] + } + ], + "properties": { + "color": { + "type": "string" + }, + "frame": { + "type": "boolean" + }, + "shadow": { + "type": "boolean" + }, + "size": { + "minimum": 0, + "type": "number" + }, + "space": { + "minimum": 0, + "type": "number" + }, + "style": { + "type": "string" + } + }, + "type": "object" + }, + "offsetFrom": { + "enum": [ + "page", + "text" + ] + }, + "right": { + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "style" + ] + }, + { + "required": [ + "size" + ] + }, + { + "required": [ + "space" + ] + }, + { + "required": [ + "color" + ] + }, + { + "required": [ + "shadow" + ] + }, + { + "required": [ + "frame" + ] + } + ], + "properties": { + "color": { + "type": "string" + }, + "frame": { + "type": "boolean" + }, + "shadow": { + "type": "boolean" + }, + "size": { + "minimum": 0, + "type": "number" + }, + "space": { + "minimum": 0, + "type": "number" + }, + "style": { + "type": "string" + } + }, + "type": "object" + }, + "top": { + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "style" + ] + }, + { + "required": [ + "size" + ] + }, + { + "required": [ + "space" + ] + }, + { + "required": [ + "color" + ] + }, + { + "required": [ + "shadow" + ] + }, + { + "required": [ + "frame" + ] + } + ], + "properties": { + "color": { + "type": "string" + }, + "frame": { + "type": "boolean" + }, + "shadow": { + "type": "boolean" + }, + "size": { + "minimum": 0, + "type": "number" + }, + "space": { + "minimum": 0, + "type": "number" + }, + "style": { + "type": "string" + } + }, + "type": "object" + }, + "zOrder": { + "enum": [ + "front", + "back" + ] + } + }, + "type": "object" + }, + "pageNumbering": { + "additionalProperties": false, + "properties": { + "format": { + "enum": [ + "decimal", + "lowerLetter", + "upperLetter", + "lowerRoman", + "upperRoman", + "numberInDash" + ] + }, + "start": { + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "pageSetup": { + "additionalProperties": false, + "properties": { + "height": { + "minimum": 0, + "type": "number" + }, + "orientation": { + "enum": [ + "portrait", + "landscape" + ] + }, + "paperSize": { + "type": "string" + }, + "width": { + "minimum": 0, + "type": "number" + } + }, + "type": "object" + }, + "range": { + "additionalProperties": false, + "properties": { + "endParagraphIndex": { + "minimum": 0, + "type": "integer" + }, + "startParagraphIndex": { + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "startParagraphIndex", + "endParagraphIndex" + ], + "type": "object" + }, + "sectionDirection": { + "enum": [ + "ltr", + "rtl" + ] + }, + "titlePage": { + "type": "boolean" + }, + "verticalAlign": { + "enum": [ + "top", + "center", + "bottom", + "both" + ] + } + }, + "required": [ + "id", + "handle", + "address", + "index", + "range" + ], + "type": "object" + }, + "type": "array" + }, + "page": { + "$ref": "#/$defs/PageInfo" + }, + "total": { + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "evaluatedRevision", + "total", + "items", + "page" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/sections/set-break-type.mdx b/apps/docs/document-api/reference/sections/set-break-type.mdx new file mode 100644 index 0000000000..5353384df6 --- /dev/null +++ b/apps/docs/document-api/reference/sections/set-break-type.mdx @@ -0,0 +1,221 @@ +--- +title: sections.setBreakType +sidebarTitle: sections.setBreakType +description: Set the section break type. +--- + +{/* 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 section break type. + +- Operation ID: `sections.setBreakType` +- API member path: `editor.doc.sections.setBreakType(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a SectionMutationResult receipt; reports NO_OP if the section already has the requested break type. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `breakType` | enum | yes | `"continuous"`, `"nextPage"`, `"evenPage"`, `"oddPage"` | +| `target` | SectionAddress | yes | SectionAddress | + +### Example request + +```json +{ + "breakType": "continuous", + "target": { + "kind": "section", + "sectionId": "example" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "section": { + "kind": "section", + "sectionId": "example" + }, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` +- `INTERNAL_ERROR` + +## Non-applied failure codes + +- `NO_OP` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "breakType": { + "enum": [ + "continuous", + "nextPage", + "evenPage", + "oddPage" + ] + }, + "target": { + "$ref": "#/$defs/SectionAddress" + } + }, + "required": [ + "target", + "breakType" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/sections/set-columns.mdx b/apps/docs/document-api/reference/sections/set-columns.mdx new file mode 100644 index 0000000000..f4b96533a4 --- /dev/null +++ b/apps/docs/document-api/reference/sections/set-columns.mdx @@ -0,0 +1,247 @@ +--- +title: sections.setColumns +sidebarTitle: sections.setColumns +description: Set column configuration for a section. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set column configuration for a section. + +- Operation ID: `sections.setColumns` +- API member path: `editor.doc.sections.setColumns(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a SectionMutationResult receipt; reports NO_OP if column configuration already matches. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `count` | integer | no | | +| `equalWidth` | boolean | no | | +| `gap` | number | no | | +| `target` | SectionAddress | yes | SectionAddress | + +### Example request + +```json +{ + "count": 1, + "equalWidth": true, + "gap": 12.5, + "target": { + "kind": "section", + "sectionId": "example" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "section": { + "kind": "section", + "sectionId": "example" + }, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` +- `INTERNAL_ERROR` + +## Non-applied failure codes + +- `NO_OP` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "target", + "count" + ] + }, + { + "required": [ + "target", + "gap" + ] + }, + { + "required": [ + "target", + "equalWidth" + ] + } + ], + "properties": { + "count": { + "minimum": 1, + "type": "integer" + }, + "equalWidth": { + "type": "boolean" + }, + "gap": { + "minimum": 0, + "type": "number" + }, + "target": { + "$ref": "#/$defs/SectionAddress" + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/sections/set-header-footer-margins.mdx b/apps/docs/document-api/reference/sections/set-header-footer-margins.mdx new file mode 100644 index 0000000000..6debe57bab --- /dev/null +++ b/apps/docs/document-api/reference/sections/set-header-footer-margins.mdx @@ -0,0 +1,236 @@ +--- +title: sections.setHeaderFooterMargins +sidebarTitle: sections.setHeaderFooterMargins +description: Set header/footer margin distances for a section. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set header/footer margin distances for a section. + +- Operation ID: `sections.setHeaderFooterMargins` +- API member path: `editor.doc.sections.setHeaderFooterMargins(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a SectionMutationResult receipt; reports NO_OP if header/footer margins already match the requested values. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `footer` | number | no | | +| `header` | number | no | | +| `target` | SectionAddress | yes | SectionAddress | + +### Example request + +```json +{ + "footer": 12.5, + "header": 12.5, + "target": { + "kind": "section", + "sectionId": "example" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "section": { + "kind": "section", + "sectionId": "example" + }, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` +- `INTERNAL_ERROR` + +## Non-applied failure codes + +- `NO_OP` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "target", + "header" + ] + }, + { + "required": [ + "target", + "footer" + ] + } + ], + "properties": { + "footer": { + "minimum": 0, + "type": "number" + }, + "header": { + "minimum": 0, + "type": "number" + }, + "target": { + "$ref": "#/$defs/SectionAddress" + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/sections/set-header-footer-ref.mdx b/apps/docs/document-api/reference/sections/set-header-footer-ref.mdx new file mode 100644 index 0000000000..94b5fb637d --- /dev/null +++ b/apps/docs/document-api/reference/sections/set-header-footer-ref.mdx @@ -0,0 +1,236 @@ +--- +title: sections.setHeaderFooterRef +sidebarTitle: sections.setHeaderFooterRef +description: Set or replace a section header/footer reference for a variant. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set or replace a section header/footer reference for a variant. + +- Operation ID: `sections.setHeaderFooterRef` +- API member path: `editor.doc.sections.setHeaderFooterRef(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a SectionMutationResult receipt; reports NO_OP if the header/footer reference already matches. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `kind` | enum | yes | `"header"`, `"footer"` | +| `refId` | string | yes | | +| `target` | SectionAddress | yes | SectionAddress | +| `variant` | enum | yes | `"default"`, `"first"`, `"even"` | + +### Example request + +```json +{ + "kind": "header", + "refId": "example", + "target": { + "kind": "section", + "sectionId": "example" + }, + "variant": "default" +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "section": { + "kind": "section", + "sectionId": "example" + }, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` +- `INTERNAL_ERROR` + +## Non-applied failure codes + +- `NO_OP` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "kind": { + "enum": [ + "header", + "footer" + ] + }, + "refId": { + "minLength": 1, + "type": "string" + }, + "target": { + "$ref": "#/$defs/SectionAddress" + }, + "variant": { + "enum": [ + "default", + "first", + "even" + ] + } + }, + "required": [ + "target", + "kind", + "variant", + "refId" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/sections/set-line-numbering.mdx b/apps/docs/document-api/reference/sections/set-line-numbering.mdx new file mode 100644 index 0000000000..feff0247f4 --- /dev/null +++ b/apps/docs/document-api/reference/sections/set-line-numbering.mdx @@ -0,0 +1,241 @@ +--- +title: sections.setLineNumbering +sidebarTitle: sections.setLineNumbering +description: Enable or configure line numbering for a section. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Enable or configure line numbering for a section. + +- Operation ID: `sections.setLineNumbering` +- API member path: `editor.doc.sections.setLineNumbering(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a SectionMutationResult receipt; reports NO_OP if line numbering settings already match. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `countBy` | integer | no | | +| `distance` | number | no | | +| `enabled` | boolean | yes | | +| `restart` | enum | no | `"continuous"`, `"newPage"`, `"newSection"` | +| `start` | integer | no | | +| `target` | SectionAddress | yes | SectionAddress | + +### Example request + +```json +{ + "countBy": 1, + "enabled": true, + "start": 1, + "target": { + "kind": "section", + "sectionId": "example" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "section": { + "kind": "section", + "sectionId": "example" + }, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` +- `INTERNAL_ERROR` + +## Non-applied failure codes + +- `NO_OP` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "countBy": { + "minimum": 1, + "type": "integer" + }, + "distance": { + "minimum": 0, + "type": "number" + }, + "enabled": { + "type": "boolean" + }, + "restart": { + "enum": [ + "continuous", + "newPage", + "newSection" + ] + }, + "start": { + "minimum": 1, + "type": "integer" + }, + "target": { + "$ref": "#/$defs/SectionAddress" + } + }, + "required": [ + "target", + "enabled" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/sections/set-link-to-previous.mdx b/apps/docs/document-api/reference/sections/set-link-to-previous.mdx new file mode 100644 index 0000000000..2e758c917b --- /dev/null +++ b/apps/docs/document-api/reference/sections/set-link-to-previous.mdx @@ -0,0 +1,235 @@ +--- +title: sections.setLinkToPrevious +sidebarTitle: sections.setLinkToPrevious +description: Set or clear link-to-previous behavior for a header/footer variant. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set or clear link-to-previous behavior for a header/footer variant. + +- Operation ID: `sections.setLinkToPrevious` +- API member path: `editor.doc.sections.setLinkToPrevious(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a SectionMutationResult receipt; reports NO_OP if link-to-previous already matches the requested value. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `kind` | enum | yes | `"header"`, `"footer"` | +| `linked` | boolean | yes | | +| `target` | SectionAddress | yes | SectionAddress | +| `variant` | enum | yes | `"default"`, `"first"`, `"even"` | + +### Example request + +```json +{ + "kind": "header", + "linked": true, + "target": { + "kind": "section", + "sectionId": "example" + }, + "variant": "default" +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "section": { + "kind": "section", + "sectionId": "example" + }, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` +- `INTERNAL_ERROR` + +## Non-applied failure codes + +- `NO_OP` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "kind": { + "enum": [ + "header", + "footer" + ] + }, + "linked": { + "type": "boolean" + }, + "target": { + "$ref": "#/$defs/SectionAddress" + }, + "variant": { + "enum": [ + "default", + "first", + "even" + ] + } + }, + "required": [ + "target", + "kind", + "variant", + "linked" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/sections/set-odd-even-headers-footers.mdx b/apps/docs/document-api/reference/sections/set-odd-even-headers-footers.mdx new file mode 100644 index 0000000000..65f8818e9a --- /dev/null +++ b/apps/docs/document-api/reference/sections/set-odd-even-headers-footers.mdx @@ -0,0 +1,190 @@ +--- +title: sections.setOddEvenHeadersFooters +sidebarTitle: sections.setOddEvenHeadersFooters +description: Enable or disable odd/even header-footer mode in document settings. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Enable or disable odd/even header-footer mode in document settings. + +- Operation ID: `sections.setOddEvenHeadersFooters` +- API member path: `editor.doc.sections.setOddEvenHeadersFooters(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a DocumentMutationResult receipt; reports NO_OP if the odd/even setting already matches. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `enabled` | boolean | yes | | + +### Example request + +```json +{ + "enabled": true +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "success": true +} +``` + +## Pre-apply throws + +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` +- `INTERNAL_ERROR` + +## Non-applied failure codes + +- `NO_OP` +- `CAPABILITY_UNAVAILABLE` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "success": { + "const": true + } + }, + "required": [ + "success" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "success": { + "const": true + } + }, + "required": [ + "success" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/sections/set-page-borders.mdx b/apps/docs/document-api/reference/sections/set-page-borders.mdx new file mode 100644 index 0000000000..56ef54fb61 --- /dev/null +++ b/apps/docs/document-api/reference/sections/set-page-borders.mdx @@ -0,0 +1,511 @@ +--- +title: sections.setPageBorders +sidebarTitle: sections.setPageBorders +description: Set page border configuration for a section. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set page border configuration for a section. + +- Operation ID: `sections.setPageBorders` +- API member path: `editor.doc.sections.setPageBorders(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a SectionMutationResult receipt; reports NO_OP if page border configuration already matches. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `borders` | any \\| any \\| any \\| any \\| any \\| any \\| any | yes | One of: any, any, any, any, any, any, any | +| `target` | SectionAddress | yes | SectionAddress | + +### Example request + +```json +{ + "borders": { + "display": "allPages", + "offsetFrom": "page", + "zOrder": "front" + }, + "target": { + "kind": "section", + "sectionId": "example" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "section": { + "kind": "section", + "sectionId": "example" + }, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` +- `INTERNAL_ERROR` + +## Non-applied failure codes + +- `NO_OP` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "borders": { + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "display" + ] + }, + { + "required": [ + "offsetFrom" + ] + }, + { + "required": [ + "zOrder" + ] + }, + { + "required": [ + "top" + ] + }, + { + "required": [ + "right" + ] + }, + { + "required": [ + "bottom" + ] + }, + { + "required": [ + "left" + ] + } + ], + "properties": { + "bottom": { + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "style" + ] + }, + { + "required": [ + "size" + ] + }, + { + "required": [ + "space" + ] + }, + { + "required": [ + "color" + ] + }, + { + "required": [ + "shadow" + ] + }, + { + "required": [ + "frame" + ] + } + ], + "properties": { + "color": { + "type": "string" + }, + "frame": { + "type": "boolean" + }, + "shadow": { + "type": "boolean" + }, + "size": { + "minimum": 0, + "type": "number" + }, + "space": { + "minimum": 0, + "type": "number" + }, + "style": { + "type": "string" + } + }, + "type": "object" + }, + "display": { + "enum": [ + "allPages", + "firstPage", + "notFirstPage" + ] + }, + "left": { + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "style" + ] + }, + { + "required": [ + "size" + ] + }, + { + "required": [ + "space" + ] + }, + { + "required": [ + "color" + ] + }, + { + "required": [ + "shadow" + ] + }, + { + "required": [ + "frame" + ] + } + ], + "properties": { + "color": { + "type": "string" + }, + "frame": { + "type": "boolean" + }, + "shadow": { + "type": "boolean" + }, + "size": { + "minimum": 0, + "type": "number" + }, + "space": { + "minimum": 0, + "type": "number" + }, + "style": { + "type": "string" + } + }, + "type": "object" + }, + "offsetFrom": { + "enum": [ + "page", + "text" + ] + }, + "right": { + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "style" + ] + }, + { + "required": [ + "size" + ] + }, + { + "required": [ + "space" + ] + }, + { + "required": [ + "color" + ] + }, + { + "required": [ + "shadow" + ] + }, + { + "required": [ + "frame" + ] + } + ], + "properties": { + "color": { + "type": "string" + }, + "frame": { + "type": "boolean" + }, + "shadow": { + "type": "boolean" + }, + "size": { + "minimum": 0, + "type": "number" + }, + "space": { + "minimum": 0, + "type": "number" + }, + "style": { + "type": "string" + } + }, + "type": "object" + }, + "top": { + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "style" + ] + }, + { + "required": [ + "size" + ] + }, + { + "required": [ + "space" + ] + }, + { + "required": [ + "color" + ] + }, + { + "required": [ + "shadow" + ] + }, + { + "required": [ + "frame" + ] + } + ], + "properties": { + "color": { + "type": "string" + }, + "frame": { + "type": "boolean" + }, + "shadow": { + "type": "boolean" + }, + "size": { + "minimum": 0, + "type": "number" + }, + "space": { + "minimum": 0, + "type": "number" + }, + "style": { + "type": "string" + } + }, + "type": "object" + }, + "zOrder": { + "enum": [ + "front", + "back" + ] + } + }, + "type": "object" + }, + "target": { + "$ref": "#/$defs/SectionAddress" + } + }, + "required": [ + "target", + "borders" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/sections/set-page-margins.mdx b/apps/docs/document-api/reference/sections/set-page-margins.mdx new file mode 100644 index 0000000000..9ea21b25ef --- /dev/null +++ b/apps/docs/document-api/reference/sections/set-page-margins.mdx @@ -0,0 +1,270 @@ +--- +title: sections.setPageMargins +sidebarTitle: sections.setPageMargins +description: Set page-edge margins for a section. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set page-edge margins for a section. + +- Operation ID: `sections.setPageMargins` +- API member path: `editor.doc.sections.setPageMargins(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a SectionMutationResult receipt; reports NO_OP if margins already match the requested values. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `bottom` | number | no | | +| `gutter` | number | no | | +| `left` | number | no | | +| `right` | number | no | | +| `target` | SectionAddress | yes | SectionAddress | +| `top` | number | no | | + +### Example request + +```json +{ + "bottom": 12.5, + "right": 12.5, + "target": { + "kind": "section", + "sectionId": "example" + }, + "top": 12.5 +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "section": { + "kind": "section", + "sectionId": "example" + }, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` +- `INTERNAL_ERROR` + +## Non-applied failure codes + +- `NO_OP` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "target", + "top" + ] + }, + { + "required": [ + "target", + "right" + ] + }, + { + "required": [ + "target", + "bottom" + ] + }, + { + "required": [ + "target", + "left" + ] + }, + { + "required": [ + "target", + "gutter" + ] + } + ], + "properties": { + "bottom": { + "minimum": 0, + "type": "number" + }, + "gutter": { + "minimum": 0, + "type": "number" + }, + "left": { + "minimum": 0, + "type": "number" + }, + "right": { + "minimum": 0, + "type": "number" + }, + "target": { + "$ref": "#/$defs/SectionAddress" + }, + "top": { + "minimum": 0, + "type": "number" + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/sections/set-page-numbering.mdx b/apps/docs/document-api/reference/sections/set-page-numbering.mdx new file mode 100644 index 0000000000..8bfef8a00d --- /dev/null +++ b/apps/docs/document-api/reference/sections/set-page-numbering.mdx @@ -0,0 +1,242 @@ +--- +title: sections.setPageNumbering +sidebarTitle: sections.setPageNumbering +description: Set page numbering format/start for a section. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set page numbering format/start for a section. + +- Operation ID: `sections.setPageNumbering` +- API member path: `editor.doc.sections.setPageNumbering(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a SectionMutationResult receipt; reports NO_OP if page numbering format already matches. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `format` | enum | no | `"decimal"`, `"lowerLetter"`, `"upperLetter"`, `"lowerRoman"`, `"upperRoman"`, `"numberInDash"` | +| `start` | integer | no | | +| `target` | SectionAddress | yes | SectionAddress | + +### Example request + +```json +{ + "format": "decimal", + "start": 1, + "target": { + "kind": "section", + "sectionId": "example" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "section": { + "kind": "section", + "sectionId": "example" + }, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` +- `INTERNAL_ERROR` + +## Non-applied failure codes + +- `NO_OP` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "target", + "start" + ] + }, + { + "required": [ + "target", + "format" + ] + } + ], + "properties": { + "format": { + "enum": [ + "decimal", + "lowerLetter", + "upperLetter", + "lowerRoman", + "upperRoman", + "numberInDash" + ] + }, + "start": { + "minimum": 1, + "type": "integer" + }, + "target": { + "$ref": "#/$defs/SectionAddress" + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/sections/set-page-setup.mdx b/apps/docs/document-api/reference/sections/set-page-setup.mdx new file mode 100644 index 0000000000..eec42bc8c0 --- /dev/null +++ b/apps/docs/document-api/reference/sections/set-page-setup.mdx @@ -0,0 +1,261 @@ +--- +title: sections.setPageSetup +sidebarTitle: sections.setPageSetup +description: Set page size/orientation properties for a section. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set page size/orientation properties for a section. + +- Operation ID: `sections.setPageSetup` +- API member path: `editor.doc.sections.setPageSetup(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a SectionMutationResult receipt; reports NO_OP if page size and orientation already match the requested values. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `height` | number | no | | +| `orientation` | enum | no | `"portrait"`, `"landscape"` | +| `paperSize` | string | no | | +| `target` | SectionAddress | yes | SectionAddress | +| `width` | number | no | | + +### Example request + +```json +{ + "height": 12.5, + "orientation": "portrait", + "target": { + "kind": "section", + "sectionId": "example" + }, + "width": 12.5 +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "section": { + "kind": "section", + "sectionId": "example" + }, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` +- `INTERNAL_ERROR` + +## Non-applied failure codes + +- `NO_OP` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "target", + "width" + ] + }, + { + "required": [ + "target", + "height" + ] + }, + { + "required": [ + "target", + "orientation" + ] + }, + { + "required": [ + "target", + "paperSize" + ] + } + ], + "properties": { + "height": { + "minimum": 0, + "type": "number" + }, + "orientation": { + "enum": [ + "portrait", + "landscape" + ] + }, + "paperSize": { + "minLength": 1, + "type": "string" + }, + "target": { + "$ref": "#/$defs/SectionAddress" + }, + "width": { + "minimum": 0, + "type": "number" + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/sections/set-section-direction.mdx b/apps/docs/document-api/reference/sections/set-section-direction.mdx new file mode 100644 index 0000000000..c4e88f649f --- /dev/null +++ b/apps/docs/document-api/reference/sections/set-section-direction.mdx @@ -0,0 +1,219 @@ +--- +title: sections.setSectionDirection +sidebarTitle: sections.setSectionDirection +description: Set section text flow direction (LTR/RTL). +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set section text flow direction (LTR/RTL). + +- Operation ID: `sections.setSectionDirection` +- API member path: `editor.doc.sections.setSectionDirection(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a SectionMutationResult receipt; reports NO_OP if text direction already matches. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `direction` | enum | yes | `"ltr"`, `"rtl"` | +| `target` | SectionAddress | yes | SectionAddress | + +### Example request + +```json +{ + "direction": "ltr", + "target": { + "kind": "section", + "sectionId": "example" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "section": { + "kind": "section", + "sectionId": "example" + }, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` +- `INTERNAL_ERROR` + +## Non-applied failure codes + +- `NO_OP` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "direction": { + "enum": [ + "ltr", + "rtl" + ] + }, + "target": { + "$ref": "#/$defs/SectionAddress" + } + }, + "required": [ + "target", + "direction" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/sections/set-title-page.mdx b/apps/docs/document-api/reference/sections/set-title-page.mdx new file mode 100644 index 0000000000..d4e8af8e98 --- /dev/null +++ b/apps/docs/document-api/reference/sections/set-title-page.mdx @@ -0,0 +1,216 @@ +--- +title: sections.setTitlePage +sidebarTitle: sections.setTitlePage +description: Enable or disable title-page behavior for a section. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Enable or disable title-page behavior for a section. + +- Operation ID: `sections.setTitlePage` +- API member path: `editor.doc.sections.setTitlePage(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a SectionMutationResult receipt; reports NO_OP if the title-page setting already matches. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `enabled` | boolean | yes | | +| `target` | SectionAddress | yes | SectionAddress | + +### Example request + +```json +{ + "enabled": true, + "target": { + "kind": "section", + "sectionId": "example" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "section": { + "kind": "section", + "sectionId": "example" + }, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` +- `INTERNAL_ERROR` + +## Non-applied failure codes + +- `NO_OP` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "target": { + "$ref": "#/$defs/SectionAddress" + } + }, + "required": [ + "target", + "enabled" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/sections/set-vertical-align.mdx b/apps/docs/document-api/reference/sections/set-vertical-align.mdx new file mode 100644 index 0000000000..f34d6b22a0 --- /dev/null +++ b/apps/docs/document-api/reference/sections/set-vertical-align.mdx @@ -0,0 +1,221 @@ +--- +title: sections.setVerticalAlign +sidebarTitle: sections.setVerticalAlign +description: Set vertical page alignment for a section. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set vertical page alignment for a section. + +- Operation ID: `sections.setVerticalAlign` +- API member path: `editor.doc.sections.setVerticalAlign(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a SectionMutationResult receipt; reports NO_OP if vertical alignment already matches. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | SectionAddress | yes | SectionAddress | +| `value` | enum | yes | `"top"`, `"center"`, `"bottom"`, `"both"` | + +### Example request + +```json +{ + "target": { + "kind": "section", + "sectionId": "example" + }, + "value": "top" +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "section": { + "kind": "section", + "sectionId": "example" + }, + "success": true +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` +- `INTERNAL_ERROR` + +## Non-applied failure codes + +- `NO_OP` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/SectionAddress" + }, + "value": { + "enum": [ + "top", + "center", + "bottom", + "both" + ] + } + }, + "required": [ + "target", + "value" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "section": { + "$ref": "#/$defs/SectionAddress" + }, + "success": { + "const": true + } + }, + "required": [ + "success", + "section" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/styles/apply.mdx b/apps/docs/document-api/reference/styles/apply.mdx index 8efef68a3c..beb5770eef 100644 --- a/apps/docs/document-api/reference/styles/apply.mdx +++ b/apps/docs/document-api/reference/styles/apply.mdx @@ -1,7 +1,7 @@ --- title: styles.apply sidebarTitle: styles.apply -description: Reference for styles.apply +description: Apply document-level default style changes to the stylesheet (word/styles.xml). Targets docDefaults run properties with boolean patch semantics. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for styles.apply ## Summary +Apply document-level default style changes to the stylesheet (word/styles.xml). Targets docDefaults run properties with boolean patch semantics. + - Operation ID: `styles.apply` - API member path: `editor.doc.styles.apply(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for styles.apply - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a StylesApplyReceipt with per-channel success/failure details for each property change. + ## Input fields _No fields._ diff --git a/apps/docs/document-api/reference/tables/apply-border-preset.mdx b/apps/docs/document-api/reference/tables/apply-border-preset.mdx index 549ecb38e1..4cb644806b 100644 --- a/apps/docs/document-api/reference/tables/apply-border-preset.mdx +++ b/apps/docs/document-api/reference/tables/apply-border-preset.mdx @@ -1,7 +1,7 @@ --- title: tables.applyBorderPreset sidebarTitle: tables.applyBorderPreset -description: Reference for tables.applyBorderPreset +description: Apply a border preset (e.g. all borders, outside only) to a table. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.applyBorderPreset ## Summary +Apply a border preset (e.g. all borders, outside only) to a table. + - Operation ID: `tables.applyBorderPreset` - API member path: `editor.doc.tables.applyBorderPreset(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.applyBorderPreset - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if the preset is already applied. + ## Input fields | Field | Type | Required | Description | @@ -29,7 +35,15 @@ description: Reference for tables.applyBorderPreset ### Example request ```json -{} +{ + "nodeId": "node-def456", + "preset": "box", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/clear-border.mdx b/apps/docs/document-api/reference/tables/clear-border.mdx index 38a32af5b1..71a1300cb1 100644 --- a/apps/docs/document-api/reference/tables/clear-border.mdx +++ b/apps/docs/document-api/reference/tables/clear-border.mdx @@ -1,7 +1,7 @@ --- title: tables.clearBorder sidebarTitle: tables.clearBorder -description: Reference for tables.clearBorder +description: Remove border formatting from a table or cell range. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.clearBorder ## Summary +Remove border formatting from a table or cell range. + - Operation ID: `tables.clearBorder` - API member path: `editor.doc.tables.clearBorder(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.clearBorder - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if no borders are set. + ## Input fields | Field | Type | Required | Description | @@ -29,7 +35,15 @@ description: Reference for tables.clearBorder ### Example request ```json -{} +{ + "edge": "top", + "nodeId": "node-def456", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/clear-cell-spacing.mdx b/apps/docs/document-api/reference/tables/clear-cell-spacing.mdx index ad105f264c..d4b458aea2 100644 --- a/apps/docs/document-api/reference/tables/clear-cell-spacing.mdx +++ b/apps/docs/document-api/reference/tables/clear-cell-spacing.mdx @@ -1,7 +1,7 @@ --- title: tables.clearCellSpacing sidebarTitle: tables.clearCellSpacing -description: Reference for tables.clearCellSpacing +description: Remove custom cell spacing from the target table. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.clearCellSpacing ## Summary +Remove custom cell spacing from the target table. + - Operation ID: `tables.clearCellSpacing` - API member path: `editor.doc.tables.clearCellSpacing(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.clearCellSpacing - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if no custom cell spacing is set. + ## Input fields | Field | Type | Required | Description | @@ -28,7 +34,14 @@ description: Reference for tables.clearCellSpacing ### Example request ```json -{} +{ + "nodeId": "node-def456", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/clear-contents.mdx b/apps/docs/document-api/reference/tables/clear-contents.mdx index 2f424b4566..b5f77ca6b4 100644 --- a/apps/docs/document-api/reference/tables/clear-contents.mdx +++ b/apps/docs/document-api/reference/tables/clear-contents.mdx @@ -1,7 +1,7 @@ --- title: tables.clearContents sidebarTitle: tables.clearContents -description: Reference for tables.clearContents +description: Clear the contents of the target table or cell range. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.clearContents ## Summary +Clear the contents of the target table or cell range. + - Operation ID: `tables.clearContents` - API member path: `editor.doc.tables.clearContents(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.clearContents - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if the target cells are already empty. + ## Input fields | Field | Type | Required | Description | @@ -28,7 +34,14 @@ description: Reference for tables.clearContents ### Example request ```json -{} +{ + "nodeId": "node-def456", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/clear-shading.mdx b/apps/docs/document-api/reference/tables/clear-shading.mdx index 4e42eb0584..cd747fceb7 100644 --- a/apps/docs/document-api/reference/tables/clear-shading.mdx +++ b/apps/docs/document-api/reference/tables/clear-shading.mdx @@ -1,7 +1,7 @@ --- title: tables.clearShading sidebarTitle: tables.clearShading -description: Reference for tables.clearShading +description: Remove shading from a table or cell range. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.clearShading ## Summary +Remove shading from a table or cell range. + - Operation ID: `tables.clearShading` - API member path: `editor.doc.tables.clearShading(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.clearShading - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if no shading is set. + ## Input fields | Field | Type | Required | Description | @@ -28,7 +34,14 @@ description: Reference for tables.clearShading ### Example request ```json -{} +{ + "nodeId": "node-def456", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/clear-style.mdx b/apps/docs/document-api/reference/tables/clear-style.mdx index 1db3c9473a..348fec9384 100644 --- a/apps/docs/document-api/reference/tables/clear-style.mdx +++ b/apps/docs/document-api/reference/tables/clear-style.mdx @@ -1,7 +1,7 @@ --- title: tables.clearStyle sidebarTitle: tables.clearStyle -description: Reference for tables.clearStyle +description: Remove the applied table style, reverting to defaults. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.clearStyle ## Summary +Remove the applied table style, reverting to defaults. + - Operation ID: `tables.clearStyle` - API member path: `editor.doc.tables.clearStyle(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.clearStyle - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if no table style is applied. + ## Input fields | Field | Type | Required | Description | @@ -28,7 +34,14 @@ description: Reference for tables.clearStyle ### Example request ```json -{} +{ + "nodeId": "node-def456", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/convert-from-text.mdx b/apps/docs/document-api/reference/tables/convert-from-text.mdx index c8fa3a5bf3..ceababdac8 100644 --- a/apps/docs/document-api/reference/tables/convert-from-text.mdx +++ b/apps/docs/document-api/reference/tables/convert-from-text.mdx @@ -1,7 +1,7 @@ --- title: tables.convertFromText sidebarTitle: tables.convertFromText -description: Reference for tables.convertFromText +description: Convert a text range into a table. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.convertFromText ## Summary +Convert a text range into a table. + - Operation ID: `tables.convertFromText` - API member path: `editor.doc.tables.convertFromText(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.convertFromText - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt confirming text was converted into a table. + ## Input fields | Field | Type | Required | Description | @@ -31,7 +37,15 @@ description: Reference for tables.convertFromText ### Example request ```json -{} +{ + "delimiter": "tab", + "nodeId": "node-def456", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/convert-to-text.mdx b/apps/docs/document-api/reference/tables/convert-to-text.mdx index 61e0221edc..895f0e0f64 100644 --- a/apps/docs/document-api/reference/tables/convert-to-text.mdx +++ b/apps/docs/document-api/reference/tables/convert-to-text.mdx @@ -1,7 +1,7 @@ --- title: tables.convertToText sidebarTitle: tables.convertToText -description: Reference for tables.convertToText +description: Convert a table back to plain text. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.convertToText ## Summary +Convert a table back to plain text. + - Operation ID: `tables.convertToText` - API member path: `editor.doc.tables.convertToText(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.convertToText - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if the table has no content to convert. + ## Input fields | Field | Type | Required | Description | @@ -29,7 +35,15 @@ description: Reference for tables.convertToText ### Example request ```json -{} +{ + "delimiter": "tab", + "nodeId": "node-def456", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/delete-cell.mdx b/apps/docs/document-api/reference/tables/delete-cell.mdx index 7ecd770743..c506cec712 100644 --- a/apps/docs/document-api/reference/tables/delete-cell.mdx +++ b/apps/docs/document-api/reference/tables/delete-cell.mdx @@ -1,7 +1,7 @@ --- title: tables.deleteCell sidebarTitle: tables.deleteCell -description: Reference for tables.deleteCell +description: Delete a cell from a table row. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.deleteCell ## Summary +Delete a cell from a table row. + - Operation ID: `tables.deleteCell` - API member path: `editor.doc.tables.deleteCell(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.deleteCell - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if the target cell does not exist. + ## Input fields | Field | Type | Required | Description | @@ -29,7 +35,15 @@ description: Reference for tables.deleteCell ### Example request ```json -{} +{ + "mode": "shiftLeft", + "nodeId": "node-def456", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/delete-column.mdx b/apps/docs/document-api/reference/tables/delete-column.mdx index 85242db9f7..1180d740af 100644 --- a/apps/docs/document-api/reference/tables/delete-column.mdx +++ b/apps/docs/document-api/reference/tables/delete-column.mdx @@ -1,7 +1,7 @@ --- title: tables.deleteColumn sidebarTitle: tables.deleteColumn -description: Reference for tables.deleteColumn +description: Delete a column from the target table. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.deleteColumn ## Summary +Delete a column from the target table. + - Operation ID: `tables.deleteColumn` - API member path: `editor.doc.tables.deleteColumn(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.deleteColumn - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if the target column does not exist. + ## Input fields | Field | Type | Required | Description | @@ -29,7 +35,15 @@ description: Reference for tables.deleteColumn ### Example request ```json -{} +{ + "columnIndex": 1, + "tableNodeId": "example", + "tableTarget": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/delete-row.mdx b/apps/docs/document-api/reference/tables/delete-row.mdx index 499cd99350..1d04856d66 100644 --- a/apps/docs/document-api/reference/tables/delete-row.mdx +++ b/apps/docs/document-api/reference/tables/delete-row.mdx @@ -1,7 +1,7 @@ --- title: tables.deleteRow sidebarTitle: tables.deleteRow -description: Reference for tables.deleteRow +description: Delete a row from the target table. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.deleteRow ## Summary +Delete a row from the target table. + - Operation ID: `tables.deleteRow` - API member path: `editor.doc.tables.deleteRow(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.deleteRow - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if the target row does not exist. + ## Input fields | Field | Type | Required | Description | @@ -31,7 +37,19 @@ description: Reference for tables.deleteRow ### Example request ```json -{} +{ + "nodeId": "node-def456", + "tableTarget": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + }, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/delete.mdx b/apps/docs/document-api/reference/tables/delete.mdx index 6dca1a4725..80ab1cd19b 100644 --- a/apps/docs/document-api/reference/tables/delete.mdx +++ b/apps/docs/document-api/reference/tables/delete.mdx @@ -1,7 +1,7 @@ --- title: tables.delete sidebarTitle: tables.delete -description: Reference for tables.delete +description: Delete the target table from the document. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.delete ## Summary +Delete the target table from the document. + - Operation ID: `tables.delete` - API member path: `editor.doc.tables.delete(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.delete - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if the table was already removed. + ## Input fields | Field | Type | Required | Description | @@ -28,7 +34,14 @@ description: Reference for tables.delete ### Example request ```json -{} +{ + "nodeId": "node-def456", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/distribute-columns.mdx b/apps/docs/document-api/reference/tables/distribute-columns.mdx index a69abd1ee7..efa3517c24 100644 --- a/apps/docs/document-api/reference/tables/distribute-columns.mdx +++ b/apps/docs/document-api/reference/tables/distribute-columns.mdx @@ -1,7 +1,7 @@ --- title: tables.distributeColumns sidebarTitle: tables.distributeColumns -description: Reference for tables.distributeColumns +description: Distribute column widths evenly across the target table. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.distributeColumns ## Summary +Distribute column widths evenly across the target table. + - Operation ID: `tables.distributeColumns` - API member path: `editor.doc.tables.distributeColumns(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.distributeColumns - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if column widths are already equal. + ## Input fields | Field | Type | Required | Description | @@ -29,7 +35,18 @@ description: Reference for tables.distributeColumns ### Example request ```json -{} +{ + "columnRange": { + "end": 10, + "start": 0 + }, + "nodeId": "node-def456", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/distribute-rows.mdx b/apps/docs/document-api/reference/tables/distribute-rows.mdx index b1e05ad136..4a940ed512 100644 --- a/apps/docs/document-api/reference/tables/distribute-rows.mdx +++ b/apps/docs/document-api/reference/tables/distribute-rows.mdx @@ -1,7 +1,7 @@ --- title: tables.distributeRows sidebarTitle: tables.distributeRows -description: Reference for tables.distributeRows +description: Distribute row heights evenly across the target table. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.distributeRows ## Summary +Distribute row heights evenly across the target table. + - Operation ID: `tables.distributeRows` - API member path: `editor.doc.tables.distributeRows(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.distributeRows - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if row heights are already equal. + ## Input fields | Field | Type | Required | Description | @@ -28,7 +34,14 @@ description: Reference for tables.distributeRows ### Example request ```json -{} +{ + "nodeId": "node-def456", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/get-cells.mdx b/apps/docs/document-api/reference/tables/get-cells.mdx index 2ab7be5570..561fd525bb 100644 --- a/apps/docs/document-api/reference/tables/get-cells.mdx +++ b/apps/docs/document-api/reference/tables/get-cells.mdx @@ -1,7 +1,7 @@ --- title: tables.getCells sidebarTitle: tables.getCells -description: Reference for tables.getCells +description: Retrieve cell information for a table, optionally filtered by row or column. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.getCells ## Summary +Retrieve cell information for a table, optionally filtered by row or column. + - Operation ID: `tables.getCells` - API member path: `editor.doc.tables.getCells(...)` - Mutates document: `no` @@ -18,6 +20,10 @@ description: Reference for tables.getCells - Supports dry run: `no` - Deterministic target resolution: `yes` +## Expected result + +Returns a TablesGetCellsOutput with cell information for the requested rows and columns. + ## Input fields | Field | Type | Required | Description | @@ -30,7 +36,15 @@ description: Reference for tables.getCells ### Example request ```json -{} +{ + "nodeId": "node-def456", + "rowIndex": 1, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/get-properties.mdx b/apps/docs/document-api/reference/tables/get-properties.mdx index 11cf87895d..2b0376def4 100644 --- a/apps/docs/document-api/reference/tables/get-properties.mdx +++ b/apps/docs/document-api/reference/tables/get-properties.mdx @@ -1,7 +1,7 @@ --- title: tables.getProperties sidebarTitle: tables.getProperties -description: Reference for tables.getProperties +description: Retrieve layout and style properties of a table. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.getProperties ## Summary +Retrieve layout and style properties of a table. + - Operation ID: `tables.getProperties` - API member path: `editor.doc.tables.getProperties(...)` - Mutates document: `no` @@ -18,6 +20,10 @@ description: Reference for tables.getProperties - Supports dry run: `no` - Deterministic target resolution: `yes` +## Expected result + +Returns a TablesGetPropertiesOutput with the table layout, style, border, and shading properties. + ## Input fields | Field | Type | Required | Description | @@ -28,7 +34,14 @@ description: Reference for tables.getProperties ### Example request ```json -{} +{ + "nodeId": "node-def456", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/get.mdx b/apps/docs/document-api/reference/tables/get.mdx index d319a7e112..4d40b130de 100644 --- a/apps/docs/document-api/reference/tables/get.mdx +++ b/apps/docs/document-api/reference/tables/get.mdx @@ -1,7 +1,7 @@ --- title: tables.get sidebarTitle: tables.get -description: Reference for tables.get +description: Retrieve table structure and dimensions by locator. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.get ## Summary +Retrieve table structure and dimensions by locator. + - Operation ID: `tables.get` - API member path: `editor.doc.tables.get(...)` - Mutates document: `no` @@ -18,6 +20,10 @@ description: Reference for tables.get - Supports dry run: `no` - Deterministic target resolution: `yes` +## Expected result + +Returns a TablesGetOutput with the table row count, column count, and structural metadata. + ## Input fields | Field | Type | Required | Description | @@ -28,7 +34,14 @@ description: Reference for tables.get ### Example request ```json -{} +{ + "nodeId": "node-def456", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/insert-cell.mdx b/apps/docs/document-api/reference/tables/insert-cell.mdx index 5e41d29a79..f4e32dcab0 100644 --- a/apps/docs/document-api/reference/tables/insert-cell.mdx +++ b/apps/docs/document-api/reference/tables/insert-cell.mdx @@ -1,7 +1,7 @@ --- title: tables.insertCell sidebarTitle: tables.insertCell -description: Reference for tables.insertCell +description: Insert a new cell into a table row. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.insertCell ## Summary +Insert a new cell into a table row. + - Operation ID: `tables.insertCell` - API member path: `editor.doc.tables.insertCell(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.insertCell - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt confirming a cell was inserted. + ## Input fields | Field | Type | Required | Description | @@ -29,7 +35,15 @@ description: Reference for tables.insertCell ### Example request ```json -{} +{ + "mode": "shiftRight", + "nodeId": "node-def456", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/insert-column.mdx b/apps/docs/document-api/reference/tables/insert-column.mdx index d2323925c9..84fe6ad5cb 100644 --- a/apps/docs/document-api/reference/tables/insert-column.mdx +++ b/apps/docs/document-api/reference/tables/insert-column.mdx @@ -1,7 +1,7 @@ --- title: tables.insertColumn sidebarTitle: tables.insertColumn -description: Reference for tables.insertColumn +description: Insert a new column into the target table. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.insertColumn ## Summary +Insert a new column into the target table. + - Operation ID: `tables.insertColumn` - API member path: `editor.doc.tables.insertColumn(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.insertColumn - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt confirming a column was inserted. + ## Input fields | Field | Type | Required | Description | @@ -31,7 +37,17 @@ description: Reference for tables.insertColumn ### Example request ```json -{} +{ + "columnIndex": 1, + "count": 1, + "position": "left", + "tableNodeId": "example", + "tableTarget": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/insert-row.mdx b/apps/docs/document-api/reference/tables/insert-row.mdx index a5672dbdf8..f304b3c8d0 100644 --- a/apps/docs/document-api/reference/tables/insert-row.mdx +++ b/apps/docs/document-api/reference/tables/insert-row.mdx @@ -1,7 +1,7 @@ --- title: tables.insertRow sidebarTitle: tables.insertRow -description: Reference for tables.insertRow +description: Insert a new row into the target table. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.insertRow ## Summary +Insert a new row into the target table. + - Operation ID: `tables.insertRow` - API member path: `editor.doc.tables.insertRow(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.insertRow - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt confirming a row was inserted. + ## Input fields | Field | Type | Required | Description | @@ -33,7 +39,20 @@ description: Reference for tables.insertRow ### Example request ```json -{} +{ + "nodeId": "node-def456", + "position": "above", + "tableTarget": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + }, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/merge-cells.mdx b/apps/docs/document-api/reference/tables/merge-cells.mdx index 5876f2774d..c600f5e765 100644 --- a/apps/docs/document-api/reference/tables/merge-cells.mdx +++ b/apps/docs/document-api/reference/tables/merge-cells.mdx @@ -1,7 +1,7 @@ --- title: tables.mergeCells sidebarTitle: tables.mergeCells -description: Reference for tables.mergeCells +description: Merge a range of table cells into one. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.mergeCells ## Summary +Merge a range of table cells into one. + - Operation ID: `tables.mergeCells` - API member path: `editor.doc.tables.mergeCells(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.mergeCells - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if the cells are already merged. + ## Input fields | Field | Type | Required | Description | @@ -30,7 +36,22 @@ description: Reference for tables.mergeCells ### Example request ```json -{} +{ + "end": { + "columnIndex": 1, + "rowIndex": 1 + }, + "start": { + "columnIndex": 1, + "rowIndex": 1 + }, + "tableNodeId": "example", + "tableTarget": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/move.mdx b/apps/docs/document-api/reference/tables/move.mdx index 35e422f758..982e8dd354 100644 --- a/apps/docs/document-api/reference/tables/move.mdx +++ b/apps/docs/document-api/reference/tables/move.mdx @@ -1,7 +1,7 @@ --- title: tables.move sidebarTitle: tables.move -description: Reference for tables.move +description: Move a table to a new position in the document. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.move ## Summary +Move a table to a new position in the document. + - Operation ID: `tables.move` - API member path: `editor.doc.tables.move(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.move - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if the table is already at the target position. + ## Input fields | Field | Type | Required | Description | @@ -29,7 +35,17 @@ description: Reference for tables.move ### Example request ```json -{} +{ + "destination": { + "kind": "documentStart" + }, + "nodeId": "node-def456", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/set-alt-text.mdx b/apps/docs/document-api/reference/tables/set-alt-text.mdx index 96675929cd..6c0024f9df 100644 --- a/apps/docs/document-api/reference/tables/set-alt-text.mdx +++ b/apps/docs/document-api/reference/tables/set-alt-text.mdx @@ -1,7 +1,7 @@ --- title: tables.setAltText sidebarTitle: tables.setAltText -description: Reference for tables.setAltText +description: Set the alternative text description for a table. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.setAltText ## Summary +Set the alternative text description for a table. + - Operation ID: `tables.setAltText` - API member path: `editor.doc.tables.setAltText(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.setAltText - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if alt text already matches. + ## Input fields | Field | Type | Required | Description | @@ -30,7 +36,15 @@ description: Reference for tables.setAltText ### Example request ```json -{} +{ + "nodeId": "node-def456", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + }, + "title": "example" +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/set-border.mdx b/apps/docs/document-api/reference/tables/set-border.mdx index 3371f98651..0bdb0f6216 100644 --- a/apps/docs/document-api/reference/tables/set-border.mdx +++ b/apps/docs/document-api/reference/tables/set-border.mdx @@ -1,7 +1,7 @@ --- title: tables.setBorder sidebarTitle: tables.setBorder -description: Reference for tables.setBorder +description: Set border properties on a table or cell range. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.setBorder ## Summary +Set border properties on a table or cell range. + - Operation ID: `tables.setBorder` - API member path: `editor.doc.tables.setBorder(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.setBorder - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if border properties already match. + ## Input fields | Field | Type | Required | Description | @@ -32,7 +38,18 @@ description: Reference for tables.setBorder ### Example request ```json -{} +{ + "color": "example", + "edge": "top", + "lineStyle": "example", + "lineWeightPt": 12.5, + "nodeId": "node-def456", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/set-cell-padding.mdx b/apps/docs/document-api/reference/tables/set-cell-padding.mdx index d5393487be..8d3e7fa104 100644 --- a/apps/docs/document-api/reference/tables/set-cell-padding.mdx +++ b/apps/docs/document-api/reference/tables/set-cell-padding.mdx @@ -1,7 +1,7 @@ --- title: tables.setCellPadding sidebarTitle: tables.setCellPadding -description: Reference for tables.setCellPadding +description: Set padding on a specific table cell or cell range. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.setCellPadding ## Summary +Set padding on a specific table cell or cell range. + - Operation ID: `tables.setCellPadding` - API member path: `editor.doc.tables.setCellPadding(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.setCellPadding - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if cell padding already matches. + ## Input fields | Field | Type | Required | Description | @@ -32,7 +38,18 @@ description: Reference for tables.setCellPadding ### Example request ```json -{} +{ + "bottomPt": 12.5, + "leftPt": 12.5, + "nodeId": "node-def456", + "rightPt": 12.5, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + }, + "topPt": 12.5 +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/set-cell-properties.mdx b/apps/docs/document-api/reference/tables/set-cell-properties.mdx index 00b1e8d1c0..00cb449b6f 100644 --- a/apps/docs/document-api/reference/tables/set-cell-properties.mdx +++ b/apps/docs/document-api/reference/tables/set-cell-properties.mdx @@ -1,7 +1,7 @@ --- title: tables.setCellProperties sidebarTitle: tables.setCellProperties -description: Reference for tables.setCellProperties +description: Set properties on a table cell such as vertical alignment or text direction. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.setCellProperties ## Summary +Set properties on a table cell such as vertical alignment or text direction. + - Operation ID: `tables.setCellProperties` - API member path: `editor.doc.tables.setCellProperties(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.setCellProperties - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if cell properties already match. + ## Input fields | Field | Type | Required | Description | @@ -32,7 +38,15 @@ description: Reference for tables.setCellProperties ### Example request ```json -{} +{ + "nodeId": "node-def456", + "preferredWidthPt": 12.5, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/set-cell-spacing.mdx b/apps/docs/document-api/reference/tables/set-cell-spacing.mdx index d305427d40..95f75a4898 100644 --- a/apps/docs/document-api/reference/tables/set-cell-spacing.mdx +++ b/apps/docs/document-api/reference/tables/set-cell-spacing.mdx @@ -1,7 +1,7 @@ --- title: tables.setCellSpacing sidebarTitle: tables.setCellSpacing -description: Reference for tables.setCellSpacing +description: Set the cell spacing for the target table. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.setCellSpacing ## Summary +Set the cell spacing for the target table. + - Operation ID: `tables.setCellSpacing` - API member path: `editor.doc.tables.setCellSpacing(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.setCellSpacing - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if cell spacing already matches. + ## Input fields | Field | Type | Required | Description | @@ -29,7 +35,15 @@ description: Reference for tables.setCellSpacing ### Example request ```json -{} +{ + "nodeId": "node-def456", + "spacingPt": 12.5, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/set-column-width.mdx b/apps/docs/document-api/reference/tables/set-column-width.mdx index 91a355733e..44378cd0da 100644 --- a/apps/docs/document-api/reference/tables/set-column-width.mdx +++ b/apps/docs/document-api/reference/tables/set-column-width.mdx @@ -1,7 +1,7 @@ --- title: tables.setColumnWidth sidebarTitle: tables.setColumnWidth -description: Reference for tables.setColumnWidth +description: Set the width of a table column. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.setColumnWidth ## Summary +Set the width of a table column. + - Operation ID: `tables.setColumnWidth` - API member path: `editor.doc.tables.setColumnWidth(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.setColumnWidth - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if the column width already matches. + ## Input fields | Field | Type | Required | Description | @@ -30,7 +36,16 @@ description: Reference for tables.setColumnWidth ### Example request ```json -{} +{ + "columnIndex": 1, + "tableNodeId": "example", + "tableTarget": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + }, + "widthPt": 12.5 +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/set-layout.mdx b/apps/docs/document-api/reference/tables/set-layout.mdx index 0647f5ae9e..41aa7b0a9d 100644 --- a/apps/docs/document-api/reference/tables/set-layout.mdx +++ b/apps/docs/document-api/reference/tables/set-layout.mdx @@ -1,7 +1,7 @@ --- title: tables.setLayout sidebarTitle: tables.setLayout -description: Reference for tables.setLayout +description: Set the layout mode of the target table. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.setLayout ## Summary +Set the layout mode of the target table. + - Operation ID: `tables.setLayout` - API member path: `editor.doc.tables.setLayout(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.setLayout - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if the table already uses the requested layout mode. + ## Input fields | Field | Type | Required | Description | @@ -33,7 +39,15 @@ description: Reference for tables.setLayout ### Example request ```json -{} +{ + "nodeId": "node-def456", + "preferredWidth": 12.5, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/set-row-height.mdx b/apps/docs/document-api/reference/tables/set-row-height.mdx index a62eb08fdd..e43be28e50 100644 --- a/apps/docs/document-api/reference/tables/set-row-height.mdx +++ b/apps/docs/document-api/reference/tables/set-row-height.mdx @@ -1,7 +1,7 @@ --- title: tables.setRowHeight sidebarTitle: tables.setRowHeight -description: Reference for tables.setRowHeight +description: Set the height of a table row. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.setRowHeight ## Summary +Set the height of a table row. + - Operation ID: `tables.setRowHeight` - API member path: `editor.doc.tables.setRowHeight(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.setRowHeight - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if the row height already matches. + ## Input fields | Field | Type | Required | Description | @@ -33,7 +39,21 @@ description: Reference for tables.setRowHeight ### Example request ```json -{} +{ + "heightPt": 12.5, + "nodeId": "node-def456", + "rule": "atLeast", + "tableTarget": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + }, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/set-row-options.mdx b/apps/docs/document-api/reference/tables/set-row-options.mdx index b7fcab8258..fb229ea004 100644 --- a/apps/docs/document-api/reference/tables/set-row-options.mdx +++ b/apps/docs/document-api/reference/tables/set-row-options.mdx @@ -1,7 +1,7 @@ --- title: tables.setRowOptions sidebarTitle: tables.setRowOptions -description: Reference for tables.setRowOptions +description: Set options on a table row such as header repeat or page break. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.setRowOptions ## Summary +Set options on a table row such as header repeat or page break. + - Operation ID: `tables.setRowOptions` - API member path: `editor.doc.tables.setRowOptions(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.setRowOptions - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if row options already match. + ## Input fields | Field | Type | Required | Description | @@ -33,7 +39,19 @@ description: Reference for tables.setRowOptions ### Example request ```json -{} +{ + "nodeId": "node-def456", + "tableTarget": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + }, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/set-shading.mdx b/apps/docs/document-api/reference/tables/set-shading.mdx index 054ed9e37a..f8c622331a 100644 --- a/apps/docs/document-api/reference/tables/set-shading.mdx +++ b/apps/docs/document-api/reference/tables/set-shading.mdx @@ -1,7 +1,7 @@ --- title: tables.setShading sidebarTitle: tables.setShading -description: Reference for tables.setShading +description: Set the background shading color on a table or cell range. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.setShading ## Summary +Set the background shading color on a table or cell range. + - Operation ID: `tables.setShading` - API member path: `editor.doc.tables.setShading(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.setShading - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if shading already matches. + ## Input fields | Field | Type | Required | Description | @@ -29,7 +35,15 @@ description: Reference for tables.setShading ### Example request ```json -{} +{ + "color": "example", + "nodeId": "node-def456", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/set-style-option.mdx b/apps/docs/document-api/reference/tables/set-style-option.mdx index 71d7a9849b..4167a15a3a 100644 --- a/apps/docs/document-api/reference/tables/set-style-option.mdx +++ b/apps/docs/document-api/reference/tables/set-style-option.mdx @@ -1,7 +1,7 @@ --- title: tables.setStyleOption sidebarTitle: tables.setStyleOption -description: Reference for tables.setStyleOption +description: Toggle a conditional style option such as banded rows or first column. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.setStyleOption ## Summary +Toggle a conditional style option such as banded rows or first column. + - Operation ID: `tables.setStyleOption` - API member path: `editor.doc.tables.setStyleOption(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.setStyleOption - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if the style option already matches. + ## Input fields | Field | Type | Required | Description | @@ -30,7 +36,16 @@ description: Reference for tables.setStyleOption ### Example request ```json -{} +{ + "enabled": true, + "flag": "headerRow", + "nodeId": "node-def456", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/set-style.mdx b/apps/docs/document-api/reference/tables/set-style.mdx index 27d84600aa..f05c4f5e06 100644 --- a/apps/docs/document-api/reference/tables/set-style.mdx +++ b/apps/docs/document-api/reference/tables/set-style.mdx @@ -1,7 +1,7 @@ --- title: tables.setStyle sidebarTitle: tables.setStyle -description: Reference for tables.setStyle +description: Apply a named table style to the target table. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.setStyle ## Summary +Apply a named table style to the target table. + - Operation ID: `tables.setStyle` - API member path: `editor.doc.tables.setStyle(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.setStyle - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if the table already uses the requested style. + ## Input fields | Field | Type | Required | Description | @@ -29,7 +35,15 @@ description: Reference for tables.setStyle ### Example request ```json -{} +{ + "nodeId": "node-def456", + "styleId": "style-001", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/set-table-padding.mdx b/apps/docs/document-api/reference/tables/set-table-padding.mdx index 88752718aa..ebba2b5ca8 100644 --- a/apps/docs/document-api/reference/tables/set-table-padding.mdx +++ b/apps/docs/document-api/reference/tables/set-table-padding.mdx @@ -1,7 +1,7 @@ --- title: tables.setTablePadding sidebarTitle: tables.setTablePadding -description: Reference for tables.setTablePadding +description: Set default cell padding for the entire table. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.setTablePadding ## Summary +Set default cell padding for the entire table. + - Operation ID: `tables.setTablePadding` - API member path: `editor.doc.tables.setTablePadding(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.setTablePadding - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if table padding already matches. + ## Input fields | Field | Type | Required | Description | @@ -32,7 +38,18 @@ description: Reference for tables.setTablePadding ### Example request ```json -{} +{ + "bottomPt": 12.5, + "leftPt": 12.5, + "nodeId": "node-def456", + "rightPt": 12.5, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + }, + "topPt": 12.5 +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/sort.mdx b/apps/docs/document-api/reference/tables/sort.mdx index bbf674c7af..14f98ce4d5 100644 --- a/apps/docs/document-api/reference/tables/sort.mdx +++ b/apps/docs/document-api/reference/tables/sort.mdx @@ -1,7 +1,7 @@ --- title: tables.sort sidebarTitle: tables.sort -description: Reference for tables.sort +description: Sort table rows by a column value. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.sort ## Summary +Sort table rows by a column value. + - Operation ID: `tables.sort` - API member path: `editor.doc.tables.sort(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.sort - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt confirming rows were reordered. + ## Input fields | Field | Type | Required | Description | @@ -29,7 +35,21 @@ description: Reference for tables.sort ### Example request ```json -{} +{ + "keys": [ + { + "columnIndex": 1, + "direction": "ascending", + "type": "text" + } + ], + "nodeId": "node-def456", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/split-cell.mdx b/apps/docs/document-api/reference/tables/split-cell.mdx index 248d088def..d4dd4250ea 100644 --- a/apps/docs/document-api/reference/tables/split-cell.mdx +++ b/apps/docs/document-api/reference/tables/split-cell.mdx @@ -1,7 +1,7 @@ --- title: tables.splitCell sidebarTitle: tables.splitCell -description: Reference for tables.splitCell +description: Split a table cell into multiple cells. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.splitCell ## Summary +Split a table cell into multiple cells. + - Operation ID: `tables.splitCell` - API member path: `editor.doc.tables.splitCell(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.splitCell - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt confirming the cell was split. + ## Input fields | Field | Type | Required | Description | @@ -30,7 +36,16 @@ description: Reference for tables.splitCell ### Example request ```json -{} +{ + "columns": 1, + "nodeId": "node-def456", + "rows": 1, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/split.mdx b/apps/docs/document-api/reference/tables/split.mdx index 2613638a91..2b18b0bd57 100644 --- a/apps/docs/document-api/reference/tables/split.mdx +++ b/apps/docs/document-api/reference/tables/split.mdx @@ -1,7 +1,7 @@ --- title: tables.split sidebarTitle: tables.split -description: Reference for tables.split +description: Split a table into two tables at the target row. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.split ## Summary +Split a table into two tables at the target row. + - Operation ID: `tables.split` - API member path: `editor.doc.tables.split(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.split - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt confirming the table was split at the target row. + ## Input fields | Field | Type | Required | Description | @@ -29,7 +35,15 @@ description: Reference for tables.split ### Example request ```json -{} +{ + "atRowIndex": 1, + "nodeId": "node-def456", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/tables/unmerge-cells.mdx b/apps/docs/document-api/reference/tables/unmerge-cells.mdx index 8b4bd41080..b8fdd3c2d4 100644 --- a/apps/docs/document-api/reference/tables/unmerge-cells.mdx +++ b/apps/docs/document-api/reference/tables/unmerge-cells.mdx @@ -1,7 +1,7 @@ --- title: tables.unmergeCells sidebarTitle: tables.unmergeCells -description: Reference for tables.unmergeCells +description: Unmerge a previously merged table cell. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for tables.unmergeCells ## Summary +Unmerge a previously merged table cell. + - Operation ID: `tables.unmergeCells` - API member path: `editor.doc.tables.unmergeCells(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for tables.unmergeCells - Supports dry run: `yes` - Deterministic target resolution: `yes` +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if the cell is not merged. + ## Input fields | Field | Type | Required | Description | @@ -28,7 +34,14 @@ description: Reference for tables.unmergeCells ### Example request ```json -{} +{ + "nodeId": "node-def456", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} ``` ## Output fields diff --git a/apps/docs/document-api/reference/track-changes/decide.mdx b/apps/docs/document-api/reference/track-changes/decide.mdx index 944f4837ad..18522d9ab5 100644 --- a/apps/docs/document-api/reference/track-changes/decide.mdx +++ b/apps/docs/document-api/reference/track-changes/decide.mdx @@ -1,7 +1,7 @@ --- title: trackChanges.decide sidebarTitle: trackChanges.decide -description: Reference for trackChanges.decide +description: "Accept or reject a tracked change (by ID or scope: all)." --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for trackChanges.decide ## Summary +Accept or reject a tracked change (by ID or scope: all). + - Operation ID: `trackChanges.decide` - API member path: `editor.doc.trackChanges.decide(...)` - Mutates document: `yes` @@ -18,6 +20,10 @@ description: Reference for trackChanges.decide - Supports dry run: `no` - Deterministic target resolution: `yes` +## Expected result + +Returns a Receipt confirming the decision was applied; reports NO_OP if the change was already resolved. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/track-changes/get.mdx b/apps/docs/document-api/reference/track-changes/get.mdx index ffce472a2e..632a6d1865 100644 --- a/apps/docs/document-api/reference/track-changes/get.mdx +++ b/apps/docs/document-api/reference/track-changes/get.mdx @@ -1,7 +1,7 @@ --- title: trackChanges.get sidebarTitle: trackChanges.get -description: Reference for trackChanges.get +description: Retrieve a single tracked change by ID. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for trackChanges.get ## Summary +Retrieve a single tracked change by ID. + - Operation ID: `trackChanges.get` - API member path: `editor.doc.trackChanges.get(...)` - Mutates document: `no` @@ -18,6 +20,10 @@ description: Reference for trackChanges.get - Supports dry run: `no` - Deterministic target resolution: `yes` +## Expected result + +Returns a TrackChangeInfo object with the change type, author, date, and affected content. + ## Input fields | Field | Type | Required | Description | diff --git a/apps/docs/document-api/reference/track-changes/list.mdx b/apps/docs/document-api/reference/track-changes/list.mdx index af2745fb90..08737cfc79 100644 --- a/apps/docs/document-api/reference/track-changes/list.mdx +++ b/apps/docs/document-api/reference/track-changes/list.mdx @@ -1,7 +1,7 @@ --- title: trackChanges.list sidebarTitle: trackChanges.list -description: Reference for trackChanges.list +description: List all tracked changes in the document. --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,6 +10,8 @@ description: Reference for trackChanges.list ## Summary +List all tracked changes in the document. + - Operation ID: `trackChanges.list` - API member path: `editor.doc.trackChanges.list(...)` - Mutates document: `no` @@ -18,6 +20,10 @@ description: Reference for trackChanges.list - Supports dry run: `no` - Deterministic target resolution: `yes` +## Expected result + +Returns a TrackChangesListResult with an array of tracked change entries and total count. + ## Input fields | Field | Type | Required | Description | diff --git a/packages/document-api/scripts/check-contract-parity.ts b/packages/document-api/scripts/check-contract-parity.ts index 3501b252b9..2a06f0174e 100644 --- a/packages/document-api/scripts/check-contract-parity.ts +++ b/packages/document-api/scripts/check-contract-parity.ts @@ -8,6 +8,8 @@ import { COMMAND_CATALOG, DOCUMENT_API_MEMBER_PATHS, + OPERATION_DESCRIPTION_MAP, + OPERATION_EXPECTED_RESULT_MAP, OPERATION_IDS, OPERATION_MEMBER_PATH_MAP, REFERENCE_OPERATION_ALIASES, @@ -289,6 +291,12 @@ function run(): void { if (OPERATION_REFERENCE_DOC_PATH_MAP[id] !== defEntry.referenceDocPath) { errors.push(`OPERATION_REFERENCE_DOC_PATH_MAP['${id}'] !== OPERATION_DEFINITIONS['${id}'].referenceDocPath`); } + if (OPERATION_DESCRIPTION_MAP[id] !== defEntry.description) { + errors.push(`OPERATION_DESCRIPTION_MAP['${id}'] !== OPERATION_DEFINITIONS['${id}'].description`); + } + if (OPERATION_EXPECTED_RESULT_MAP[id] !== defEntry.expectedResult) { + errors.push(`OPERATION_EXPECTED_RESULT_MAP['${id}'] !== OPERATION_DEFINITIONS['${id}'].expectedResult`); + } } if (errors.length > 0) { diff --git a/packages/document-api/scripts/lib/contract-output-artifacts.ts b/packages/document-api/scripts/lib/contract-output-artifacts.ts index 6055677ebf..b0c856853b 100644 --- a/packages/document-api/scripts/lib/contract-output-artifacts.ts +++ b/packages/document-api/scripts/lib/contract-output-artifacts.ts @@ -1,5 +1,6 @@ import { buildContractSnapshot } from './contract-snapshot.js'; import { stableStringify, type GeneratedFile } from './generation-utils.js'; +import { OPERATION_EXPECTED_RESULT_MAP } from '../../src/index.js'; const GENERATED_FILE_HEADER = 'GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`.\n'; @@ -71,6 +72,7 @@ export function buildToolManifestArtifacts(): GeneratedFile[] { name: operationId, memberPath: operation.memberPath, description: toToolDescription(operationId, operation.metadata.mutates), + expectedResult: OPERATION_EXPECTED_RESULT_MAP[operationId as keyof typeof OPERATION_EXPECTED_RESULT_MAP], mutates: operation.metadata.mutates, idempotency: operation.metadata.idempotency, supportsTrackedMode: operation.metadata.supportsTrackedMode, diff --git a/packages/document-api/scripts/lib/contract-snapshot.ts b/packages/document-api/scripts/lib/contract-snapshot.ts index 5db30ea7ad..0c6828b496 100644 --- a/packages/document-api/scripts/lib/contract-snapshot.ts +++ b/packages/document-api/scripts/lib/contract-snapshot.ts @@ -2,6 +2,8 @@ import { COMMAND_CATALOG, CONTRACT_VERSION, JSON_SCHEMA_DIALECT, + OPERATION_DESCRIPTION_MAP, + OPERATION_EXPECTED_RESULT_MAP, OPERATION_IDS, OPERATION_MEMBER_PATH_MAP, buildInternalContractSchemas, @@ -44,6 +46,8 @@ export function buildContractSnapshot(): ContractSnapshot { schemaDialect: JSON_SCHEMA_DIALECT, operationCatalog: COMMAND_CATALOG, operationMap: OPERATION_MEMBER_PATH_MAP, + operationDescriptions: OPERATION_DESCRIPTION_MAP, + operationExpectedResults: OPERATION_EXPECTED_RESULT_MAP, schemas: internalSchemas.operations, }; diff --git a/packages/document-api/scripts/lib/reference-docs-artifacts.ts b/packages/document-api/scripts/lib/reference-docs-artifacts.ts index c3d5582472..5ce29fb562 100644 --- a/packages/document-api/scripts/lib/reference-docs-artifacts.ts +++ b/packages/document-api/scripts/lib/reference-docs-artifacts.ts @@ -10,6 +10,7 @@ import { } from './generation-utils.js'; import { OPERATION_DESCRIPTION_MAP, + OPERATION_EXPECTED_RESULT_MAP, OPERATION_REFERENCE_DOC_PATH_MAP, REFERENCE_OPERATION_ALIASES, REFERENCE_OPERATION_GROUPS, @@ -56,6 +57,18 @@ function toPublicDocHref(path: string): string { return `/${path.replace(/^apps\/docs\//u, '').replace(/\.mdx$/u, '')}`; } +/** + * Quote a string for safe use as a YAML frontmatter value. + * Wraps in double quotes when the value contains characters that would + * break unquoted YAML scalars (colons, hash signs, brackets, etc.). + */ +function yamlQuote(value: string): string { + if (/[:#\[\]{}&*!|>'"%@`]/u.test(value)) { + return `"${value.replace(/\\/gu, '\\\\').replace(/"/gu, '\\"')}"`; + } + return value; +} + function renderList(values: readonly string[]): string { if (values.length === 0) return '- None'; return values.map((value) => `- \`${value}\``).join('\n'); @@ -310,6 +323,28 @@ const INTEGER_EXAMPLES: Record = { listLevel: 0, }; +function applyNumericBounds(value: number, schema: JsonSchema, type: 'integer' | 'number'): number { + let bounded = value; + + const minimum = typeof schema.minimum === 'number' ? schema.minimum : undefined; + const maximum = typeof schema.maximum === 'number' ? schema.maximum : undefined; + const exclusiveMinimum = typeof schema.exclusiveMinimum === 'number' ? schema.exclusiveMinimum : undefined; + const exclusiveMaximum = typeof schema.exclusiveMaximum === 'number' ? schema.exclusiveMaximum : undefined; + + if (minimum !== undefined && bounded < minimum) bounded = minimum; + if (exclusiveMinimum !== undefined && bounded <= exclusiveMinimum) { + bounded = type === 'integer' ? Math.floor(exclusiveMinimum) + 1 : exclusiveMinimum + 0.1; + } + + if (maximum !== undefined && bounded > maximum) bounded = maximum; + if (exclusiveMaximum !== undefined && bounded >= exclusiveMaximum) { + bounded = type === 'integer' ? Math.ceil(exclusiveMaximum) - 1 : exclusiveMaximum - 0.1; + } + + if (!Number.isFinite(bounded)) return type === 'integer' ? 1 : 12.5; + return type === 'integer' ? Math.trunc(bounded) : bounded; +} + /** * Generate a deterministic example value from a JSON Schema node. * `fieldName` is used to pick contextual string/integer values. @@ -330,14 +365,6 @@ function generateExample(schema: JsonSchema, $defs: Defs, fieldName?: string, de return generateExample(resolved, $defs, fieldName, depth); } - // oneOf / anyOf — first variant - for (const keyword of ['oneOf', 'anyOf'] as const) { - const variants = schema[keyword]; - if (Array.isArray(variants) && variants.length > 0) { - return generateExample(variants[0] as JsonSchema, $defs, fieldName, depth); - } - } - // array — single item if (schema.type === 'array') { const items = schema.items as JsonSchema | undefined; @@ -350,6 +377,17 @@ function generateExample(schema: JsonSchema, $defs: Defs, fieldName?: string, de if (schema.type === 'object' && schema.properties) { const properties = schema.properties as Record; const requiredSet = new Set(Array.isArray(schema.required) ? (schema.required as string[]) : []); + for (const keyword of ['oneOf', 'anyOf'] as const) { + const variants = schema[keyword]; + if (!Array.isArray(variants) || variants.length === 0) continue; + const firstVariant = variants[0] as JsonSchema; + if (Array.isArray(firstVariant.required)) { + for (const requiredField of firstVariant.required as string[]) { + requiredSet.add(requiredField); + } + } + break; + } const result: Record = {}; const keys = Object.keys(properties); @@ -366,18 +404,26 @@ function generateExample(schema: JsonSchema, $defs: Defs, fieldName?: string, de return result; } + // oneOf / anyOf — first variant (non-object union fallback) + for (const keyword of ['oneOf', 'anyOf'] as const) { + const variants = schema[keyword]; + if (Array.isArray(variants) && variants.length > 0) { + return generateExample(variants[0] as JsonSchema, $defs, fieldName, depth); + } + } + // primitives if (schema.type === 'string') { if (fieldName && STRING_EXAMPLES[fieldName] !== undefined) return STRING_EXAMPLES[fieldName]; return 'example'; } if (schema.type === 'integer') { - if (fieldName && INTEGER_EXAMPLES[fieldName] !== undefined) return INTEGER_EXAMPLES[fieldName]; - return 1; + const base = fieldName && INTEGER_EXAMPLES[fieldName] !== undefined ? INTEGER_EXAMPLES[fieldName] : 1; + return applyNumericBounds(base, schema, 'integer'); } if (schema.type === 'number') { - if (fieldName && INTEGER_EXAMPLES[fieldName] !== undefined) return INTEGER_EXAMPLES[fieldName]; - return 12.5; + const base = fieldName && INTEGER_EXAMPLES[fieldName] !== undefined ? INTEGER_EXAMPLES[fieldName] : 12.5; + return applyNumericBounds(base, schema, 'number'); } if (schema.type === 'boolean') return true; @@ -436,6 +482,8 @@ function buildOperationGroups(operations: ContractOperationSnapshot[]): Operatio function renderOperationPage(operation: ContractOperationSnapshot, $defs: Defs): string { const title = operation.operationId; const metadata = operation.metadata; + const description = OPERATION_DESCRIPTION_MAP[operation.operationId]; + const expectedResult = OPERATION_EXPECTED_RESULT_MAP[operation.operationId]; const inputRows = buildFieldRows(operation.schemas.input, $defs); const outputRows = buildFieldRows(operation.schemas.output, $defs); @@ -457,7 +505,7 @@ function renderOperationPage(operation: ContractOperationSnapshot, $defs: Defs): return `--- title: ${title} sidebarTitle: ${title} -description: Reference for ${title} +description: ${yamlQuote(description)} --- ${GENERATED_MARKER} @@ -466,6 +514,8 @@ ${GENERATED_MARKER} ## Summary +${description} + - Operation ID: \`${operation.operationId}\` - API member path: \`${formatMemberPath(operation.memberPath)}\` - Mutates document: \`${metadata.mutates ? 'yes' : 'no'}\` @@ -474,6 +524,10 @@ ${GENERATED_MARKER} - Supports dry run: \`${metadata.supportsDryRun ? 'yes' : 'no'}\` - Deterministic target resolution: \`${metadata.deterministicTargetResolution ? 'yes' : 'no'}\` +## Expected result + +${expectedResult} + ## Input fields ${renderFieldTable(inputRows)} @@ -751,9 +805,30 @@ export function buildReferenceDocsArtifacts(): GeneratedFile[] { } /** - * Checks that generated `.mdx` files contain the generated marker and that - * the overview doc's API-surface block is up to date. Skips files already - * present in {@link existingIssuePaths} to avoid duplicate reports. + * Validate that YAML frontmatter values don't contain unquoted special characters. + * Returns an array of field names with invalid values. + */ +function validateFrontmatter(content: string): string[] { + const match = /^---\n([\s\S]*?)\n---/u.exec(content); + if (!match) return []; + + const invalid: string[] = []; + for (const line of match[1].split('\n')) { + const kvMatch = /^(\w+):\s+(.+)$/u.exec(line); + if (!kvMatch) continue; + const [, key, value] = kvMatch; + // Unquoted values containing colons break YAML parsing + if (!value.startsWith('"') && !value.startsWith("'") && /:/u.test(value)) { + invalid.push(key); + } + } + return invalid; +} + +/** + * Checks that generated `.mdx` files contain the generated marker, have valid + * YAML frontmatter, and that the overview doc's API-surface block is up to date. + * Skips files already present in {@link existingIssuePaths} to avoid duplicate reports. */ export async function checkReferenceDocsExtras(files: GeneratedFile[], issues: GeneratedCheckIssue[]): Promise { const existingIssuePaths = new Set(issues.map((issue) => issue.path)); @@ -763,6 +838,11 @@ export async function checkReferenceDocsExtras(files: GeneratedFile[], issues: G const content = await readFile(resolveWorkspacePath(file.path), 'utf8').catch(() => null); if (content == null || !content.includes(GENERATED_MARKER)) { issues.push({ kind: 'content', path: file.path }); + continue; + } + const invalidFields = validateFrontmatter(content); + if (invalidFields.length > 0) { + issues.push({ kind: 'content', path: file.path }); } } diff --git a/packages/document-api/src/contract/command-catalog.ts b/packages/document-api/src/contract/command-catalog.ts index 2d356472ee..af909cfd30 100644 --- a/packages/document-api/src/contract/command-catalog.ts +++ b/packages/document-api/src/contract/command-catalog.ts @@ -16,6 +16,11 @@ export const OPERATION_REQUIRES_DOCUMENT_CONTEXT_MAP: Record entry.requiresDocumentContext, ); +/** Maps each operation to its expected-result description. */ +export const OPERATION_EXPECTED_RESULT_MAP: Record = projectFromDefinitions( + (_id, entry) => entry.expectedResult, +); + /** * Returns the static metadata for a given operation. * diff --git a/packages/document-api/src/contract/contract.test.ts b/packages/document-api/src/contract/contract.test.ts index 61f02bb9c3..0eb2811ebe 100644 --- a/packages/document-api/src/contract/contract.test.ts +++ b/packages/document-api/src/contract/contract.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { COMMAND_CATALOG } from './command-catalog.js'; +import { COMMAND_CATALOG, OPERATION_DESCRIPTION_MAP, OPERATION_EXPECTED_RESULT_MAP } from './command-catalog.js'; import { OPERATION_DEFINITIONS, type ReferenceGroupKey } from './operation-definitions.js'; import { DOCUMENT_API_MEMBER_PATHS, OPERATION_MEMBER_PATH_MAP, memberPathForOperation } from './operation-map.js'; import { OPERATION_REFERENCE_DOC_PATH_MAP, REFERENCE_OPERATION_GROUPS } from './reference-doc-map.js'; @@ -121,6 +121,7 @@ describe('document-api contract catalog', () => { 'blocks', 'capabilities', 'create', + 'sections', 'format', 'styles', 'lists', @@ -152,4 +153,25 @@ describe('document-api contract catalog', () => { expect(OPERATION_REFERENCE_DOC_PATH_MAP[id]).toBe(OPERATION_DEFINITIONS[id].referenceDocPath); } }); + + it('projects descriptions that match OPERATION_DEFINITIONS', () => { + for (const id of OPERATION_IDS) { + expect(OPERATION_DESCRIPTION_MAP[id]).toBe(OPERATION_DEFINITIONS[id].description); + } + }); + + it('projects expected results that match OPERATION_DEFINITIONS', () => { + for (const id of OPERATION_IDS) { + expect(OPERATION_EXPECTED_RESULT_MAP[id]).toBe(OPERATION_DEFINITIONS[id].expectedResult); + } + }); + + it('ensures every operation has a non-empty expectedResult', () => { + for (const id of OPERATION_IDS) { + const expectedResult = OPERATION_DEFINITIONS[id].expectedResult; + expect(expectedResult, `${id} has empty expectedResult`).toBeTruthy(); + expect(typeof expectedResult).toBe('string'); + expect(expectedResult.length, `${id} expectedResult is too short`).toBeGreaterThan(10); + } + }); }); diff --git a/packages/document-api/src/contract/operation-definitions.ts b/packages/document-api/src/contract/operation-definitions.ts index 3e069af1d7..47c8e35c16 100644 --- a/packages/document-api/src/contract/operation-definitions.ts +++ b/packages/document-api/src/contract/operation-definitions.ts @@ -9,7 +9,7 @@ * ## Adding a new operation * * 1. **Here** (`operation-definitions.ts`) — add an entry to `OPERATION_DEFINITIONS` - * with `memberPath`, `metadata`, `referenceDocPath`, and `referenceGroup`. + * with `memberPath`, `description`, `expectedResult`, `metadata`, `referenceDocPath`, and `referenceGroup`. * 2. **`operation-registry.ts`** — add a type entry (`input`, `options`, `output`). * The bidirectional `Assert` checks will error until this is done. * 3. **`invoke.ts`** (`buildDispatchTable`) — add a one-line dispatch entry calling @@ -36,6 +36,7 @@ export type ReferenceGroupKey = | 'blocks' | 'capabilities' | 'create' + | 'sections' | 'format' | 'styles' | 'lists' @@ -52,6 +53,7 @@ export type ReferenceGroupKey = export interface OperationDefinitionEntry { memberPath: string; description: string; + expectedResult: string; requiresDocumentContext: boolean; metadata: CommandStaticMetadata; referenceDocPath: string; @@ -142,6 +144,23 @@ const T_NOT_FOUND_COMMAND = ['TARGET_NOT_FOUND', 'INVALID_TARGET', 'CAPABILITY_U const T_NOT_FOUND_COMMAND_TRACKED = [...T_NOT_FOUND_COMMAND] as const; const T_QUERY_MATCH = ['MATCH_NOT_FOUND', 'AMBIGUOUS_MATCH', 'INVALID_INPUT', 'INTERNAL_ERROR'] as const; +const T_SECTION_CREATE = [ + 'TARGET_NOT_FOUND', + 'INVALID_TARGET', + 'AMBIGUOUS_TARGET', + 'INVALID_INPUT', + 'CAPABILITY_UNAVAILABLE', + 'INTERNAL_ERROR', +] as const; +const T_SECTION_READ = ['TARGET_NOT_FOUND', 'INVALID_TARGET', 'INVALID_INPUT', 'CAPABILITY_UNAVAILABLE'] as const; +const T_SECTION_MUTATION = [ + 'TARGET_NOT_FOUND', + 'INVALID_TARGET', + 'INVALID_INPUT', + 'CAPABILITY_UNAVAILABLE', + 'INTERNAL_ERROR', +] as const; +const T_SECTION_SETTINGS_MUTATION = ['INVALID_INPUT', 'CAPABILITY_UNAVAILABLE', 'INTERNAL_ERROR'] as const; // --------------------------------------------------------------------------- // Canonical definitions @@ -151,6 +170,8 @@ export const OPERATION_DEFINITIONS = { find: { memberPath: 'find', description: 'Search the document for nodes matching type, text, or attribute criteria.', + expectedResult: + 'Returns a FindOutput with matched items array and total count, or an empty items array if no nodes match.', requiresDocumentContext: true, metadata: readOperation({ idempotency: 'idempotent', @@ -163,6 +184,7 @@ export const OPERATION_DEFINITIONS = { getNode: { memberPath: 'getNode', description: 'Retrieve a single node by target position.', + expectedResult: 'Returns a NodeInfo object with the node type, address, content, and typed properties.', requiresDocumentContext: true, metadata: readOperation({ idempotency: 'idempotent', @@ -174,6 +196,7 @@ export const OPERATION_DEFINITIONS = { getNodeById: { memberPath: 'getNodeById', description: 'Retrieve a single node by its unique ID.', + expectedResult: 'Returns a NodeInfo object with the node type, address, content, and typed properties.', requiresDocumentContext: true, metadata: readOperation({ idempotency: 'idempotent', @@ -185,6 +208,7 @@ export const OPERATION_DEFINITIONS = { getText: { memberPath: 'getText', description: 'Extract the plain-text content of the document.', + expectedResult: 'Returns the full plain-text content of the document as a string.', requiresDocumentContext: true, metadata: readOperation(), referenceDocPath: 'get-text.mdx', @@ -193,6 +217,7 @@ export const OPERATION_DEFINITIONS = { info: { memberPath: 'info', description: 'Return document metadata including revision, node count, and capabilities.', + expectedResult: 'Returns a DocumentInfo object with revision, word/paragraph/heading counts, and capability flags.', requiresDocumentContext: true, metadata: readOperation(), referenceDocPath: 'info.mdx', @@ -203,6 +228,8 @@ export const OPERATION_DEFINITIONS = { memberPath: 'insert', description: 'Insert content at a target position. Supports text (default), markdown, and html content types via the `type` field.', + expectedResult: + 'Returns a TextMutationReceipt with applied status; receipt reports NO_OP if the insertion point is invalid or content is empty.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'non-idempotent', @@ -217,6 +244,8 @@ export const OPERATION_DEFINITIONS = { replace: { memberPath: 'replace', description: 'Replace content at a target position with new text or inline content.', + expectedResult: + 'Returns a TextMutationReceipt with applied status; receipt reports NO_OP if the target range already contains identical content.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -231,6 +260,8 @@ export const OPERATION_DEFINITIONS = { delete: { memberPath: 'delete', description: 'Delete content at a target position.', + expectedResult: + 'Returns a TextMutationReceipt with applied status; receipt reports NO_OP if the target range is already empty.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -246,6 +277,7 @@ export const OPERATION_DEFINITIONS = { 'blocks.delete': { memberPath: 'blocks.delete', description: 'Delete an entire block node (paragraph, heading, list item, table, image, or sdt) deterministically.', + expectedResult: 'Returns a BlocksDeleteResult receipt confirming the block was removed from the document.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -269,6 +301,7 @@ export const OPERATION_DEFINITIONS = { memberPath: 'format.apply', description: "Apply explicit inline style changes (bold, italic, underline, strike) to the target range using directive semantics ('on', 'off', 'clear').", + expectedResult: 'Returns a TextMutationReceipt confirming inline styles were applied to the target range.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -283,6 +316,8 @@ export const OPERATION_DEFINITIONS = { 'format.fontSize': { memberPath: 'format.fontSize', description: 'Set or unset the font size on the target text range. Pass null to remove.', + expectedResult: + 'Returns a TextMutationReceipt; receipt reports NO_OP if the target already has the requested font size.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -297,6 +332,8 @@ export const OPERATION_DEFINITIONS = { 'format.fontFamily': { memberPath: 'format.fontFamily', description: 'Set or unset the font family on the target text range. Pass null to remove.', + expectedResult: + 'Returns a TextMutationReceipt; receipt reports NO_OP if the target already has the requested font family.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -311,6 +348,8 @@ export const OPERATION_DEFINITIONS = { 'format.color': { memberPath: 'format.color', description: 'Set or unset the text color on the target text range. Pass null to remove.', + expectedResult: + 'Returns a TextMutationReceipt; receipt reports NO_OP if the target already has the requested color.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -325,6 +364,8 @@ export const OPERATION_DEFINITIONS = { 'format.align': { memberPath: 'format.align', description: 'Set or unset paragraph alignment on the block containing the target. Pass null to reset to default.', + expectedResult: + 'Returns a TextMutationReceipt; receipt reports NO_OP if the block already has the requested alignment.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -341,6 +382,7 @@ export const OPERATION_DEFINITIONS = { memberPath: 'styles.apply', description: 'Apply document-level default style changes to the stylesheet (word/styles.xml). Targets docDefaults run properties with boolean patch semantics.', + expectedResult: 'Returns a StylesApplyReceipt with per-channel success/failure details for each property change.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'idempotent', @@ -356,6 +398,7 @@ export const OPERATION_DEFINITIONS = { 'create.paragraph': { memberPath: 'create.paragraph', description: 'Create a new paragraph at the target position.', + expectedResult: 'Returns a CreateParagraphResult with the new paragraph block ID and address.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'non-idempotent', @@ -370,6 +413,7 @@ export const OPERATION_DEFINITIONS = { 'create.heading': { memberPath: 'create.heading', description: 'Create a new heading at the target position.', + expectedResult: 'Returns a CreateHeadingResult with the new heading block ID and address.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'non-idempotent', @@ -381,10 +425,301 @@ export const OPERATION_DEFINITIONS = { referenceDocPath: 'create/heading.mdx', referenceGroup: 'create', }, + 'create.sectionBreak': { + memberPath: 'create.sectionBreak', + description: 'Create a section break at the target location with optional initial section properties.', + expectedResult: 'Returns a CreateSectionBreakResult with the new section break position and section address.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'non-idempotent', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['INVALID_TARGET', 'CAPABILITY_UNAVAILABLE'], + throws: T_SECTION_CREATE, + }), + referenceDocPath: 'create/section-break.mdx', + referenceGroup: 'create', + }, + + 'sections.list': { + memberPath: 'sections.list', + description: 'List sections in deterministic order with section-target handles.', + expectedResult: 'Returns a SectionsListResult with an ordered array of section summaries and their target handles.', + requiresDocumentContext: true, + metadata: readOperation({ + idempotency: 'idempotent', + throws: ['INVALID_INPUT', 'CAPABILITY_UNAVAILABLE'], + }), + referenceDocPath: 'sections/list.mdx', + referenceGroup: 'sections', + }, + 'sections.get': { + memberPath: 'sections.get', + description: 'Retrieve full section information by section address.', + expectedResult: + 'Returns a SectionInfo object with full section properties including margins, columns, and header/footer refs.', + requiresDocumentContext: true, + metadata: readOperation({ + idempotency: 'idempotent', + throws: T_SECTION_READ, + }), + referenceDocPath: 'sections/get.mdx', + referenceGroup: 'sections', + }, + 'sections.setBreakType': { + memberPath: 'sections.setBreakType', + description: 'Set the section break type.', + expectedResult: + 'Returns a SectionMutationResult receipt; reports NO_OP if the section already has the requested break type.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP', 'INVALID_TARGET', 'CAPABILITY_UNAVAILABLE'], + throws: T_SECTION_MUTATION, + }), + referenceDocPath: 'sections/set-break-type.mdx', + referenceGroup: 'sections', + }, + 'sections.setPageMargins': { + memberPath: 'sections.setPageMargins', + description: 'Set page-edge margins for a section.', + expectedResult: + 'Returns a SectionMutationResult receipt; reports NO_OP if margins already match the requested values.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP', 'INVALID_TARGET', 'CAPABILITY_UNAVAILABLE'], + throws: T_SECTION_MUTATION, + }), + referenceDocPath: 'sections/set-page-margins.mdx', + referenceGroup: 'sections', + }, + 'sections.setHeaderFooterMargins': { + memberPath: 'sections.setHeaderFooterMargins', + description: 'Set header/footer margin distances for a section.', + expectedResult: + 'Returns a SectionMutationResult receipt; reports NO_OP if header/footer margins already match the requested values.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP', 'INVALID_TARGET', 'CAPABILITY_UNAVAILABLE'], + throws: T_SECTION_MUTATION, + }), + referenceDocPath: 'sections/set-header-footer-margins.mdx', + referenceGroup: 'sections', + }, + 'sections.setPageSetup': { + memberPath: 'sections.setPageSetup', + description: 'Set page size/orientation properties for a section.', + expectedResult: + 'Returns a SectionMutationResult receipt; reports NO_OP if page size and orientation already match the requested values.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP', 'INVALID_TARGET', 'CAPABILITY_UNAVAILABLE'], + throws: T_SECTION_MUTATION, + }), + referenceDocPath: 'sections/set-page-setup.mdx', + referenceGroup: 'sections', + }, + 'sections.setColumns': { + memberPath: 'sections.setColumns', + description: 'Set column configuration for a section.', + expectedResult: 'Returns a SectionMutationResult receipt; reports NO_OP if column configuration already matches.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP', 'INVALID_TARGET', 'CAPABILITY_UNAVAILABLE'], + throws: T_SECTION_MUTATION, + }), + referenceDocPath: 'sections/set-columns.mdx', + referenceGroup: 'sections', + }, + 'sections.setLineNumbering': { + memberPath: 'sections.setLineNumbering', + description: 'Enable or configure line numbering for a section.', + expectedResult: 'Returns a SectionMutationResult receipt; reports NO_OP if line numbering settings already match.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP', 'INVALID_TARGET', 'CAPABILITY_UNAVAILABLE'], + throws: T_SECTION_MUTATION, + }), + referenceDocPath: 'sections/set-line-numbering.mdx', + referenceGroup: 'sections', + }, + 'sections.setPageNumbering': { + memberPath: 'sections.setPageNumbering', + description: 'Set page numbering format/start for a section.', + expectedResult: 'Returns a SectionMutationResult receipt; reports NO_OP if page numbering format already matches.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP', 'INVALID_TARGET', 'CAPABILITY_UNAVAILABLE'], + throws: T_SECTION_MUTATION, + }), + referenceDocPath: 'sections/set-page-numbering.mdx', + referenceGroup: 'sections', + }, + 'sections.setTitlePage': { + memberPath: 'sections.setTitlePage', + description: 'Enable or disable title-page behavior for a section.', + expectedResult: 'Returns a SectionMutationResult receipt; reports NO_OP if the title-page setting already matches.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP', 'INVALID_TARGET', 'CAPABILITY_UNAVAILABLE'], + throws: T_SECTION_MUTATION, + }), + referenceDocPath: 'sections/set-title-page.mdx', + referenceGroup: 'sections', + }, + 'sections.setOddEvenHeadersFooters': { + memberPath: 'sections.setOddEvenHeadersFooters', + description: 'Enable or disable odd/even header-footer mode in document settings.', + expectedResult: 'Returns a DocumentMutationResult receipt; reports NO_OP if the odd/even setting already matches.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP', 'CAPABILITY_UNAVAILABLE'], + throws: T_SECTION_SETTINGS_MUTATION, + }), + referenceDocPath: 'sections/set-odd-even-headers-footers.mdx', + referenceGroup: 'sections', + }, + 'sections.setVerticalAlign': { + memberPath: 'sections.setVerticalAlign', + description: 'Set vertical page alignment for a section.', + expectedResult: 'Returns a SectionMutationResult receipt; reports NO_OP if vertical alignment already matches.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP', 'INVALID_TARGET', 'CAPABILITY_UNAVAILABLE'], + throws: T_SECTION_MUTATION, + }), + referenceDocPath: 'sections/set-vertical-align.mdx', + referenceGroup: 'sections', + }, + 'sections.setSectionDirection': { + memberPath: 'sections.setSectionDirection', + description: 'Set section text flow direction (LTR/RTL).', + expectedResult: 'Returns a SectionMutationResult receipt; reports NO_OP if text direction already matches.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP', 'INVALID_TARGET', 'CAPABILITY_UNAVAILABLE'], + throws: T_SECTION_MUTATION, + }), + referenceDocPath: 'sections/set-section-direction.mdx', + referenceGroup: 'sections', + }, + 'sections.setHeaderFooterRef': { + memberPath: 'sections.setHeaderFooterRef', + description: 'Set or replace a section header/footer reference for a variant.', + expectedResult: + 'Returns a SectionMutationResult receipt; reports NO_OP if the header/footer reference already matches.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP', 'INVALID_TARGET', 'CAPABILITY_UNAVAILABLE'], + throws: T_SECTION_MUTATION, + }), + referenceDocPath: 'sections/set-header-footer-ref.mdx', + referenceGroup: 'sections', + }, + 'sections.clearHeaderFooterRef': { + memberPath: 'sections.clearHeaderFooterRef', + description: 'Clear a section header/footer reference for a specific variant.', + expectedResult: + 'Returns a SectionMutationResult receipt; reports NO_OP if no reference exists for the specified variant.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP', 'INVALID_TARGET', 'CAPABILITY_UNAVAILABLE'], + throws: T_SECTION_MUTATION, + }), + referenceDocPath: 'sections/clear-header-footer-ref.mdx', + referenceGroup: 'sections', + }, + 'sections.setLinkToPrevious': { + memberPath: 'sections.setLinkToPrevious', + description: 'Set or clear link-to-previous behavior for a header/footer variant.', + expectedResult: + 'Returns a SectionMutationResult receipt; reports NO_OP if link-to-previous already matches the requested value.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP', 'INVALID_TARGET', 'CAPABILITY_UNAVAILABLE'], + throws: T_SECTION_MUTATION, + }), + referenceDocPath: 'sections/set-link-to-previous.mdx', + referenceGroup: 'sections', + }, + 'sections.setPageBorders': { + memberPath: 'sections.setPageBorders', + description: 'Set page border configuration for a section.', + expectedResult: + 'Returns a SectionMutationResult receipt; reports NO_OP if page border configuration already matches.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP', 'INVALID_TARGET', 'CAPABILITY_UNAVAILABLE'], + throws: T_SECTION_MUTATION, + }), + referenceDocPath: 'sections/set-page-borders.mdx', + referenceGroup: 'sections', + }, + 'sections.clearPageBorders': { + memberPath: 'sections.clearPageBorders', + description: 'Clear page border configuration for a section.', + expectedResult: + 'Returns a SectionMutationResult receipt; reports NO_OP if no page borders are configured on the section.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP', 'INVALID_TARGET', 'CAPABILITY_UNAVAILABLE'], + throws: T_SECTION_MUTATION, + }), + referenceDocPath: 'sections/clear-page-borders.mdx', + referenceGroup: 'sections', + }, 'lists.list': { memberPath: 'lists.list', description: 'List all list nodes in the document, optionally filtered by scope.', + expectedResult: 'Returns a ListsListResult with an array of list item summaries and total count.', requiresDocumentContext: true, metadata: readOperation({ idempotency: 'idempotent', @@ -396,6 +731,7 @@ export const OPERATION_DEFINITIONS = { 'lists.get': { memberPath: 'lists.get', description: 'Retrieve a specific list node by target.', + expectedResult: 'Returns a ListItemInfo object with the item kind, level, marker, and address.', requiresDocumentContext: true, metadata: readOperation({ idempotency: 'idempotent', @@ -407,6 +743,7 @@ export const OPERATION_DEFINITIONS = { 'lists.insert': { memberPath: 'lists.insert', description: 'Insert a new list at the target position.', + expectedResult: 'Returns a ListsInsertResult with the new list item address and block ID.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'non-idempotent', @@ -421,6 +758,8 @@ export const OPERATION_DEFINITIONS = { 'lists.setType': { memberPath: 'lists.setType', description: 'Change the list type (ordered, unordered) of a target list.', + expectedResult: + 'Returns a ListsMutateItemResult receipt; reports NO_OP if the list already has the requested type.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -435,6 +774,8 @@ export const OPERATION_DEFINITIONS = { 'lists.indent': { memberPath: 'lists.indent', description: 'Increase the indentation level of a list item.', + expectedResult: + 'Returns a ListsMutateItemResult receipt; reports NO_OP if the item is already at maximum indent level.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -449,6 +790,7 @@ export const OPERATION_DEFINITIONS = { 'lists.outdent': { memberPath: 'lists.outdent', description: 'Decrease the indentation level of a list item.', + expectedResult: 'Returns a ListsMutateItemResult receipt; reports NO_OP if the item is already at the root level.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -463,6 +805,8 @@ export const OPERATION_DEFINITIONS = { 'lists.restart': { memberPath: 'lists.restart', description: 'Restart numbering of an ordered list at the target item.', + expectedResult: + 'Returns a ListsMutateItemResult receipt; reports NO_OP if numbering already restarts at the target item.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -477,6 +821,7 @@ export const OPERATION_DEFINITIONS = { 'lists.exit': { memberPath: 'lists.exit', description: 'Exit a list context, converting the target item to a paragraph.', + expectedResult: 'Returns a ListsExitResult confirming the item was converted to a plain paragraph.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -492,6 +837,8 @@ export const OPERATION_DEFINITIONS = { 'comments.create': { memberPath: 'comments.create', description: 'Create a new comment thread (or reply when parentCommentId is given).', + expectedResult: + 'Returns a Receipt confirming the comment was created; reports NO_OP if the anchor target is invalid.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'non-idempotent', @@ -506,6 +853,7 @@ export const OPERATION_DEFINITIONS = { 'comments.patch': { memberPath: 'comments.patch', description: 'Patch fields on an existing comment (text, target, status, or isInternal).', + expectedResult: 'Returns a Receipt confirming the comment was updated; reports NO_OP if no fields changed.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -520,6 +868,8 @@ export const OPERATION_DEFINITIONS = { 'comments.delete': { memberPath: 'comments.delete', description: 'Remove a comment or reply by ID.', + expectedResult: + 'Returns a Receipt confirming the comment was removed; reports NO_OP if the comment was already deleted.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -534,6 +884,7 @@ export const OPERATION_DEFINITIONS = { 'comments.get': { memberPath: 'comments.get', description: 'Retrieve a single comment thread by ID.', + expectedResult: 'Returns a CommentInfo object with the comment text, author, date, and thread metadata.', requiresDocumentContext: true, metadata: readOperation({ idempotency: 'idempotent', @@ -545,6 +896,7 @@ export const OPERATION_DEFINITIONS = { 'comments.list': { memberPath: 'comments.list', description: 'List all comment threads in the document.', + expectedResult: 'Returns a CommentsListResult with an array of comment threads and total count.', requiresDocumentContext: true, metadata: readOperation({ idempotency: 'idempotent', @@ -557,6 +909,7 @@ export const OPERATION_DEFINITIONS = { 'trackChanges.list': { memberPath: 'trackChanges.list', description: 'List all tracked changes in the document.', + expectedResult: 'Returns a TrackChangesListResult with an array of tracked change entries and total count.', requiresDocumentContext: true, metadata: readOperation({ idempotency: 'idempotent', @@ -568,6 +921,7 @@ export const OPERATION_DEFINITIONS = { 'trackChanges.get': { memberPath: 'trackChanges.get', description: 'Retrieve a single tracked change by ID.', + expectedResult: 'Returns a TrackChangeInfo object with the change type, author, date, and affected content.', requiresDocumentContext: true, metadata: readOperation({ idempotency: 'idempotent', @@ -579,6 +933,8 @@ export const OPERATION_DEFINITIONS = { 'trackChanges.decide': { memberPath: 'trackChanges.decide', description: 'Accept or reject a tracked change (by ID or scope: all).', + expectedResult: + 'Returns a Receipt confirming the decision was applied; reports NO_OP if the change was already resolved.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -594,6 +950,7 @@ export const OPERATION_DEFINITIONS = { 'query.match': { memberPath: 'query.match', description: 'Deterministic selector-based search with cardinality contracts for mutation targeting.', + expectedResult: 'Returns a QueryMatchOutput with the resolved target address and cardinality metadata.', requiresDocumentContext: true, metadata: readOperation({ idempotency: 'idempotent', @@ -607,6 +964,7 @@ export const OPERATION_DEFINITIONS = { 'mutations.preview': { memberPath: 'mutations.preview', description: 'Dry-run a mutation plan, returning resolved targets without applying changes.', + expectedResult: 'Returns a MutationsPreviewOutput with resolved targets and step details without applying changes.', requiresDocumentContext: true, metadata: readOperation({ idempotency: 'idempotent', @@ -620,6 +978,7 @@ export const OPERATION_DEFINITIONS = { 'mutations.apply': { memberPath: 'mutations.apply', description: 'Execute a mutation plan atomically against the document.', + expectedResult: 'Returns a PlanReceipt with per-step results for the atomically applied mutation plan.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'non-idempotent', @@ -636,6 +995,7 @@ export const OPERATION_DEFINITIONS = { 'capabilities.get': { memberPath: 'capabilities', description: 'Query runtime capabilities supported by the current document engine.', + expectedResult: 'Returns a DocumentApiCapabilities object describing supported features of the current engine.', requiresDocumentContext: false, metadata: readOperation({ idempotency: 'idempotent', @@ -652,6 +1012,7 @@ export const OPERATION_DEFINITIONS = { 'create.table': { memberPath: 'create.table', description: 'Create a new table at the target position.', + expectedResult: 'Returns a CreateTableResult with the new table block ID and address.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'non-idempotent', @@ -671,6 +1032,7 @@ export const OPERATION_DEFINITIONS = { 'tables.convertFromText': { memberPath: 'tables.convertFromText', description: 'Convert a text range into a table.', + expectedResult: 'Returns a TableMutationResult receipt confirming text was converted into a table.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'non-idempotent', @@ -685,6 +1047,7 @@ export const OPERATION_DEFINITIONS = { 'tables.delete': { memberPath: 'tables.delete', description: 'Delete the target table from the document.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if the table was already removed.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -699,6 +1062,7 @@ export const OPERATION_DEFINITIONS = { 'tables.clearContents': { memberPath: 'tables.clearContents', description: 'Clear the contents of the target table or cell range.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if the target cells are already empty.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -713,6 +1077,8 @@ export const OPERATION_DEFINITIONS = { 'tables.move': { memberPath: 'tables.move', description: 'Move a table to a new position in the document.', + expectedResult: + 'Returns a TableMutationResult receipt; reports NO_OP if the table is already at the target position.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -727,6 +1093,7 @@ export const OPERATION_DEFINITIONS = { 'tables.split': { memberPath: 'tables.split', description: 'Split a table into two tables at the target row.', + expectedResult: 'Returns a TableMutationResult receipt confirming the table was split at the target row.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'non-idempotent', @@ -741,6 +1108,7 @@ export const OPERATION_DEFINITIONS = { 'tables.convertToText': { memberPath: 'tables.convertToText', description: 'Convert a table back to plain text.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if the table has no content to convert.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -760,6 +1128,8 @@ export const OPERATION_DEFINITIONS = { 'tables.setLayout': { memberPath: 'tables.setLayout', description: 'Set the layout mode of the target table.', + expectedResult: + 'Returns a TableMutationResult receipt; reports NO_OP if the table already uses the requested layout mode.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'idempotent', @@ -779,6 +1149,7 @@ export const OPERATION_DEFINITIONS = { 'tables.insertRow': { memberPath: 'tables.insertRow', description: 'Insert a new row into the target table.', + expectedResult: 'Returns a TableMutationResult receipt confirming a row was inserted.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'non-idempotent', @@ -793,6 +1164,7 @@ export const OPERATION_DEFINITIONS = { 'tables.deleteRow': { memberPath: 'tables.deleteRow', description: 'Delete a row from the target table.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if the target row does not exist.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -807,6 +1179,7 @@ export const OPERATION_DEFINITIONS = { 'tables.setRowHeight': { memberPath: 'tables.setRowHeight', description: 'Set the height of a table row.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if the row height already matches.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'idempotent', @@ -821,6 +1194,7 @@ export const OPERATION_DEFINITIONS = { 'tables.distributeRows': { memberPath: 'tables.distributeRows', description: 'Distribute row heights evenly across the target table.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if row heights are already equal.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -835,6 +1209,7 @@ export const OPERATION_DEFINITIONS = { 'tables.setRowOptions': { memberPath: 'tables.setRowOptions', description: 'Set options on a table row such as header repeat or page break.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if row options already match.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'idempotent', @@ -854,6 +1229,7 @@ export const OPERATION_DEFINITIONS = { 'tables.insertColumn': { memberPath: 'tables.insertColumn', description: 'Insert a new column into the target table.', + expectedResult: 'Returns a TableMutationResult receipt confirming a column was inserted.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'non-idempotent', @@ -868,6 +1244,7 @@ export const OPERATION_DEFINITIONS = { 'tables.deleteColumn': { memberPath: 'tables.deleteColumn', description: 'Delete a column from the target table.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if the target column does not exist.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -882,6 +1259,7 @@ export const OPERATION_DEFINITIONS = { 'tables.setColumnWidth': { memberPath: 'tables.setColumnWidth', description: 'Set the width of a table column.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if the column width already matches.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'idempotent', @@ -896,6 +1274,7 @@ export const OPERATION_DEFINITIONS = { 'tables.distributeColumns': { memberPath: 'tables.distributeColumns', description: 'Distribute column widths evenly across the target table.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if column widths are already equal.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -915,6 +1294,7 @@ export const OPERATION_DEFINITIONS = { 'tables.insertCell': { memberPath: 'tables.insertCell', description: 'Insert a new cell into a table row.', + expectedResult: 'Returns a TableMutationResult receipt confirming a cell was inserted.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'non-idempotent', @@ -929,6 +1309,7 @@ export const OPERATION_DEFINITIONS = { 'tables.deleteCell': { memberPath: 'tables.deleteCell', description: 'Delete a cell from a table row.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if the target cell does not exist.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -943,6 +1324,7 @@ export const OPERATION_DEFINITIONS = { 'tables.mergeCells': { memberPath: 'tables.mergeCells', description: 'Merge a range of table cells into one.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if the cells are already merged.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -957,6 +1339,7 @@ export const OPERATION_DEFINITIONS = { 'tables.unmergeCells': { memberPath: 'tables.unmergeCells', description: 'Unmerge a previously merged table cell.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if the cell is not merged.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -971,6 +1354,7 @@ export const OPERATION_DEFINITIONS = { 'tables.splitCell': { memberPath: 'tables.splitCell', description: 'Split a table cell into multiple cells.', + expectedResult: 'Returns a TableMutationResult receipt confirming the cell was split.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'non-idempotent', @@ -985,6 +1369,7 @@ export const OPERATION_DEFINITIONS = { 'tables.setCellProperties': { memberPath: 'tables.setCellProperties', description: 'Set properties on a table cell such as vertical alignment or text direction.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if cell properties already match.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'idempotent', @@ -1004,6 +1389,7 @@ export const OPERATION_DEFINITIONS = { 'tables.sort': { memberPath: 'tables.sort', description: 'Sort table rows by a column value.', + expectedResult: 'Returns a TableMutationResult receipt confirming rows were reordered.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'non-idempotent', @@ -1018,6 +1404,7 @@ export const OPERATION_DEFINITIONS = { 'tables.setAltText': { memberPath: 'tables.setAltText', description: 'Set the alternative text description for a table.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if alt text already matches.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'idempotent', @@ -1037,6 +1424,8 @@ export const OPERATION_DEFINITIONS = { 'tables.setStyle': { memberPath: 'tables.setStyle', description: 'Apply a named table style to the target table.', + expectedResult: + 'Returns a TableMutationResult receipt; reports NO_OP if the table already uses the requested style.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'idempotent', @@ -1051,6 +1440,7 @@ export const OPERATION_DEFINITIONS = { 'tables.clearStyle': { memberPath: 'tables.clearStyle', description: 'Remove the applied table style, reverting to defaults.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if no table style is applied.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -1065,6 +1455,7 @@ export const OPERATION_DEFINITIONS = { 'tables.setStyleOption': { memberPath: 'tables.setStyleOption', description: 'Toggle a conditional style option such as banded rows or first column.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if the style option already matches.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'idempotent', @@ -1079,6 +1470,7 @@ export const OPERATION_DEFINITIONS = { 'tables.setBorder': { memberPath: 'tables.setBorder', description: 'Set border properties on a table or cell range.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if border properties already match.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'idempotent', @@ -1093,6 +1485,7 @@ export const OPERATION_DEFINITIONS = { 'tables.clearBorder': { memberPath: 'tables.clearBorder', description: 'Remove border formatting from a table or cell range.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if no borders are set.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -1107,6 +1500,7 @@ export const OPERATION_DEFINITIONS = { 'tables.applyBorderPreset': { memberPath: 'tables.applyBorderPreset', description: 'Apply a border preset (e.g. all borders, outside only) to a table.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if the preset is already applied.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'idempotent', @@ -1121,6 +1515,7 @@ export const OPERATION_DEFINITIONS = { 'tables.setShading': { memberPath: 'tables.setShading', description: 'Set the background shading color on a table or cell range.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if shading already matches.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'idempotent', @@ -1135,6 +1530,7 @@ export const OPERATION_DEFINITIONS = { 'tables.clearShading': { memberPath: 'tables.clearShading', description: 'Remove shading from a table or cell range.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if no shading is set.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -1149,6 +1545,7 @@ export const OPERATION_DEFINITIONS = { 'tables.setTablePadding': { memberPath: 'tables.setTablePadding', description: 'Set default cell padding for the entire table.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if table padding already matches.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'idempotent', @@ -1163,6 +1560,7 @@ export const OPERATION_DEFINITIONS = { 'tables.setCellPadding': { memberPath: 'tables.setCellPadding', description: 'Set padding on a specific table cell or cell range.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if cell padding already matches.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'idempotent', @@ -1177,6 +1575,7 @@ export const OPERATION_DEFINITIONS = { 'tables.setCellSpacing': { memberPath: 'tables.setCellSpacing', description: 'Set the cell spacing for the target table.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if cell spacing already matches.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'idempotent', @@ -1191,6 +1590,7 @@ export const OPERATION_DEFINITIONS = { 'tables.clearCellSpacing': { memberPath: 'tables.clearCellSpacing', description: 'Remove custom cell spacing from the target table.', + expectedResult: 'Returns a TableMutationResult receipt; reports NO_OP if no custom cell spacing is set.', requiresDocumentContext: true, metadata: mutationOperation({ idempotency: 'conditional', @@ -1210,6 +1610,7 @@ export const OPERATION_DEFINITIONS = { 'tables.get': { memberPath: 'tables.get', description: 'Retrieve table structure and dimensions by locator.', + expectedResult: 'Returns a TablesGetOutput with the table row count, column count, and structural metadata.', requiresDocumentContext: true, metadata: readOperation({ idempotency: 'idempotent', @@ -1221,6 +1622,7 @@ export const OPERATION_DEFINITIONS = { 'tables.getCells': { memberPath: 'tables.getCells', description: 'Retrieve cell information for a table, optionally filtered by row or column.', + expectedResult: 'Returns a TablesGetCellsOutput with cell information for the requested rows and columns.', requiresDocumentContext: true, metadata: readOperation({ idempotency: 'idempotent', @@ -1232,6 +1634,7 @@ export const OPERATION_DEFINITIONS = { 'tables.getProperties': { memberPath: 'tables.getProperties', description: 'Retrieve layout and style properties of a table.', + expectedResult: 'Returns a TablesGetPropertiesOutput with the table layout, style, border, and shading properties.', requiresDocumentContext: true, metadata: readOperation({ idempotency: 'idempotent', diff --git a/packages/document-api/src/contract/operation-registry.ts b/packages/document-api/src/contract/operation-registry.ts index e6c0a6193c..5a84c2f174 100644 --- a/packages/document-api/src/contract/operation-registry.ts +++ b/packages/document-api/src/contract/operation-registry.ts @@ -57,6 +57,32 @@ import type { ListTargetInput, ListsExitResult, } from '../lists/lists.types.js'; +import type { + CreateSectionBreakInput, + CreateSectionBreakResult, + DocumentMutationResult, + SectionInfo, + SectionMutationResult, + SectionsClearHeaderFooterRefInput, + SectionsClearPageBordersInput, + SectionsGetInput, + SectionsListQuery, + SectionsListResult, + SectionsSetBreakTypeInput, + SectionsSetColumnsInput, + SectionsSetHeaderFooterMarginsInput, + SectionsSetHeaderFooterRefInput, + SectionsSetLineNumberingInput, + SectionsSetLinkToPreviousInput, + SectionsSetOddEvenHeadersFootersInput, + SectionsSetPageBordersInput, + SectionsSetPageMarginsInput, + SectionsSetPageNumberingInput, + SectionsSetPageSetupInput, + SectionsSetSectionDirectionInput, + SectionsSetTitlePageInput, + SectionsSetVerticalAlignInput, +} from '../sections/sections.types.js'; import type { QueryMatchInput, QueryMatchOutput } from '../types/query-match.types.js'; import type { MutationsApplyInput, @@ -140,6 +166,7 @@ export interface OperationRegistry { // --- create.* --- 'create.paragraph': { input: CreateParagraphInput; options: MutationOptions; output: CreateParagraphResult }; 'create.heading': { input: CreateHeadingInput; options: MutationOptions; output: CreateHeadingResult }; + 'create.sectionBreak': { input: CreateSectionBreakInput; options: MutationOptions; output: CreateSectionBreakResult }; // --- lists.* --- 'lists.list': { input: ListsListQuery | undefined; options: never; output: ListsListResult }; @@ -151,6 +178,86 @@ export interface OperationRegistry { 'lists.restart': { input: ListTargetInput; options: MutationOptions; output: ListsMutateItemResult }; 'lists.exit': { input: ListTargetInput; options: MutationOptions; output: ListsExitResult }; + // --- sections.* --- + 'sections.list': { input: SectionsListQuery | undefined; options: never; output: SectionsListResult }; + 'sections.get': { input: SectionsGetInput; options: never; output: SectionInfo }; + 'sections.setBreakType': { + input: SectionsSetBreakTypeInput; + options: MutationOptions; + output: SectionMutationResult; + }; + 'sections.setPageMargins': { + input: SectionsSetPageMarginsInput; + options: MutationOptions; + output: SectionMutationResult; + }; + 'sections.setHeaderFooterMargins': { + input: SectionsSetHeaderFooterMarginsInput; + options: MutationOptions; + output: SectionMutationResult; + }; + 'sections.setPageSetup': { + input: SectionsSetPageSetupInput; + options: MutationOptions; + output: SectionMutationResult; + }; + 'sections.setColumns': { input: SectionsSetColumnsInput; options: MutationOptions; output: SectionMutationResult }; + 'sections.setLineNumbering': { + input: SectionsSetLineNumberingInput; + options: MutationOptions; + output: SectionMutationResult; + }; + 'sections.setPageNumbering': { + input: SectionsSetPageNumberingInput; + options: MutationOptions; + output: SectionMutationResult; + }; + 'sections.setTitlePage': { + input: SectionsSetTitlePageInput; + options: MutationOptions; + output: SectionMutationResult; + }; + 'sections.setOddEvenHeadersFooters': { + input: SectionsSetOddEvenHeadersFootersInput; + options: MutationOptions; + output: DocumentMutationResult; + }; + 'sections.setVerticalAlign': { + input: SectionsSetVerticalAlignInput; + options: MutationOptions; + output: SectionMutationResult; + }; + 'sections.setSectionDirection': { + input: SectionsSetSectionDirectionInput; + options: MutationOptions; + output: SectionMutationResult; + }; + 'sections.setHeaderFooterRef': { + input: SectionsSetHeaderFooterRefInput; + options: MutationOptions; + output: SectionMutationResult; + }; + 'sections.clearHeaderFooterRef': { + input: SectionsClearHeaderFooterRefInput; + options: MutationOptions; + output: SectionMutationResult; + }; + 'sections.setLinkToPrevious': { + input: SectionsSetLinkToPreviousInput; + options: MutationOptions; + output: SectionMutationResult; + }; + 'sections.setPageBorders': { + input: SectionsSetPageBordersInput; + options: MutationOptions; + output: SectionMutationResult; + }; + 'sections.clearPageBorders': { + input: SectionsClearPageBordersInput; + options: MutationOptions; + output: SectionMutationResult; + }; + // --- comments.* --- 'comments.create': { input: CommentsCreateInput; options: RevisionGuardOptions; output: Receipt }; 'comments.patch': { input: CommentsPatchInput; options: RevisionGuardOptions; output: Receipt }; diff --git a/packages/document-api/src/contract/reference-doc-map.ts b/packages/document-api/src/contract/reference-doc-map.ts index e6b0d180db..283e8f1836 100644 --- a/packages/document-api/src/contract/reference-doc-map.ts +++ b/packages/document-api/src/contract/reference-doc-map.ts @@ -41,6 +41,11 @@ const GROUP_METADATA: Record = { }, ['kind', 'nodeType', 'nodeId'], ), + SectionAddress: objectSchema( + { + kind: { const: 'section' }, + sectionId: { type: 'string' }, + }, + ['kind', 'sectionId'], + ), InlineNodeAddress: objectSchema( { kind: { const: 'inline' }, @@ -328,6 +335,7 @@ const deletableBlockNodeAddressSchema = ref('DeletableBlockNodeAddress'); const paragraphAddressSchema = ref('ParagraphAddress'); const headingAddressSchema = ref('HeadingAddress'); const listItemAddressSchema = ref('ListItemAddress'); +const sectionAddressSchema = ref('SectionAddress'); const inlineNodeAddressSchema = ref('InlineNodeAddress'); const nodeAddressSchema = ref('NodeAddress'); const commentAddressSchema = ref('CommentAddress'); @@ -762,6 +770,232 @@ const listItemDomainItemSchema = discoveryItemSchema( const listsListResultSchema = discoveryResultSchema(listItemDomainItemSchema); +const sectionBreakTypeSchema: JsonSchema = { enum: ['continuous', 'nextPage', 'evenPage', 'oddPage'] }; +const sectionOrientationSchema: JsonSchema = { enum: ['portrait', 'landscape'] }; +const sectionVerticalAlignSchema: JsonSchema = { enum: ['top', 'center', 'bottom', 'both'] }; +const sectionDirectionSchema: JsonSchema = { enum: ['ltr', 'rtl'] }; +const sectionHeaderFooterKindSchema: JsonSchema = { enum: ['header', 'footer'] }; +const sectionHeaderFooterVariantSchema: JsonSchema = { enum: ['default', 'first', 'even'] }; +const sectionLineNumberRestartSchema: JsonSchema = { enum: ['continuous', 'newPage', 'newSection'] }; +const sectionPageNumberFormatSchema: JsonSchema = { + enum: ['decimal', 'lowerLetter', 'upperLetter', 'lowerRoman', 'upperRoman', 'numberInDash'], +}; + +const sectionRangeDomainSchema = objectSchema( + { + startParagraphIndex: { type: 'integer', minimum: 0 }, + endParagraphIndex: { type: 'integer', minimum: 0 }, + }, + ['startParagraphIndex', 'endParagraphIndex'], +); + +const sectionPageMarginsSchema = objectSchema({ + top: { type: 'number', minimum: 0 }, + right: { type: 'number', minimum: 0 }, + bottom: { type: 'number', minimum: 0 }, + left: { type: 'number', minimum: 0 }, + gutter: { type: 'number', minimum: 0 }, +}); + +const sectionHeaderFooterMarginsSchema = objectSchema({ + header: { type: 'number', minimum: 0 }, + footer: { type: 'number', minimum: 0 }, +}); + +const sectionPageSetupSchema = objectSchema({ + width: { type: 'number', minimum: 0 }, + height: { type: 'number', minimum: 0 }, + orientation: sectionOrientationSchema, + paperSize: { type: 'string' }, +}); + +const sectionColumnsSchema = objectSchema({ + count: { type: 'integer', minimum: 1 }, + gap: { type: 'number', minimum: 0 }, + equalWidth: { type: 'boolean' }, +}); + +const sectionLineNumberingSchema = objectSchema( + { + enabled: { type: 'boolean' }, + countBy: { type: 'integer', minimum: 1 }, + start: { type: 'integer', minimum: 1 }, + distance: { type: 'number', minimum: 0 }, + restart: sectionLineNumberRestartSchema, + }, + ['enabled'], +); + +const sectionPageNumberingSchema = objectSchema({ + start: { type: 'integer', minimum: 1 }, + format: sectionPageNumberFormatSchema, +}); + +const sectionHeaderFooterRefsSchema = objectSchema({ + default: { type: 'string' }, + first: { type: 'string' }, + even: { type: 'string' }, +}); + +const sectionBorderSpecSchema = objectSchema({ + style: { type: 'string' }, + size: { type: 'number', minimum: 0 }, + space: { type: 'number', minimum: 0 }, + color: { type: 'string' }, + shadow: { type: 'boolean' }, + frame: { type: 'boolean' }, +}); + +sectionBorderSpecSchema.oneOf = [ + { required: ['style'] }, + { required: ['size'] }, + { required: ['space'] }, + { required: ['color'] }, + { required: ['shadow'] }, + { required: ['frame'] }, +]; + +const sectionPageBordersSchema = objectSchema({ + display: { enum: ['allPages', 'firstPage', 'notFirstPage'] }, + offsetFrom: { enum: ['page', 'text'] }, + zOrder: { enum: ['front', 'back'] }, + top: sectionBorderSpecSchema, + right: sectionBorderSpecSchema, + bottom: sectionBorderSpecSchema, + left: sectionBorderSpecSchema, +}); + +sectionPageBordersSchema.oneOf = [ + { required: ['display'] }, + { required: ['offsetFrom'] }, + { required: ['zOrder'] }, + { required: ['top'] }, + { required: ['right'] }, + { required: ['bottom'] }, + { required: ['left'] }, +]; + +const sectionInfoSchema = objectSchema( + { + address: sectionAddressSchema, + index: { type: 'integer', minimum: 0 }, + range: sectionRangeDomainSchema, + breakType: sectionBreakTypeSchema, + pageSetup: sectionPageSetupSchema, + margins: sectionPageMarginsSchema, + headerFooterMargins: sectionHeaderFooterMarginsSchema, + columns: sectionColumnsSchema, + lineNumbering: sectionLineNumberingSchema, + pageNumbering: sectionPageNumberingSchema, + titlePage: { type: 'boolean' }, + oddEvenHeadersFooters: { type: 'boolean' }, + verticalAlign: sectionVerticalAlignSchema, + sectionDirection: sectionDirectionSchema, + headerRefs: sectionHeaderFooterRefsSchema, + footerRefs: sectionHeaderFooterRefsSchema, + pageBorders: sectionPageBordersSchema, + }, + ['address', 'index', 'range'], +); + +const sectionResolvedHandleSchema = objectSchema( + { + ref: { type: 'string' }, + refStability: { const: 'ephemeral' }, + targetKind: { const: 'section' }, + }, + ['ref', 'refStability', 'targetKind'], +); + +const sectionDomainItemSchema = objectSchema( + { + id: { type: 'string' }, + handle: sectionResolvedHandleSchema, + address: sectionAddressSchema, + index: { type: 'integer', minimum: 0 }, + range: sectionRangeDomainSchema, + breakType: sectionBreakTypeSchema, + pageSetup: sectionPageSetupSchema, + margins: sectionPageMarginsSchema, + headerFooterMargins: sectionHeaderFooterMarginsSchema, + columns: sectionColumnsSchema, + lineNumbering: sectionLineNumberingSchema, + pageNumbering: sectionPageNumberingSchema, + titlePage: { type: 'boolean' }, + oddEvenHeadersFooters: { type: 'boolean' }, + verticalAlign: sectionVerticalAlignSchema, + sectionDirection: sectionDirectionSchema, + headerRefs: sectionHeaderFooterRefsSchema, + footerRefs: sectionHeaderFooterRefsSchema, + pageBorders: sectionPageBordersSchema, + }, + ['id', 'handle', 'address', 'index', 'range'], +); + +const sectionsListResultSchema = discoveryResultSchema(sectionDomainItemSchema); + +const sectionMutationSuccessSchema = objectSchema( + { + success: { const: true }, + section: sectionAddressSchema, + }, + ['success', 'section'], +); + +function sectionMutationFailureSchemaFor(operationId: OperationId): JsonSchema { + return objectSchema( + { + success: { const: false }, + failure: receiptFailureSchemaFor(operationId), + }, + ['success', 'failure'], + ); +} + +function sectionMutationResultSchemaFor(operationId: OperationId): JsonSchema { + return { + oneOf: [sectionMutationSuccessSchema, sectionMutationFailureSchemaFor(operationId)], + }; +} + +const documentMutationSuccessSchema = objectSchema( + { + success: { const: true }, + }, + ['success'], +); + +function documentMutationResultSchemaFor(operationId: OperationId): JsonSchema { + return { + oneOf: [documentMutationSuccessSchema, sectionMutationFailureSchemaFor(operationId)], + }; +} + +const createSectionBreakSuccessSchema = objectSchema( + { + success: { const: true }, + section: sectionAddressSchema, + breakParagraph: blockNodeAddressSchema, + }, + ['success', 'section'], +); + +function createSectionBreakFailureSchemaFor(operationId: OperationId): JsonSchema { + return objectSchema( + { + success: { const: false }, + failure: receiptFailureSchemaFor(operationId), + }, + ['success', 'failure'], + ); +} + +function createSectionBreakResultSchemaFor(operationId: OperationId): JsonSchema { + return { + oneOf: [createSectionBreakSuccessSchema, createSectionBreakFailureSchemaFor(operationId)], + }; +} + const commentInfoSchema = objectSchema( { address: commentAddressSchema, @@ -942,7 +1176,7 @@ const tableLocatorSchema: JsonSchema = { oneOf: [{ required: ['target'] }, { required: ['nodeId'] }], }; -const tableScopedRowLocatorSchema: JsonSchema = { +const _tableScopedRowLocatorSchema: JsonSchema = { ...objectSchema({ tableTarget: blockNodeAddressSchema, tableNodeId: { type: 'string' }, @@ -951,7 +1185,7 @@ const tableScopedRowLocatorSchema: JsonSchema = { oneOf: [{ required: ['tableTarget'] }, { required: ['tableNodeId'] }], }; -const tableScopedColumnLocatorSchema: JsonSchema = { +const _tableScopedColumnLocatorSchema: JsonSchema = { ...objectSchema( { tableTarget: blockNodeAddressSchema, @@ -1440,6 +1674,277 @@ const operationSchemas: Record = { success: createHeadingSuccessSchema, failure: createHeadingFailureSchemaFor('create.heading'), }, + 'create.sectionBreak': { + input: objectSchema({ + 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'], + ), + ], + }, + breakType: sectionBreakTypeSchema, + pageMargins: sectionPageMarginsSchema, + headerFooterMargins: sectionHeaderFooterMarginsSchema, + }), + output: createSectionBreakResultSchemaFor('create.sectionBreak'), + success: createSectionBreakSuccessSchema, + failure: createSectionBreakFailureSchemaFor('create.sectionBreak'), + }, + 'sections.list': { + input: objectSchema({ + limit: { type: 'integer', minimum: 1 }, + offset: { type: 'integer', minimum: 0 }, + }), + output: sectionsListResultSchema, + }, + 'sections.get': { + input: objectSchema({ address: sectionAddressSchema }, ['address']), + output: sectionInfoSchema, + }, + 'sections.setBreakType': { + input: objectSchema( + { + target: sectionAddressSchema, + breakType: sectionBreakTypeSchema, + }, + ['target', 'breakType'], + ), + output: sectionMutationResultSchemaFor('sections.setBreakType'), + success: sectionMutationSuccessSchema, + failure: sectionMutationFailureSchemaFor('sections.setBreakType'), + }, + 'sections.setPageMargins': { + input: { + ...objectSchema( + { + target: sectionAddressSchema, + top: { type: 'number', minimum: 0 }, + right: { type: 'number', minimum: 0 }, + bottom: { type: 'number', minimum: 0 }, + left: { type: 'number', minimum: 0 }, + gutter: { type: 'number', minimum: 0 }, + }, + ['target'], + ), + oneOf: [ + { required: ['target', 'top'] }, + { required: ['target', 'right'] }, + { required: ['target', 'bottom'] }, + { required: ['target', 'left'] }, + { required: ['target', 'gutter'] }, + ], + }, + output: sectionMutationResultSchemaFor('sections.setPageMargins'), + success: sectionMutationSuccessSchema, + failure: sectionMutationFailureSchemaFor('sections.setPageMargins'), + }, + 'sections.setHeaderFooterMargins': { + input: { + ...objectSchema( + { + target: sectionAddressSchema, + header: { type: 'number', minimum: 0 }, + footer: { type: 'number', minimum: 0 }, + }, + ['target'], + ), + oneOf: [{ required: ['target', 'header'] }, { required: ['target', 'footer'] }], + }, + output: sectionMutationResultSchemaFor('sections.setHeaderFooterMargins'), + success: sectionMutationSuccessSchema, + failure: sectionMutationFailureSchemaFor('sections.setHeaderFooterMargins'), + }, + 'sections.setPageSetup': { + input: { + ...objectSchema( + { + target: sectionAddressSchema, + width: { type: 'number', minimum: 0 }, + height: { type: 'number', minimum: 0 }, + orientation: sectionOrientationSchema, + paperSize: { type: 'string', minLength: 1 }, + }, + ['target'], + ), + oneOf: [ + { required: ['target', 'width'] }, + { required: ['target', 'height'] }, + { required: ['target', 'orientation'] }, + { required: ['target', 'paperSize'] }, + ], + }, + output: sectionMutationResultSchemaFor('sections.setPageSetup'), + success: sectionMutationSuccessSchema, + failure: sectionMutationFailureSchemaFor('sections.setPageSetup'), + }, + 'sections.setColumns': { + input: { + ...objectSchema( + { + target: sectionAddressSchema, + count: { type: 'integer', minimum: 1 }, + gap: { type: 'number', minimum: 0 }, + equalWidth: { type: 'boolean' }, + }, + ['target'], + ), + oneOf: [ + { required: ['target', 'count'] }, + { required: ['target', 'gap'] }, + { required: ['target', 'equalWidth'] }, + ], + }, + output: sectionMutationResultSchemaFor('sections.setColumns'), + success: sectionMutationSuccessSchema, + failure: sectionMutationFailureSchemaFor('sections.setColumns'), + }, + 'sections.setLineNumbering': { + input: objectSchema( + { + target: sectionAddressSchema, + enabled: { type: 'boolean' }, + countBy: { type: 'integer', minimum: 1 }, + start: { type: 'integer', minimum: 1 }, + distance: { type: 'number', minimum: 0 }, + restart: sectionLineNumberRestartSchema, + }, + ['target', 'enabled'], + ), + output: sectionMutationResultSchemaFor('sections.setLineNumbering'), + success: sectionMutationSuccessSchema, + failure: sectionMutationFailureSchemaFor('sections.setLineNumbering'), + }, + 'sections.setPageNumbering': { + input: { + ...objectSchema( + { + target: sectionAddressSchema, + start: { type: 'integer', minimum: 1 }, + format: sectionPageNumberFormatSchema, + }, + ['target'], + ), + oneOf: [{ required: ['target', 'start'] }, { required: ['target', 'format'] }], + }, + output: sectionMutationResultSchemaFor('sections.setPageNumbering'), + success: sectionMutationSuccessSchema, + failure: sectionMutationFailureSchemaFor('sections.setPageNumbering'), + }, + 'sections.setTitlePage': { + input: objectSchema( + { + target: sectionAddressSchema, + enabled: { type: 'boolean' }, + }, + ['target', 'enabled'], + ), + output: sectionMutationResultSchemaFor('sections.setTitlePage'), + success: sectionMutationSuccessSchema, + failure: sectionMutationFailureSchemaFor('sections.setTitlePage'), + }, + 'sections.setOddEvenHeadersFooters': { + input: objectSchema({ enabled: { type: 'boolean' } }, ['enabled']), + output: documentMutationResultSchemaFor('sections.setOddEvenHeadersFooters'), + success: documentMutationSuccessSchema, + failure: sectionMutationFailureSchemaFor('sections.setOddEvenHeadersFooters'), + }, + 'sections.setVerticalAlign': { + input: objectSchema( + { + target: sectionAddressSchema, + value: sectionVerticalAlignSchema, + }, + ['target', 'value'], + ), + output: sectionMutationResultSchemaFor('sections.setVerticalAlign'), + success: sectionMutationSuccessSchema, + failure: sectionMutationFailureSchemaFor('sections.setVerticalAlign'), + }, + 'sections.setSectionDirection': { + input: objectSchema( + { + target: sectionAddressSchema, + direction: sectionDirectionSchema, + }, + ['target', 'direction'], + ), + output: sectionMutationResultSchemaFor('sections.setSectionDirection'), + success: sectionMutationSuccessSchema, + failure: sectionMutationFailureSchemaFor('sections.setSectionDirection'), + }, + 'sections.setHeaderFooterRef': { + input: objectSchema( + { + target: sectionAddressSchema, + kind: sectionHeaderFooterKindSchema, + variant: sectionHeaderFooterVariantSchema, + refId: { type: 'string', minLength: 1 }, + }, + ['target', 'kind', 'variant', 'refId'], + ), + output: sectionMutationResultSchemaFor('sections.setHeaderFooterRef'), + success: sectionMutationSuccessSchema, + failure: sectionMutationFailureSchemaFor('sections.setHeaderFooterRef'), + }, + 'sections.clearHeaderFooterRef': { + input: objectSchema( + { + target: sectionAddressSchema, + kind: sectionHeaderFooterKindSchema, + variant: sectionHeaderFooterVariantSchema, + }, + ['target', 'kind', 'variant'], + ), + output: sectionMutationResultSchemaFor('sections.clearHeaderFooterRef'), + success: sectionMutationSuccessSchema, + failure: sectionMutationFailureSchemaFor('sections.clearHeaderFooterRef'), + }, + 'sections.setLinkToPrevious': { + input: objectSchema( + { + target: sectionAddressSchema, + kind: sectionHeaderFooterKindSchema, + variant: sectionHeaderFooterVariantSchema, + linked: { type: 'boolean' }, + }, + ['target', 'kind', 'variant', 'linked'], + ), + output: sectionMutationResultSchemaFor('sections.setLinkToPrevious'), + success: sectionMutationSuccessSchema, + failure: sectionMutationFailureSchemaFor('sections.setLinkToPrevious'), + }, + 'sections.setPageBorders': { + input: objectSchema( + { + target: sectionAddressSchema, + borders: sectionPageBordersSchema, + }, + ['target', 'borders'], + ), + output: sectionMutationResultSchemaFor('sections.setPageBorders'), + success: sectionMutationSuccessSchema, + failure: sectionMutationFailureSchemaFor('sections.setPageBorders'), + }, + 'sections.clearPageBorders': { + input: objectSchema({ target: sectionAddressSchema }, ['target']), + output: sectionMutationResultSchemaFor('sections.clearPageBorders'), + success: sectionMutationSuccessSchema, + failure: sectionMutationFailureSchemaFor('sections.clearPageBorders'), + }, 'lists.list': { input: objectSchema({ within: blockNodeAddressSchema, diff --git a/packages/document-api/src/create/create.test.ts b/packages/document-api/src/create/create.test.ts index b8210fbd2b..3a1459aa7a 100644 --- a/packages/document-api/src/create/create.test.ts +++ b/packages/document-api/src/create/create.test.ts @@ -1,4 +1,9 @@ -import { executeCreateTable, normalizeCreateParagraphInput } from './create.js'; +import { + executeCreateParagraph, + executeCreateSectionBreak, + executeCreateTable, + normalizeCreateParagraphInput, +} from './create.js'; describe('normalizeCreateParagraphInput', () => { it('defaults location to documentEnd when at is omitted', () => { @@ -106,3 +111,84 @@ describe('executeCreateTable', () => { expect(tableCalled).toBe(false); }); }); + +describe('create target validation', () => { + it('rejects nodeId-based before/after placement for create.paragraph', () => { + let paragraphCalled = false; + const adapter = { + paragraph: () => { + paragraphCalled = true; + return { + success: true, + paragraph: { kind: 'block', nodeType: 'paragraph', nodeId: 'p2' }, + insertionPoint: { kind: 'text', blockId: 'p2', range: { start: 0, end: 0 } }, + }; + }, + heading: () => ({ success: true }), + table: () => ({ success: true }), + sectionBreak: () => ({ success: true }), + } as any; + + expect(() => + executeCreateParagraph(adapter, { + at: { kind: 'after', nodeId: 'p1' } as any, + }), + ).toThrow(/does not support at\.nodeId/i); + expect(paragraphCalled).toBe(false); + }); +}); + +describe('executeCreateSectionBreak', () => { + it('defaults create.sectionBreak location to documentEnd', () => { + const adapter = { + paragraph: () => ({ success: true }), + heading: () => ({ success: true }), + table: () => ({ success: true }), + sectionBreak: vi.fn(() => ({ + success: true, + section: { kind: 'section', sectionId: 'section-1' }, + })), + } as any; + + executeCreateSectionBreak(adapter, { breakType: 'nextPage' }); + + expect(adapter.sectionBreak).toHaveBeenCalledWith( + expect.objectContaining({ + at: { kind: 'documentEnd' }, + breakType: 'nextPage', + }), + { changeMode: 'direct', dryRun: false, expectedRevision: undefined }, + ); + }); + + it('rejects invalid section break type', () => { + const adapter = { + paragraph: () => ({ success: true }), + heading: () => ({ success: true }), + table: () => ({ success: true }), + sectionBreak: vi.fn(() => ({ success: true })), + } as any; + + expect(() => + executeCreateSectionBreak(adapter, { + breakType: 'invalidBreakType' as any, + }), + ).toThrow(/create\.sectionBreak breakType must be one of/i); + }); + + it('rejects nodeId-based before/after placement', () => { + const adapter = { + paragraph: () => ({ success: true }), + heading: () => ({ success: true }), + table: () => ({ success: true }), + sectionBreak: vi.fn(() => ({ success: true })), + } as any; + + expect(() => + executeCreateSectionBreak(adapter, { + at: { kind: 'before', nodeId: 'p1' } as any, + }), + ).toThrow(/does not support at\.nodeId/i); + expect(adapter.sectionBreak).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/document-api/src/create/create.ts b/packages/document-api/src/create/create.ts index 348da48f9d..24f8cef5f1 100644 --- a/packages/document-api/src/create/create.ts +++ b/packages/document-api/src/create/create.ts @@ -9,26 +9,59 @@ import type { HeadingCreateLocation, } from '../types/create.types.js'; import type { CreateTableInput, CreateTableResult, TableCreateLocation } from '../types/table-operations.types.js'; +import type { + CreateSectionBreakInput, + CreateSectionBreakResult, + SectionBreakCreateLocation, + SectionBreakType, +} from '../sections/sections.types.js'; import { DocumentApiValidationError } from '../errors.js'; export interface CreateApi { paragraph(input: CreateParagraphInput, options?: MutationOptions): CreateParagraphResult; heading(input: CreateHeadingInput, options?: MutationOptions): CreateHeadingResult; table(input: CreateTableInput, options?: MutationOptions): CreateTableResult; + sectionBreak(input: CreateSectionBreakInput, options?: MutationOptions): CreateSectionBreakResult; } export type CreateAdapter = CreateApi; /** - * Validates the `at` location for create operations when `before`/`after` is used. - * Ensures either `target` or `nodeId` is provided. + * Validates target-only create locations (paragraph, heading, section break) + * when `before`/`after` is used. + * These operations require `at.target` and do not accept `at.nodeId`. */ -function validateCreateLocation( - at: ParagraphCreateLocation | HeadingCreateLocation | TableCreateLocation, +function validateTargetOnlyCreateLocation( + at: ParagraphCreateLocation | HeadingCreateLocation | SectionBreakCreateLocation, operationName: string, ): void { if (at.kind !== 'before' && at.kind !== 'after') return; + const loc = at as { kind: string; target?: unknown; nodeId?: unknown }; + if (loc.nodeId !== undefined) { + throw new DocumentApiValidationError( + 'INVALID_TARGET', + `${operationName} does not support at.nodeId. Use at.target for before/after placement.`, + { field: 'at.nodeId' }, + ); + } + + if (loc.target === undefined) { + throw new DocumentApiValidationError( + 'INVALID_TARGET', + `${operationName} with at.kind="${at.kind}" requires at.target.`, + { field: 'at.target' }, + ); + } +} + +/** + * Validates create locations that support either `at.target` or `at.nodeId` + * when `before`/`after` is used. + */ +function validateTargetOrNodeIdCreateLocation(at: TableCreateLocation, operationName: string): void { + if (at.kind !== 'before' && at.kind !== 'after') return; + const loc = at as { kind: string; target?: unknown; nodeId?: unknown }; const hasTarget = loc.target !== undefined; const hasNodeId = loc.nodeId !== undefined; @@ -50,6 +83,46 @@ function validateCreateLocation( } } +const SECTION_BREAK_TYPES: readonly SectionBreakType[] = ['continuous', 'nextPage', 'evenPage', 'oddPage'] as const; + +function normalizeSectionBreakCreateLocation(location?: SectionBreakCreateLocation): SectionBreakCreateLocation { + return location ?? { kind: 'documentEnd' }; +} + +function validateMarginValue(field: string, value: unknown): void { + if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) { + throw new DocumentApiValidationError('INVALID_INPUT', `${field} must be a non-negative number.`, { + field, + value, + }); + } +} + +function validateCreateSectionBreakInput(input: CreateSectionBreakInput): void { + if (input.breakType !== undefined && !SECTION_BREAK_TYPES.includes(input.breakType)) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `create.sectionBreak breakType must be one of: ${SECTION_BREAK_TYPES.join(', ')}.`, + { field: 'breakType', value: input.breakType }, + ); + } + + if (input.pageMargins) { + const { top, right, bottom, left, gutter } = input.pageMargins; + if (top !== undefined) validateMarginValue('pageMargins.top', top); + if (right !== undefined) validateMarginValue('pageMargins.right', right); + if (bottom !== undefined) validateMarginValue('pageMargins.bottom', bottom); + if (left !== undefined) validateMarginValue('pageMargins.left', left); + if (gutter !== undefined) validateMarginValue('pageMargins.gutter', gutter); + } + + if (input.headerFooterMargins) { + const { header, footer } = input.headerFooterMargins; + if (header !== undefined) validateMarginValue('headerFooterMargins.header', header); + if (footer !== undefined) validateMarginValue('headerFooterMargins.footer', footer); + } +} + function normalizeParagraphCreateLocation(location?: ParagraphCreateLocation): ParagraphCreateLocation { return location ?? { kind: 'documentEnd' }; } @@ -67,7 +140,7 @@ export function executeCreateParagraph( options?: MutationOptions, ): CreateParagraphResult { const normalized = normalizeCreateParagraphInput(input); - validateCreateLocation(normalized.at!, 'create.paragraph'); + validateTargetOnlyCreateLocation(normalized.at!, 'create.paragraph'); return adapter.paragraph(normalized, normalizeMutationOptions(options)); } @@ -89,7 +162,7 @@ export function executeCreateHeading( options?: MutationOptions, ): CreateHeadingResult { const normalized = normalizeCreateHeadingInput(input); - validateCreateLocation(normalized.at!, 'create.heading'); + validateTargetOnlyCreateLocation(normalized.at!, 'create.heading'); return adapter.heading(normalized, normalizeMutationOptions(options)); } @@ -111,6 +184,26 @@ export function executeCreateTable( options?: MutationOptions, ): CreateTableResult { const normalized = normalizeCreateTableInput(input); - validateCreateLocation(normalized.at!, 'create.table'); + validateTargetOrNodeIdCreateLocation(normalized.at!, 'create.table'); return adapter.table(normalized, normalizeMutationOptions(options)); } + +export function normalizeCreateSectionBreakInput(input: CreateSectionBreakInput): CreateSectionBreakInput { + return { + at: normalizeSectionBreakCreateLocation(input.at), + breakType: input.breakType, + pageMargins: input.pageMargins, + headerFooterMargins: input.headerFooterMargins, + }; +} + +export function executeCreateSectionBreak( + adapter: CreateAdapter, + input: CreateSectionBreakInput, + options?: MutationOptions, +): CreateSectionBreakResult { + const normalized = normalizeCreateSectionBreakInput(input); + validateTargetOnlyCreateLocation(normalized.at!, 'create.sectionBreak'); + validateCreateSectionBreakInput(normalized); + return adapter.sectionBreak(normalized, normalizeMutationOptions(options)); +} diff --git a/packages/document-api/src/index.ts b/packages/document-api/src/index.ts index 9638c91dff..d44c57bcb5 100644 --- a/packages/document-api/src/index.ts +++ b/packages/document-api/src/index.ts @@ -66,7 +66,7 @@ import type { StylesApplyOptions, StylesApplyReceipt, } from './styles/styles.js'; -import { executeStylesApply, PROPERTY_REGISTRY } from './styles/styles.js'; +import { executeStylesApply } from './styles/styles.js'; import type { GetNodeAdapter, GetNodeByIdInput } from './get-node/get-node.js'; import { executeGetNode, executeGetNodeById } from './get-node/get-node.js'; import { executeGetText, type GetTextAdapter, type GetTextInput } from './get-text/get-text.js'; @@ -99,7 +99,12 @@ import { } from './lists/lists.js'; import { executeReplace, type ReplaceInput } from './replace/replace.js'; import type { CreateAdapter, CreateApi } from './create/create.js'; -import { executeCreateParagraph, executeCreateHeading, executeCreateTable } from './create/create.js'; +import { + executeCreateParagraph, + executeCreateHeading, + executeCreateTable, + executeCreateSectionBreak, +} from './create/create.js'; import type { BlocksAdapter, BlocksApi } from './blocks/blocks.js'; import { executeBlocksDelete } from './blocks/blocks.js'; import type { BlocksDeleteInput, BlocksDeleteResult } from './types/blocks.types.js'; @@ -172,6 +177,53 @@ import type { OperationId } from './contract/types.js'; import type { DynamicInvokeRequest, InvokeRequest, InvokeResult } from './contract/operation-registry.js'; import { buildDispatchTable } from './invoke/invoke.js'; import { executeTableOperation } from './tables/tables.js'; +import type { SectionsAdapter, SectionsApi } from './sections/sections.js'; +import type { + CreateSectionBreakInput, + CreateSectionBreakResult, + DocumentMutationResult, + SectionInfo, + SectionsClearHeaderFooterRefInput, + SectionsClearPageBordersInput, + SectionsGetInput, + SectionsListQuery, + SectionsListResult, + SectionsSetBreakTypeInput, + SectionsSetColumnsInput, + SectionsSetHeaderFooterMarginsInput, + SectionsSetHeaderFooterRefInput, + SectionsSetLineNumberingInput, + SectionsSetLinkToPreviousInput, + SectionsSetOddEvenHeadersFootersInput, + SectionsSetPageBordersInput, + SectionsSetPageMarginsInput, + SectionsSetPageNumberingInput, + SectionsSetPageSetupInput, + SectionsSetSectionDirectionInput, + SectionsSetTitlePageInput, + SectionsSetVerticalAlignInput, + SectionMutationResult, +} from './sections/sections.types.js'; +import { + executeSectionsClearHeaderFooterRef, + executeSectionsClearPageBorders, + executeSectionsGet, + executeSectionsList, + executeSectionsSetBreakType, + executeSectionsSetColumns, + executeSectionsSetHeaderFooterMargins, + executeSectionsSetHeaderFooterRef, + executeSectionsSetLineNumbering, + executeSectionsSetLinkToPrevious, + executeSectionsSetOddEvenHeadersFooters, + executeSectionsSetPageBorders, + executeSectionsSetPageMargins, + executeSectionsSetPageNumbering, + executeSectionsSetPageSetup, + executeSectionsSetSectionDirection, + executeSectionsSetTitlePage, + executeSectionsSetVerticalAlign, +} from './sections/sections.js'; export type { FindAdapter, FindOptions } from './find/find.js'; export type { GetNodeAdapter, GetNodeByIdInput } from './get-node/get-node.js'; @@ -229,6 +281,7 @@ export type { } from './track-changes/track-changes.js'; export type { BlocksAdapter } from './blocks/blocks.js'; export type { ListsAdapter } from './lists/lists.js'; +export type { SectionsAdapter } from './sections/sections.js'; export type { ListInsertInput, ListItemAddress, @@ -244,6 +297,54 @@ export type { ListTargetInput, } from './lists/lists.types.js'; export { LIST_KINDS, LIST_INSERT_POSITIONS } from './lists/lists.types.js'; +export type { + CreateSectionBreakInput, + CreateSectionBreakResult, + DocumentMutationResult, + SectionAddress, + SectionBorderSpec, + SectionBreakCreateLocation, + SectionBreakType, + SectionColumns, + SectionDirection, + SectionDomain, + SectionHeaderFooterKind, + SectionHeaderFooterMargins, + SectionHeaderFooterRefs, + SectionHeaderFooterVariant, + SectionInfo, + SectionLineNumbering, + SectionLineNumberRestart, + SectionMutationResult, + SectionOrientation, + SectionPageBorders, + SectionPageMargins, + SectionPageNumbering, + SectionPageNumberingFormat, + SectionPageSetup, + SectionRangeDomain, + SectionTargetInput, + SectionVerticalAlign, + SectionsClearHeaderFooterRefInput, + SectionsClearPageBordersInput, + SectionsGetInput, + SectionsListQuery, + SectionsListResult, + SectionsSetBreakTypeInput, + SectionsSetColumnsInput, + SectionsSetHeaderFooterMarginsInput, + SectionsSetHeaderFooterRefInput, + SectionsSetLineNumberingInput, + SectionsSetLinkToPreviousInput, + SectionsSetOddEvenHeadersFootersInput, + SectionsSetPageBordersInput, + SectionsSetPageMarginsInput, + SectionsSetPageNumberingInput, + SectionsSetPageSetupInput, + SectionsSetSectionDirectionInput, + SectionsSetTitlePageInput, + SectionsSetVerticalAlignInput, +} from './sections/sections.types.js'; export type { CommentsCreateInput, CommentsPatchInput, @@ -417,6 +518,10 @@ export interface DocumentApi { * List item operations. */ lists: ListsApi; + /** + * Section structure and page setup operations. + */ + sections: SectionsApi; /** * Table operations. */ @@ -464,6 +569,7 @@ export interface DocumentApiAdapters { create: CreateAdapter; blocks: BlocksAdapter; lists: ListsAdapter; + sections: SectionsAdapter; tables: TablesAdapter; query: QueryAdapter; mutations: MutationsAdapter; @@ -591,6 +697,9 @@ export function createDocumentApi(adapters: DocumentApiAdapters): DocumentApi { table(input: CreateTableInput, options?: MutationOptions): CreateTableResult { return executeCreateTable(adapters.create, input, options); }, + sectionBreak(input: CreateSectionBreakInput, options?: MutationOptions): CreateSectionBreakResult { + return executeCreateSectionBreak(adapters.create, input, options); + }, }, capabilities, lists: { @@ -619,6 +728,68 @@ export function createDocumentApi(adapters: DocumentApiAdapters): DocumentApi { return executeListsExit(adapters.lists, input, options); }, }, + sections: { + list(query?: SectionsListQuery): SectionsListResult { + return executeSectionsList(adapters.sections, query); + }, + get(input: SectionsGetInput): SectionInfo { + return executeSectionsGet(adapters.sections, input); + }, + setBreakType(input: SectionsSetBreakTypeInput, options?: MutationOptions): SectionMutationResult { + return executeSectionsSetBreakType(adapters.sections, input, options); + }, + setPageMargins(input: SectionsSetPageMarginsInput, options?: MutationOptions): SectionMutationResult { + return executeSectionsSetPageMargins(adapters.sections, input, options); + }, + setHeaderFooterMargins( + input: SectionsSetHeaderFooterMarginsInput, + options?: MutationOptions, + ): SectionMutationResult { + return executeSectionsSetHeaderFooterMargins(adapters.sections, input, options); + }, + setPageSetup(input: SectionsSetPageSetupInput, options?: MutationOptions): SectionMutationResult { + return executeSectionsSetPageSetup(adapters.sections, input, options); + }, + setColumns(input: SectionsSetColumnsInput, options?: MutationOptions): SectionMutationResult { + return executeSectionsSetColumns(adapters.sections, input, options); + }, + setLineNumbering(input: SectionsSetLineNumberingInput, options?: MutationOptions): SectionMutationResult { + return executeSectionsSetLineNumbering(adapters.sections, input, options); + }, + setPageNumbering(input: SectionsSetPageNumberingInput, options?: MutationOptions): SectionMutationResult { + return executeSectionsSetPageNumbering(adapters.sections, input, options); + }, + setTitlePage(input: SectionsSetTitlePageInput, options?: MutationOptions): SectionMutationResult { + return executeSectionsSetTitlePage(adapters.sections, input, options); + }, + setOddEvenHeadersFooters( + input: SectionsSetOddEvenHeadersFootersInput, + options?: MutationOptions, + ): DocumentMutationResult { + return executeSectionsSetOddEvenHeadersFooters(adapters.sections, input, options); + }, + setVerticalAlign(input: SectionsSetVerticalAlignInput, options?: MutationOptions): SectionMutationResult { + return executeSectionsSetVerticalAlign(adapters.sections, input, options); + }, + setSectionDirection(input: SectionsSetSectionDirectionInput, options?: MutationOptions): SectionMutationResult { + return executeSectionsSetSectionDirection(adapters.sections, input, options); + }, + setHeaderFooterRef(input: SectionsSetHeaderFooterRefInput, options?: MutationOptions): SectionMutationResult { + return executeSectionsSetHeaderFooterRef(adapters.sections, input, options); + }, + clearHeaderFooterRef(input: SectionsClearHeaderFooterRefInput, options?: MutationOptions): SectionMutationResult { + return executeSectionsClearHeaderFooterRef(adapters.sections, input, options); + }, + setLinkToPrevious(input: SectionsSetLinkToPreviousInput, options?: MutationOptions): SectionMutationResult { + return executeSectionsSetLinkToPrevious(adapters.sections, input, options); + }, + setPageBorders(input: SectionsSetPageBordersInput, options?: MutationOptions): SectionMutationResult { + return executeSectionsSetPageBorders(adapters.sections, input, options); + }, + clearPageBorders(input: SectionsClearPageBordersInput, options?: MutationOptions): SectionMutationResult { + return executeSectionsClearPageBorders(adapters.sections, input, options); + }, + }, tables: { convertFromText(input, options?) { return executeTableOperation( diff --git a/packages/document-api/src/invoke/invoke.ts b/packages/document-api/src/invoke/invoke.ts index 4eae8b5a3f..1e4997305a 100644 --- a/packages/document-api/src/invoke/invoke.ts +++ b/packages/document-api/src/invoke/invoke.ts @@ -60,6 +60,7 @@ export function buildDispatchTable(api: DocumentApi): TypedDispatchTable { // --- create.* --- 'create.paragraph': (input, options) => api.create.paragraph(input, options), 'create.heading': (input, options) => api.create.heading(input, options), + 'create.sectionBreak': (input, options) => api.create.sectionBreak(input, options), // --- lists.* --- 'lists.list': (input) => api.lists.list(input), @@ -71,6 +72,26 @@ export function buildDispatchTable(api: DocumentApi): TypedDispatchTable { 'lists.restart': (input, options) => api.lists.restart(input, options), 'lists.exit': (input, options) => api.lists.exit(input, options), + // --- sections.* --- + 'sections.list': (input) => api.sections.list(input), + 'sections.get': (input) => api.sections.get(input), + 'sections.setBreakType': (input, options) => api.sections.setBreakType(input, options), + 'sections.setPageMargins': (input, options) => api.sections.setPageMargins(input, options), + 'sections.setHeaderFooterMargins': (input, options) => api.sections.setHeaderFooterMargins(input, options), + 'sections.setPageSetup': (input, options) => api.sections.setPageSetup(input, options), + 'sections.setColumns': (input, options) => api.sections.setColumns(input, options), + 'sections.setLineNumbering': (input, options) => api.sections.setLineNumbering(input, options), + 'sections.setPageNumbering': (input, options) => api.sections.setPageNumbering(input, options), + 'sections.setTitlePage': (input, options) => api.sections.setTitlePage(input, options), + 'sections.setOddEvenHeadersFooters': (input, options) => api.sections.setOddEvenHeadersFooters(input, options), + 'sections.setVerticalAlign': (input, options) => api.sections.setVerticalAlign(input, options), + 'sections.setSectionDirection': (input, options) => api.sections.setSectionDirection(input, options), + 'sections.setHeaderFooterRef': (input, options) => api.sections.setHeaderFooterRef(input, options), + 'sections.clearHeaderFooterRef': (input, options) => api.sections.clearHeaderFooterRef(input, options), + 'sections.setLinkToPrevious': (input, options) => api.sections.setLinkToPrevious(input, options), + 'sections.setPageBorders': (input, options) => api.sections.setPageBorders(input, options), + 'sections.clearPageBorders': (input, options) => api.sections.clearPageBorders(input, options), + // --- comments.* --- 'comments.create': (input, options) => api.comments.create(input, options), 'comments.patch': (input, options) => api.comments.patch(input, options), diff --git a/packages/document-api/src/sections/sections.test.ts b/packages/document-api/src/sections/sections.test.ts new file mode 100644 index 0000000000..4172713db6 --- /dev/null +++ b/packages/document-api/src/sections/sections.test.ts @@ -0,0 +1,119 @@ +import { + executeSectionsList, + executeSectionsGet, + executeSectionsSetPageMargins, + executeSectionsSetPageNumbering, + executeSectionsSetPageBorders, + executeSectionsSetHeaderFooterRef, + executeSectionsSetOddEvenHeadersFooters, + type SectionsAdapter, +} from './sections.js'; + +function makeAdapter(overrides: Partial = {}): SectionsAdapter { + const base: SectionsAdapter = { + list: () => ({ + evaluatedRevision: '0', + total: 0, + items: [], + page: { limit: 250, offset: 0, returned: 0 }, + }), + get: () => ({ + address: { kind: 'section', sectionId: 'section-0' }, + index: 0, + range: { startParagraphIndex: 0, endParagraphIndex: 0 }, + }), + setBreakType: () => ({ success: true, section: { kind: 'section', sectionId: 'section-0' } }), + setPageMargins: () => ({ success: true, section: { kind: 'section', sectionId: 'section-0' } }), + setHeaderFooterMargins: () => ({ success: true, section: { kind: 'section', sectionId: 'section-0' } }), + setPageSetup: () => ({ success: true, section: { kind: 'section', sectionId: 'section-0' } }), + setColumns: () => ({ success: true, section: { kind: 'section', sectionId: 'section-0' } }), + setLineNumbering: () => ({ success: true, section: { kind: 'section', sectionId: 'section-0' } }), + setPageNumbering: () => ({ success: true, section: { kind: 'section', sectionId: 'section-0' } }), + setTitlePage: () => ({ success: true, section: { kind: 'section', sectionId: 'section-0' } }), + setOddEvenHeadersFooters: () => ({ success: true }), + setVerticalAlign: () => ({ success: true, section: { kind: 'section', sectionId: 'section-0' } }), + setSectionDirection: () => ({ success: true, section: { kind: 'section', sectionId: 'section-0' } }), + setHeaderFooterRef: () => ({ success: true, section: { kind: 'section', sectionId: 'section-0' } }), + clearHeaderFooterRef: () => ({ success: true, section: { kind: 'section', sectionId: 'section-0' } }), + setLinkToPrevious: () => ({ success: true, section: { kind: 'section', sectionId: 'section-0' } }), + setPageBorders: () => ({ success: true, section: { kind: 'section', sectionId: 'section-0' } }), + clearPageBorders: () => ({ success: true, section: { kind: 'section', sectionId: 'section-0' } }), + }; + + return { + ...base, + ...overrides, + }; +} + +describe('sections API validation', () => { + it('normalizes list defaults to limit=250 and offset=0', () => { + const list = vi.fn(makeAdapter().list); + const adapter = makeAdapter({ list }); + + executeSectionsList(adapter); + + expect(list).toHaveBeenCalledWith({ limit: 250, offset: 0 }); + }); + + it('rejects invalid list limit', () => { + const adapter = makeAdapter(); + expect(() => executeSectionsList(adapter, { limit: 0 })).toThrow(/limit must be a positive integer/i); + }); + + it('validates section address for sections.get', () => { + const adapter = makeAdapter(); + expect(() => executeSectionsGet(adapter, { address: { kind: 'node', sectionId: 'x' } as any })).toThrow( + /must be a section address/i, + ); + }); + + it('requires at least one field for setPageMargins', () => { + const adapter = makeAdapter(); + expect(() => + executeSectionsSetPageMargins(adapter, { + target: { kind: 'section', sectionId: 'section-0' }, + }), + ).toThrow(/requires at least one margin field/i); + }); + + it('rejects empty refId for setHeaderFooterRef', () => { + const adapter = makeAdapter(); + expect(() => + executeSectionsSetHeaderFooterRef(adapter, { + target: { kind: 'section', sectionId: 'section-0' }, + kind: 'header', + variant: 'default', + refId: ' ', + }), + ).toThrow(/must be a non-empty string/i); + }); + + it('accepts targetless odd/even settings mutation input', () => { + const setOddEvenHeadersFooters = vi.fn(makeAdapter().setOddEvenHeadersFooters); + const adapter = makeAdapter({ setOddEvenHeadersFooters }); + + executeSectionsSetOddEvenHeadersFooters(adapter, { enabled: true }, { dryRun: true }); + + expect(setOddEvenHeadersFooters).toHaveBeenCalledWith({ enabled: true }, { changeMode: 'direct', dryRun: true }); + }); + + it('requires at least one field for setPageNumbering', () => { + const adapter = makeAdapter(); + expect(() => + executeSectionsSetPageNumbering(adapter, { + target: { kind: 'section', sectionId: 'section-0' }, + }), + ).toThrow(/requires at least one of start or format/i); + }); + + it('requires at least one field for setPageBorders', () => { + const adapter = makeAdapter(); + expect(() => + executeSectionsSetPageBorders(adapter, { + target: { kind: 'section', sectionId: 'section-0' }, + borders: {}, + }), + ).toThrow(/requires at least one border field/i); + }); +}); diff --git a/packages/document-api/src/sections/sections.ts b/packages/document-api/src/sections/sections.ts new file mode 100644 index 0000000000..483a45610f --- /dev/null +++ b/packages/document-api/src/sections/sections.ts @@ -0,0 +1,497 @@ +import { DocumentApiValidationError } from '../errors.js'; +import { normalizeMutationOptions, type MutationOptions } from '../write/write.js'; +import { isRecord } from '../validation-primitives.js'; +import type { + DocumentMutationResult, + SectionAddress, + SectionMutationResult, + SectionBreakType, + SectionHeaderFooterKind, + SectionHeaderFooterVariant, + SectionDirection, + SectionOrientation, + SectionVerticalAlign, + SectionsClearHeaderFooterRefInput, + SectionsClearPageBordersInput, + SectionsGetInput, + SectionsListQuery, + SectionsListResult, + SectionsSetBreakTypeInput, + SectionsSetColumnsInput, + SectionsSetHeaderFooterMarginsInput, + SectionsSetHeaderFooterRefInput, + SectionsSetLineNumberingInput, + SectionsSetLinkToPreviousInput, + SectionsSetOddEvenHeadersFootersInput, + SectionsSetPageBordersInput, + SectionsSetPageMarginsInput, + SectionsSetPageNumberingInput, + SectionsSetPageSetupInput, + SectionsSetSectionDirectionInput, + SectionsSetTitlePageInput, + SectionsSetVerticalAlignInput, + SectionInfo, + SectionTargetInput, +} from './sections.types.js'; + +export type { + DocumentMutationResult, + SectionAddress, + SectionMutationResult, + SectionBreakType, + SectionHeaderFooterKind, + SectionHeaderFooterVariant, + SectionsClearHeaderFooterRefInput, + SectionsClearPageBordersInput, + SectionsGetInput, + SectionsListQuery, + SectionsListResult, + SectionsSetBreakTypeInput, + SectionsSetColumnsInput, + SectionsSetHeaderFooterMarginsInput, + SectionsSetHeaderFooterRefInput, + SectionsSetLineNumberingInput, + SectionsSetLinkToPreviousInput, + SectionsSetOddEvenHeadersFootersInput, + SectionsSetPageBordersInput, + SectionsSetPageMarginsInput, + SectionsSetPageNumberingInput, + SectionsSetPageSetupInput, + SectionsSetSectionDirectionInput, + SectionsSetTitlePageInput, + SectionsSetVerticalAlignInput, + SectionInfo, + SectionTargetInput, +} from './sections.types.js'; + +const DEFAULT_SECTIONS_LIST_LIMIT = 250; + +const SECTION_BREAK_TYPES: readonly SectionBreakType[] = ['continuous', 'nextPage', 'evenPage', 'oddPage'] as const; +const SECTION_ORIENTATIONS: readonly SectionOrientation[] = ['portrait', 'landscape'] as const; +const SECTION_VERTICAL_ALIGNS: readonly SectionVerticalAlign[] = ['top', 'center', 'bottom', 'both'] as const; +const SECTION_DIRECTIONS: readonly SectionDirection[] = ['ltr', 'rtl'] as const; +const HEADER_FOOTER_KINDS: readonly SectionHeaderFooterKind[] = ['header', 'footer'] as const; +const HEADER_FOOTER_VARIANTS: readonly SectionHeaderFooterVariant[] = ['default', 'first', 'even'] as const; +const LINE_NUMBER_RESTARTS = ['continuous', 'newPage', 'newSection'] as const; +const PAGE_NUMBER_FORMATS = [ + 'decimal', + 'lowerLetter', + 'upperLetter', + 'lowerRoman', + 'upperRoman', + 'numberInDash', +] as const; +const PAGE_BORDER_DISPLAYS = ['allPages', 'firstPage', 'notFirstPage'] as const; +const PAGE_BORDER_OFFSET_FROM_VALUES = ['page', 'text'] as const; +const PAGE_BORDER_Z_ORDER_VALUES = ['front', 'back'] as const; + +function assertSectionAddress(value: unknown, fieldName: string): asserts value is SectionAddress { + if ( + !isRecord(value) || + value.kind !== 'section' || + typeof value.sectionId !== 'string' || + value.sectionId.length === 0 + ) { + throw new DocumentApiValidationError('INVALID_TARGET', `${fieldName} must be a section address.`, { + field: fieldName, + value, + }); + } +} + +function assertSectionTarget(input: unknown, operationName: string): asserts input is SectionTargetInput { + if (!isRecord(input)) { + throw new DocumentApiValidationError('INVALID_TARGET', `${operationName} input must be an object.`); + } + assertSectionAddress(input.target, `${operationName}.target`); +} + +function assertBoolean(value: unknown, fieldName: string): asserts value is boolean { + if (typeof value !== 'boolean') { + throw new DocumentApiValidationError('INVALID_INPUT', `${fieldName} must be a boolean.`, { + field: fieldName, + value, + }); + } +} + +function assertString(value: unknown, fieldName: string): asserts value is string { + if (typeof value !== 'string') { + throw new DocumentApiValidationError('INVALID_INPUT', `${fieldName} must be a string.`, { + field: fieldName, + value, + }); + } +} + +function assertNonEmptyString(value: unknown, fieldName: string): asserts value is string { + assertString(value, fieldName); + if (value.trim().length === 0) { + throw new DocumentApiValidationError('INVALID_INPUT', `${fieldName} must be a non-empty string.`, { + field: fieldName, + value, + }); + } +} + +function assertPositiveInteger(value: unknown, fieldName: string): void { + if (!Number.isInteger(value) || Number(value) <= 0) { + throw new DocumentApiValidationError('INVALID_INPUT', `${fieldName} must be a positive integer.`, { + field: fieldName, + value, + }); + } +} + +function assertNonNegativeNumber(value: unknown, fieldName: string): void { + if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) { + throw new DocumentApiValidationError('INVALID_INPUT', `${fieldName} must be a non-negative number.`, { + field: fieldName, + value, + }); + } +} + +function assertOneOf(value: unknown, fieldName: string, allowed: readonly T[]): asserts value is T { + if (typeof value !== 'string' || !(allowed as readonly string[]).includes(value)) { + throw new DocumentApiValidationError('INVALID_INPUT', `${fieldName} must be one of: ${allowed.join(', ')}.`, { + field: fieldName, + value, + allowed, + }); + } +} + +function hasAnyDefined(value: Record, keys: readonly string[]): boolean { + return keys.some((key) => value[key] !== undefined); +} + +function assertObject(value: unknown, fieldName: string): asserts value is Record { + if (!isRecord(value)) { + throw new DocumentApiValidationError('INVALID_INPUT', `${fieldName} must be an object.`, { + field: fieldName, + value, + }); + } +} + +function validateBorderSpec(value: unknown, fieldName: string): void { + assertObject(value, fieldName); + if (!hasAnyDefined(value, ['style', 'size', 'space', 'color', 'shadow', 'frame'])) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${fieldName} must include at least one of style, size, space, color, shadow, or frame.`, + { field: fieldName, value }, + ); + } + if (value.style !== undefined) assertString(value.style, `${fieldName}.style`); + if (value.size !== undefined) assertNonNegativeNumber(value.size, `${fieldName}.size`); + if (value.space !== undefined) assertNonNegativeNumber(value.space, `${fieldName}.space`); + if (value.color !== undefined) assertString(value.color, `${fieldName}.color`); + if (value.shadow !== undefined) assertBoolean(value.shadow, `${fieldName}.shadow`); + if (value.frame !== undefined) assertBoolean(value.frame, `${fieldName}.frame`); +} + +function validatePageBorders(value: unknown, fieldName: string): void { + assertObject(value, fieldName); + if (!hasAnyDefined(value, ['display', 'offsetFrom', 'zOrder', 'top', 'right', 'bottom', 'left'])) { + throw new DocumentApiValidationError('INVALID_INPUT', `${fieldName} requires at least one border field.`, { + field: fieldName, + value, + }); + } + + if (value.display !== undefined) assertOneOf(value.display, `${fieldName}.display`, PAGE_BORDER_DISPLAYS); + if (value.offsetFrom !== undefined) { + assertOneOf(value.offsetFrom, `${fieldName}.offsetFrom`, PAGE_BORDER_OFFSET_FROM_VALUES); + } + if (value.zOrder !== undefined) assertOneOf(value.zOrder, `${fieldName}.zOrder`, PAGE_BORDER_Z_ORDER_VALUES); + + if (value.top !== undefined) validateBorderSpec(value.top, `${fieldName}.top`); + if (value.right !== undefined) validateBorderSpec(value.right, `${fieldName}.right`); + if (value.bottom !== undefined) validateBorderSpec(value.bottom, `${fieldName}.bottom`); + if (value.left !== undefined) validateBorderSpec(value.left, `${fieldName}.left`); +} + +function normalizeSectionsListQuery(query?: SectionsListQuery): Required { + const limit = query?.limit ?? DEFAULT_SECTIONS_LIST_LIMIT; + const offset = query?.offset ?? 0; + + if (!Number.isInteger(limit) || Number(limit) <= 0) { + throw new DocumentApiValidationError('INVALID_INPUT', 'sections.list limit must be a positive integer.', { + field: 'limit', + value: limit, + }); + } + + if (!Number.isInteger(offset) || Number(offset) < 0) { + throw new DocumentApiValidationError('INVALID_INPUT', 'sections.list offset must be a non-negative integer.', { + field: 'offset', + value: offset, + }); + } + + return { limit: Number(limit), offset: Number(offset) }; +} + +function validateHeaderFooterRefParams( + operationName: string, + kind: unknown, + variant: unknown, +): asserts kind is SectionHeaderFooterKind & string { + assertOneOf(kind, `${operationName}.kind`, HEADER_FOOTER_KINDS); + assertOneOf(variant, `${operationName}.variant`, HEADER_FOOTER_VARIANTS); +} + +export interface SectionsAdapter { + list(query?: SectionsListQuery): SectionsListResult; + get(input: SectionsGetInput): SectionInfo; + setBreakType(input: SectionsSetBreakTypeInput, options?: MutationOptions): SectionMutationResult; + setPageMargins(input: SectionsSetPageMarginsInput, options?: MutationOptions): SectionMutationResult; + setHeaderFooterMargins(input: SectionsSetHeaderFooterMarginsInput, options?: MutationOptions): SectionMutationResult; + setPageSetup(input: SectionsSetPageSetupInput, options?: MutationOptions): SectionMutationResult; + setColumns(input: SectionsSetColumnsInput, options?: MutationOptions): SectionMutationResult; + setLineNumbering(input: SectionsSetLineNumberingInput, options?: MutationOptions): SectionMutationResult; + setPageNumbering(input: SectionsSetPageNumberingInput, options?: MutationOptions): SectionMutationResult; + setTitlePage(input: SectionsSetTitlePageInput, options?: MutationOptions): SectionMutationResult; + setOddEvenHeadersFooters( + input: SectionsSetOddEvenHeadersFootersInput, + options?: MutationOptions, + ): DocumentMutationResult; + setVerticalAlign(input: SectionsSetVerticalAlignInput, options?: MutationOptions): SectionMutationResult; + setSectionDirection(input: SectionsSetSectionDirectionInput, options?: MutationOptions): SectionMutationResult; + setHeaderFooterRef(input: SectionsSetHeaderFooterRefInput, options?: MutationOptions): SectionMutationResult; + clearHeaderFooterRef(input: SectionsClearHeaderFooterRefInput, options?: MutationOptions): SectionMutationResult; + setLinkToPrevious(input: SectionsSetLinkToPreviousInput, options?: MutationOptions): SectionMutationResult; + setPageBorders(input: SectionsSetPageBordersInput, options?: MutationOptions): SectionMutationResult; + clearPageBorders(input: SectionsClearPageBordersInput, options?: MutationOptions): SectionMutationResult; +} + +export type SectionsApi = SectionsAdapter; + +export function executeSectionsList(adapter: SectionsAdapter, query?: SectionsListQuery): SectionsListResult { + return adapter.list(normalizeSectionsListQuery(query)); +} + +export function executeSectionsGet(adapter: SectionsAdapter, input: SectionsGetInput): SectionInfo { + assertSectionAddress(input?.address, 'sections.get.address'); + return adapter.get(input); +} + +export function executeSectionsSetBreakType( + adapter: SectionsAdapter, + input: SectionsSetBreakTypeInput, + options?: MutationOptions, +): SectionMutationResult { + assertSectionTarget(input, 'sections.setBreakType'); + assertOneOf(input.breakType, 'sections.setBreakType.breakType', SECTION_BREAK_TYPES); + return adapter.setBreakType(input, normalizeMutationOptions(options)); +} + +export function executeSectionsSetPageMargins( + adapter: SectionsAdapter, + input: SectionsSetPageMarginsInput, + options?: MutationOptions, +): SectionMutationResult { + assertSectionTarget(input, 'sections.setPageMargins'); + if (!hasAnyDefined(input as unknown as Record, ['top', 'right', 'bottom', 'left', 'gutter'])) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + 'sections.setPageMargins requires at least one margin field.', + ); + } + + if (input.top !== undefined) assertNonNegativeNumber(input.top, 'sections.setPageMargins.top'); + if (input.right !== undefined) assertNonNegativeNumber(input.right, 'sections.setPageMargins.right'); + if (input.bottom !== undefined) assertNonNegativeNumber(input.bottom, 'sections.setPageMargins.bottom'); + if (input.left !== undefined) assertNonNegativeNumber(input.left, 'sections.setPageMargins.left'); + if (input.gutter !== undefined) assertNonNegativeNumber(input.gutter, 'sections.setPageMargins.gutter'); + + return adapter.setPageMargins(input, normalizeMutationOptions(options)); +} + +export function executeSectionsSetHeaderFooterMargins( + adapter: SectionsAdapter, + input: SectionsSetHeaderFooterMarginsInput, + options?: MutationOptions, +): SectionMutationResult { + assertSectionTarget(input, 'sections.setHeaderFooterMargins'); + if (!hasAnyDefined(input as unknown as Record, ['header', 'footer'])) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + 'sections.setHeaderFooterMargins requires at least one margin field.', + ); + } + + if (input.header !== undefined) assertNonNegativeNumber(input.header, 'sections.setHeaderFooterMargins.header'); + if (input.footer !== undefined) assertNonNegativeNumber(input.footer, 'sections.setHeaderFooterMargins.footer'); + + return adapter.setHeaderFooterMargins(input, normalizeMutationOptions(options)); +} + +export function executeSectionsSetPageSetup( + adapter: SectionsAdapter, + input: SectionsSetPageSetupInput, + options?: MutationOptions, +): SectionMutationResult { + assertSectionTarget(input, 'sections.setPageSetup'); + if (!hasAnyDefined(input as unknown as Record, ['width', 'height', 'orientation', 'paperSize'])) { + throw new DocumentApiValidationError('INVALID_INPUT', 'sections.setPageSetup requires at least one setup field.'); + } + + if (input.width !== undefined) assertNonNegativeNumber(input.width, 'sections.setPageSetup.width'); + if (input.height !== undefined) assertNonNegativeNumber(input.height, 'sections.setPageSetup.height'); + if (input.orientation !== undefined) { + assertOneOf(input.orientation, 'sections.setPageSetup.orientation', SECTION_ORIENTATIONS); + } + if (input.paperSize !== undefined) assertNonEmptyString(input.paperSize, 'sections.setPageSetup.paperSize'); + + return adapter.setPageSetup(input, normalizeMutationOptions(options)); +} + +export function executeSectionsSetColumns( + adapter: SectionsAdapter, + input: SectionsSetColumnsInput, + options?: MutationOptions, +): SectionMutationResult { + assertSectionTarget(input, 'sections.setColumns'); + if (!hasAnyDefined(input as unknown as Record, ['count', 'gap', 'equalWidth'])) { + throw new DocumentApiValidationError('INVALID_INPUT', 'sections.setColumns requires at least one columns field.'); + } + + if (input.count !== undefined) assertPositiveInteger(input.count, 'sections.setColumns.count'); + if (input.gap !== undefined) assertNonNegativeNumber(input.gap, 'sections.setColumns.gap'); + if (input.equalWidth !== undefined) assertBoolean(input.equalWidth, 'sections.setColumns.equalWidth'); + + return adapter.setColumns(input, normalizeMutationOptions(options)); +} + +export function executeSectionsSetLineNumbering( + adapter: SectionsAdapter, + input: SectionsSetLineNumberingInput, + options?: MutationOptions, +): SectionMutationResult { + assertSectionTarget(input, 'sections.setLineNumbering'); + assertBoolean(input.enabled, 'sections.setLineNumbering.enabled'); + + if (input.countBy !== undefined) assertPositiveInteger(input.countBy, 'sections.setLineNumbering.countBy'); + if (input.start !== undefined) assertPositiveInteger(input.start, 'sections.setLineNumbering.start'); + if (input.distance !== undefined) assertNonNegativeNumber(input.distance, 'sections.setLineNumbering.distance'); + if (input.restart !== undefined) { + assertOneOf(input.restart, 'sections.setLineNumbering.restart', LINE_NUMBER_RESTARTS); + } + + return adapter.setLineNumbering(input, normalizeMutationOptions(options)); +} + +export function executeSectionsSetPageNumbering( + adapter: SectionsAdapter, + input: SectionsSetPageNumberingInput, + options?: MutationOptions, +): SectionMutationResult { + assertSectionTarget(input, 'sections.setPageNumbering'); + if (!hasAnyDefined(input as unknown as Record, ['start', 'format'])) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + 'sections.setPageNumbering requires at least one of start or format.', + ); + } + + if (input.start !== undefined) assertPositiveInteger(input.start, 'sections.setPageNumbering.start'); + if (input.format !== undefined) { + assertOneOf(input.format, 'sections.setPageNumbering.format', PAGE_NUMBER_FORMATS); + } + + return adapter.setPageNumbering(input, normalizeMutationOptions(options)); +} + +export function executeSectionsSetTitlePage( + adapter: SectionsAdapter, + input: SectionsSetTitlePageInput, + options?: MutationOptions, +): SectionMutationResult { + assertSectionTarget(input, 'sections.setTitlePage'); + assertBoolean(input.enabled, 'sections.setTitlePage.enabled'); + return adapter.setTitlePage(input, normalizeMutationOptions(options)); +} + +export function executeSectionsSetOddEvenHeadersFooters( + adapter: SectionsAdapter, + input: SectionsSetOddEvenHeadersFootersInput, + options?: MutationOptions, +): DocumentMutationResult { + assertBoolean(input?.enabled, 'sections.setOddEvenHeadersFooters.enabled'); + return adapter.setOddEvenHeadersFooters(input, normalizeMutationOptions(options)); +} + +export function executeSectionsSetVerticalAlign( + adapter: SectionsAdapter, + input: SectionsSetVerticalAlignInput, + options?: MutationOptions, +): SectionMutationResult { + assertSectionTarget(input, 'sections.setVerticalAlign'); + assertOneOf(input.value, 'sections.setVerticalAlign.value', SECTION_VERTICAL_ALIGNS); + return adapter.setVerticalAlign(input, normalizeMutationOptions(options)); +} + +export function executeSectionsSetSectionDirection( + adapter: SectionsAdapter, + input: SectionsSetSectionDirectionInput, + options?: MutationOptions, +): SectionMutationResult { + assertSectionTarget(input, 'sections.setSectionDirection'); + assertOneOf(input.direction, 'sections.setSectionDirection.direction', SECTION_DIRECTIONS); + return adapter.setSectionDirection(input, normalizeMutationOptions(options)); +} + +export function executeSectionsSetHeaderFooterRef( + adapter: SectionsAdapter, + input: SectionsSetHeaderFooterRefInput, + options?: MutationOptions, +): SectionMutationResult { + assertSectionTarget(input, 'sections.setHeaderFooterRef'); + validateHeaderFooterRefParams('sections.setHeaderFooterRef', input.kind, input.variant); + assertNonEmptyString(input.refId, 'sections.setHeaderFooterRef.refId'); + return adapter.setHeaderFooterRef(input, normalizeMutationOptions(options)); +} + +export function executeSectionsClearHeaderFooterRef( + adapter: SectionsAdapter, + input: SectionsClearHeaderFooterRefInput, + options?: MutationOptions, +): SectionMutationResult { + assertSectionTarget(input, 'sections.clearHeaderFooterRef'); + validateHeaderFooterRefParams('sections.clearHeaderFooterRef', input.kind, input.variant); + return adapter.clearHeaderFooterRef(input, normalizeMutationOptions(options)); +} + +export function executeSectionsSetLinkToPrevious( + adapter: SectionsAdapter, + input: SectionsSetLinkToPreviousInput, + options?: MutationOptions, +): SectionMutationResult { + assertSectionTarget(input, 'sections.setLinkToPrevious'); + validateHeaderFooterRefParams('sections.setLinkToPrevious', input.kind, input.variant); + assertBoolean(input.linked, 'sections.setLinkToPrevious.linked'); + return adapter.setLinkToPrevious(input, normalizeMutationOptions(options)); +} + +export function executeSectionsSetPageBorders( + adapter: SectionsAdapter, + input: SectionsSetPageBordersInput, + options?: MutationOptions, +): SectionMutationResult { + assertSectionTarget(input, 'sections.setPageBorders'); + validatePageBorders(input.borders, 'sections.setPageBorders.borders'); + + return adapter.setPageBorders(input, normalizeMutationOptions(options)); +} + +export function executeSectionsClearPageBorders( + adapter: SectionsAdapter, + input: SectionsClearPageBordersInput, + options?: MutationOptions, +): SectionMutationResult { + assertSectionTarget(input, 'sections.clearPageBorders'); + return adapter.clearPageBorders(input, normalizeMutationOptions(options)); +} diff --git a/packages/document-api/src/sections/sections.types.ts b/packages/document-api/src/sections/sections.types.ts new file mode 100644 index 0000000000..ad6a9f3647 --- /dev/null +++ b/packages/document-api/src/sections/sections.types.ts @@ -0,0 +1,269 @@ +import type { BlockNodeAddress, ReceiptFailure } from '../types/index.js'; +import type { DiscoveryOutput } from '../types/discovery.js'; + +export type SectionAddress = { + kind: 'section'; + sectionId: string; +}; + +export type SectionTargetInput = { + target: SectionAddress; +}; + +export type SectionBreakType = 'continuous' | 'nextPage' | 'evenPage' | 'oddPage'; + +export type SectionHeaderFooterKind = 'header' | 'footer'; + +/** + * Word models odd-page variants via the default header/footer reference when + * even/odd mode is enabled, so the API keeps this enum aligned with OOXML refs. + */ +export type SectionHeaderFooterVariant = 'default' | 'first' | 'even'; + +export type SectionOrientation = 'portrait' | 'landscape'; + +export type SectionVerticalAlign = 'top' | 'center' | 'bottom' | 'both'; + +export type SectionDirection = 'ltr' | 'rtl'; + +export type SectionLineNumberRestart = 'continuous' | 'newPage' | 'newSection'; + +export type SectionPageNumberingFormat = + | 'decimal' + | 'lowerLetter' + | 'upperLetter' + | 'lowerRoman' + | 'upperRoman' + | 'numberInDash'; + +export interface SectionPageMargins { + top?: number; + right?: number; + bottom?: number; + left?: number; + gutter?: number; +} + +export interface SectionHeaderFooterMargins { + header?: number; + footer?: number; +} + +export interface SectionPageSetup { + width?: number; + height?: number; + orientation?: SectionOrientation; + paperSize?: string; +} + +export interface SectionColumns { + count?: number; + gap?: number; + equalWidth?: boolean; +} + +export interface SectionLineNumbering { + enabled: boolean; + countBy?: number; + start?: number; + distance?: number; + restart?: SectionLineNumberRestart; +} + +export interface SectionPageNumbering { + start?: number; + format?: SectionPageNumberingFormat; +} + +export interface SectionHeaderFooterRefs { + default?: string; + first?: string; + even?: string; +} + +export interface SectionBorderSpec { + style?: string; + size?: number; + space?: number; + color?: string; + shadow?: boolean; + frame?: boolean; +} + +export interface SectionPageBorders { + display?: 'allPages' | 'firstPage' | 'notFirstPage'; + offsetFrom?: 'page' | 'text'; + zOrder?: 'front' | 'back'; + top?: SectionBorderSpec; + right?: SectionBorderSpec; + bottom?: SectionBorderSpec; + left?: SectionBorderSpec; +} + +export interface SectionRangeDomain { + startParagraphIndex: number; + endParagraphIndex: number; +} + +export interface SectionDomain { + address: SectionAddress; + index: number; + range: SectionRangeDomain; + breakType?: SectionBreakType; + pageSetup?: SectionPageSetup; + margins?: SectionPageMargins; + headerFooterMargins?: SectionHeaderFooterMargins; + columns?: SectionColumns; + lineNumbering?: SectionLineNumbering; + pageNumbering?: SectionPageNumbering; + titlePage?: boolean; + oddEvenHeadersFooters?: boolean; + verticalAlign?: SectionVerticalAlign; + sectionDirection?: SectionDirection; + headerRefs?: SectionHeaderFooterRefs; + footerRefs?: SectionHeaderFooterRefs; + pageBorders?: SectionPageBorders; +} + +export type SectionInfo = SectionDomain; + +export interface SectionsListQuery { + limit?: number; + offset?: number; +} + +export interface SectionsGetInput { + address: SectionAddress; +} + +export type SectionsListResult = DiscoveryOutput; + +export interface SectionMutationSuccessResult { + success: true; + section: SectionAddress; +} + +export interface SectionMutationFailureResult { + success: false; + failure: ReceiptFailure; +} + +export type SectionMutationResult = SectionMutationSuccessResult | SectionMutationFailureResult; + +/** + * Mutation receipt for document-scoped section settings operations that do not + * target a specific section address. + */ +export interface DocumentMutationSuccessResult { + success: true; +} + +export type DocumentMutationResult = DocumentMutationSuccessResult | SectionMutationFailureResult; + +export interface CreateSectionBreakSuccessResult { + success: true; + section: SectionAddress; + breakParagraph?: BlockNodeAddress; +} + +export interface CreateSectionBreakFailureResult { + success: false; + failure: ReceiptFailure; +} + +export type CreateSectionBreakResult = CreateSectionBreakSuccessResult | CreateSectionBreakFailureResult; + +export type SectionBreakCreateLocation = + | { kind: 'documentStart' } + | { kind: 'documentEnd' } + | { kind: 'before'; target: BlockNodeAddress } + | { kind: 'after'; target: BlockNodeAddress }; + +export interface CreateSectionBreakInput { + at?: SectionBreakCreateLocation; + breakType?: SectionBreakType; + pageMargins?: SectionPageMargins; + headerFooterMargins?: SectionHeaderFooterMargins; +} + +export interface SectionsSetBreakTypeInput extends SectionTargetInput { + breakType: SectionBreakType; +} + +export interface SectionsSetPageMarginsInput extends SectionTargetInput { + top?: number; + right?: number; + bottom?: number; + left?: number; + gutter?: number; +} + +export interface SectionsSetHeaderFooterMarginsInput extends SectionTargetInput { + header?: number; + footer?: number; +} + +export interface SectionsSetPageSetupInput extends SectionTargetInput { + width?: number; + height?: number; + orientation?: SectionOrientation; + paperSize?: string; +} + +export interface SectionsSetColumnsInput extends SectionTargetInput { + count?: number; + gap?: number; + equalWidth?: boolean; +} + +export interface SectionsSetLineNumberingInput extends SectionTargetInput { + enabled: boolean; + countBy?: number; + start?: number; + distance?: number; + restart?: SectionLineNumberRestart; +} + +export interface SectionsSetPageNumberingInput extends SectionTargetInput { + start?: number; + format?: SectionPageNumberingFormat; +} + +export interface SectionsSetTitlePageInput extends SectionTargetInput { + enabled: boolean; +} + +export interface SectionsSetOddEvenHeadersFootersInput { + enabled: boolean; +} + +export interface SectionsSetVerticalAlignInput extends SectionTargetInput { + value: SectionVerticalAlign; +} + +export interface SectionsSetSectionDirectionInput extends SectionTargetInput { + direction: SectionDirection; +} + +export interface SectionsSetHeaderFooterRefInput extends SectionTargetInput { + kind: SectionHeaderFooterKind; + variant: SectionHeaderFooterVariant; + refId: string; +} + +export interface SectionsClearHeaderFooterRefInput extends SectionTargetInput { + kind: SectionHeaderFooterKind; + variant: SectionHeaderFooterVariant; +} + +export interface SectionsSetLinkToPreviousInput extends SectionTargetInput { + kind: SectionHeaderFooterKind; + variant: SectionHeaderFooterVariant; + linked: boolean; +} + +export interface SectionsSetPageBordersInput extends SectionTargetInput { + borders: SectionPageBorders; +} + +export type SectionsClearPageBordersInput = SectionTargetInput; diff --git a/packages/layout-engine/pm-adapter/package.json b/packages/layout-engine/pm-adapter/package.json index 4a873418d7..abc0a590cd 100644 --- a/packages/layout-engine/pm-adapter/package.json +++ b/packages/layout-engine/pm-adapter/package.json @@ -11,8 +11,15 @@ "types": "./src/index.ts", "default": "./src/index.ts" }, + "./*.js": { + "types": "./src/*.ts", + "source": "./src/*.ts", + "default": "./src/*.ts" + }, "./*": { - "source": "./src/*" + "types": "./src/*", + "source": "./src/*", + "default": "./src/*" } }, "scripts": { 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 30e7234780..29c8abcd9f 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 @@ -80,6 +80,25 @@ import { executePlan } from '../plan-engine/executor.js'; import { toCanonicalTrackedChangeId } from '../helpers/tracked-change-resolver.js'; import { writeAdapter } from '../write-adapter.js'; import { tablesGetCellsAdapter, tablesGetPropertiesAdapter } from '../tables-adapter.js'; +import { + createSectionBreakAdapter, + sectionsSetBreakTypeAdapter, + sectionsSetPageMarginsAdapter, + sectionsSetHeaderFooterMarginsAdapter, + sectionsSetPageSetupAdapter, + sectionsSetColumnsAdapter, + sectionsSetLineNumberingAdapter, + sectionsSetPageNumberingAdapter, + sectionsSetTitlePageAdapter, + sectionsSetOddEvenHeadersFootersAdapter, + sectionsSetVerticalAlignAdapter, + sectionsSetSectionDirectionAdapter, + sectionsSetHeaderFooterRefAdapter, + sectionsClearHeaderFooterRefAdapter, + sectionsSetLinkToPreviousAdapter, + sectionsSetPageBordersAdapter, + sectionsClearPageBordersAdapter, +} from '../sections-adapter.js'; import { validateJsonSchema } from './schema-validator.js'; const mockedDeps = vi.hoisted(() => ({ @@ -857,6 +876,247 @@ function makeTableEditor( } as unknown as Editor; } +type SectionEditorOptions = { + bodySectPr?: Record | null; + paragraphSectPr?: Record | null; + includeConverter?: boolean; + throwOnInsert?: boolean; + includeParagraphNodeType?: boolean; +}; + +const BASE_SECTION_BODY_SECT_PR: Record = { + type: 'element', + name: 'w:sectPr', + elements: [ + { type: 'element', name: 'w:type', attributes: { 'w:val': 'continuous' } }, + { + type: 'element', + name: 'w:pgMar', + attributes: { + 'w:top': '1440', + 'w:right': '1440', + 'w:bottom': '1440', + 'w:left': '1440', + 'w:gutter': '0', + 'w:header': '720', + 'w:footer': '720', + }, + }, + { + type: 'element', + name: 'w:pgSz', + attributes: { + 'w:w': '12240', + 'w:h': '15840', + 'w:orient': 'portrait', + 'w:code': '1', + }, + }, + { + type: 'element', + name: 'w:cols', + attributes: { 'w:num': '1', 'w:space': '720', 'w:equalWidth': '1' }, + }, + { + type: 'element', + name: 'w:lnNumType', + attributes: { 'w:countBy': '1', 'w:start': '1', 'w:distance': '720', 'w:restart': 'continuous' }, + }, + { + type: 'element', + name: 'w:pgNumType', + attributes: { 'w:start': '1', 'w:fmt': 'decimal' }, + }, + { type: 'element', name: 'w:titlePg', elements: [] }, + { type: 'element', name: 'w:vAlign', attributes: { 'w:val': 'top' } }, + { + type: 'element', + name: 'w:headerReference', + attributes: { 'w:type': 'default', 'r:id': 'rIdHeaderDefault' }, + }, + { + type: 'element', + name: 'w:footerReference', + attributes: { 'w:type': 'default', 'r:id': 'rIdFooterDefault' }, + }, + { + type: 'element', + name: 'w:pgBorders', + attributes: { 'w:display': 'allPages', 'w:offsetFrom': 'page', 'w:zOrder': 'front' }, + elements: [ + { + type: 'element', + name: 'w:top', + attributes: { 'w:val': 'single', 'w:sz': '8', 'w:space': '0', 'w:color': '000000' }, + }, + ], + }, + ], +}; + +const PREVIOUS_SECTION_SECT_PR: Record = { + type: 'element', + name: 'w:sectPr', + elements: [ + { type: 'element', name: 'w:type', attributes: { 'w:val': 'nextPage' } }, + { + type: 'element', + name: 'w:headerReference', + attributes: { 'w:type': 'default', 'r:id': 'rIdPrevHeader' }, + }, + { + type: 'element', + name: 'w:footerReference', + attributes: { 'w:type': 'default', 'r:id': 'rIdPrevFooter' }, + }, + ], +}; + +function clone(value: T): T { + return JSON.parse(JSON.stringify(value)) as T; +} + +function makeSectionsEditor(options: SectionEditorOptions = {}): Editor { + const { + bodySectPr = BASE_SECTION_BODY_SECT_PR, + paragraphSectPr = null, + includeConverter = true, + throwOnInsert = false, + includeParagraphNodeType = true, + } = options; + + const paragraphAttrs: Record = { + sdBlockId: 'p1', + paraId: 'p1', + paragraphProperties: paragraphSectPr ? { sectPr: clone(paragraphSectPr) } : {}, + }; + const paragraphNode = createNode('paragraph', [createNode('text', [], { text: 'Section text' })], { + attrs: paragraphAttrs, + isBlock: true, + inlineContent: true, + }); + + const docAttrs: Record = {}; + if (bodySectPr) { + docAttrs.bodySectPr = clone(bodySectPr); + } + + const doc = createNode('doc', [paragraphNode], { + attrs: docAttrs, + isBlock: false, + }) as unknown as ProseMirrorNode & { toJSON?: () => unknown }; + + const docJson = { + type: 'doc', + attrs: docAttrs, + content: [ + { + type: 'paragraph', + attrs: paragraphAttrs, + }, + ], + }; + doc.toJSON = () => clone(docJson); + + const tr = { + insert: vi.fn(function insert() { + if (throwOnInsert) { + throw new Error('insert failed'); + } + return tr; + }), + setNodeMarkup: vi.fn(() => tr), + setMeta: vi.fn(() => tr), + mapping: { + maps: [] as unknown[], + map: (position: number) => position, + slice: () => ({ map: (position: number) => position }), + }, + doc, + }; + + const schemaNodes = includeParagraphNodeType + ? { + paragraph: { + createAndFill: vi.fn((attrs?: Record) => + createNode('paragraph', [], { attrs: attrs ?? {}, isBlock: true, inlineContent: true }), + ), + create: vi.fn((attrs?: Record) => + createNode('paragraph', [], { attrs: attrs ?? {}, isBlock: true, inlineContent: true }), + ), + }, + } + : {}; + + const editor = { + state: { + doc, + tr, + schema: { + nodes: schemaNodes, + }, + }, + dispatch: vi.fn(), + commands: {}, + schema: { marks: {}, nodes: schemaNodes }, + options: {}, + } as unknown as Editor; + + if (includeConverter) { + (editor as unknown as { converter?: Record }).converter = { + bodySectPr: bodySectPr ? clone(bodySectPr) : undefined, + convertedXml: { + 'word/settings.xml': { + type: 'element', + name: 'document', + elements: [{ type: 'element', name: 'w:settings', elements: [] }], + }, + 'word/_rels/document.xml.rels': { + elements: [ + { + type: 'element', + name: 'Relationships', + attributes: { xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' }, + elements: [ + { + type: 'element', + name: 'Relationship', + attributes: { + Id: 'rIdHeaderDefault', + Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/header', + Target: 'header1.xml', + }, + }, + { + type: 'element', + name: 'Relationship', + attributes: { + Id: 'rIdFooterDefault', + Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer', + Target: 'footer1.xml', + }, + }, + { + type: 'element', + name: 'Relationship', + attributes: { + Id: 'rIdHeaderAlt', + Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/header', + Target: 'header2.xml', + }, + }, + ], + }, + ], + }, + }, + pageStyles: {}, + }; + } + + return editor; +} + /** Table operation IDs that are actually implemented (not stubs). */ const IMPLEMENTED_TABLE_OPS: ReadonlySet = new Set([ 'create.table', @@ -1222,6 +1482,491 @@ const mutationVectors: Partial> = { ); }, }, + 'create.sectionBreak': { + throwCase: () => { + const editor = makeSectionsEditor({ includeParagraphNodeType: false }); + return createSectionBreakAdapter(editor, { at: { kind: 'documentEnd' } }, { changeMode: 'direct' }); + }, + failureCase: () => { + const editor = makeSectionsEditor({ throwOnInsert: true }); + return createSectionBreakAdapter( + editor, + { at: { kind: 'documentEnd' }, breakType: 'nextPage' }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const editor = makeSectionsEditor(); + return createSectionBreakAdapter( + editor, + { at: { kind: 'documentEnd' }, breakType: 'nextPage' }, + { changeMode: 'direct' }, + ); + }, + }, + 'sections.setBreakType': { + throwCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetBreakTypeAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-missing' }, breakType: 'continuous' }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetBreakTypeAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, breakType: 'continuous' }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetBreakTypeAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, breakType: 'nextPage' }, + { changeMode: 'direct' }, + ); + }, + }, + 'sections.setPageMargins': { + throwCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetPageMarginsAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-missing' }, top: 1 }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetPageMarginsAdapter( + editor, + { + target: { kind: 'section', sectionId: 'section-0' }, + top: 1, + right: 1, + bottom: 1, + left: 1, + gutter: 0, + }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetPageMarginsAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, top: 1.25 }, + { changeMode: 'direct' }, + ); + }, + }, + 'sections.setHeaderFooterMargins': { + throwCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetHeaderFooterMarginsAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-missing' }, header: 0.5 }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetHeaderFooterMarginsAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, header: 0.5, footer: 0.5 }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetHeaderFooterMarginsAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, header: 0.75 }, + { changeMode: 'direct' }, + ); + }, + }, + 'sections.setPageSetup': { + throwCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetPageSetupAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-missing' }, orientation: 'portrait' }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetPageSetupAdapter( + editor, + { + target: { kind: 'section', sectionId: 'section-0' }, + width: 8.5, + height: 11, + orientation: 'portrait', + paperSize: '1', + }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetPageSetupAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, orientation: 'landscape' }, + { changeMode: 'direct' }, + ); + }, + }, + 'sections.setColumns': { + throwCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetColumnsAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-missing' }, count: 1 }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetColumnsAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, count: 1, gap: 0.5, equalWidth: true }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetColumnsAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, count: 2 }, + { changeMode: 'direct' }, + ); + }, + }, + 'sections.setLineNumbering': { + throwCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetLineNumberingAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-missing' }, enabled: true }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetLineNumberingAdapter( + editor, + { + target: { kind: 'section', sectionId: 'section-0' }, + enabled: true, + countBy: 1, + start: 1, + distance: 0.5, + restart: 'continuous', + }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetLineNumberingAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, enabled: false }, + { changeMode: 'direct' }, + ); + }, + }, + 'sections.setPageNumbering': { + throwCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetPageNumberingAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-missing' }, start: 1 }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetPageNumberingAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, start: 1, format: 'decimal' }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetPageNumberingAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, start: 2 }, + { changeMode: 'direct' }, + ); + }, + }, + 'sections.setTitlePage': { + throwCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetTitlePageAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-missing' }, enabled: true }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetTitlePageAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, enabled: true }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetTitlePageAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, enabled: false }, + { changeMode: 'direct' }, + ); + }, + }, + 'sections.setOddEvenHeadersFooters': { + throwCase: () => { + const editor = makeSectionsEditor({ includeConverter: false }); + return sectionsSetOddEvenHeadersFootersAdapter(editor, { enabled: true }, { changeMode: 'direct' }); + }, + failureCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetOddEvenHeadersFootersAdapter(editor, { enabled: false }, { changeMode: 'direct' }); + }, + applyCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetOddEvenHeadersFootersAdapter(editor, { enabled: true }, { changeMode: 'direct' }); + }, + }, + 'sections.setVerticalAlign': { + throwCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetVerticalAlignAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-missing' }, value: 'top' }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetVerticalAlignAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, value: 'top' }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetVerticalAlignAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, value: 'center' }, + { changeMode: 'direct' }, + ); + }, + }, + 'sections.setSectionDirection': { + throwCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetSectionDirectionAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-missing' }, direction: 'ltr' }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetSectionDirectionAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, direction: 'ltr' }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetSectionDirectionAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, direction: 'rtl' }, + { changeMode: 'direct' }, + ); + }, + }, + 'sections.setHeaderFooterRef': { + throwCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetHeaderFooterRefAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-missing' }, kind: 'header', variant: 'default', refId: 'x' }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetHeaderFooterRefAdapter( + editor, + { + target: { kind: 'section', sectionId: 'section-0' }, + kind: 'header', + variant: 'default', + refId: 'rIdHeaderDefault', + }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetHeaderFooterRefAdapter( + editor, + { + target: { kind: 'section', sectionId: 'section-0' }, + kind: 'header', + variant: 'default', + refId: 'rIdHeaderAlt', + }, + { changeMode: 'direct' }, + ); + }, + }, + 'sections.clearHeaderFooterRef': { + throwCase: () => { + const editor = makeSectionsEditor(); + return sectionsClearHeaderFooterRefAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-missing' }, kind: 'header', variant: 'default' }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const editor = makeSectionsEditor(); + return sectionsClearHeaderFooterRefAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, kind: 'header', variant: 'even' }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const editor = makeSectionsEditor(); + return sectionsClearHeaderFooterRefAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, kind: 'header', variant: 'default' }, + { changeMode: 'direct' }, + ); + }, + }, + 'sections.setLinkToPrevious': { + throwCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetLinkToPreviousAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-missing' }, kind: 'header', variant: 'default', linked: true }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetLinkToPreviousAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, kind: 'header', variant: 'default', linked: true }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const bodyWithoutRefs = clone(BASE_SECTION_BODY_SECT_PR); + const filteredBodyElements = ((bodyWithoutRefs.elements ?? []) as Array<{ name?: string }>).filter( + (element) => element.name !== 'w:headerReference' && element.name !== 'w:footerReference', + ); + bodyWithoutRefs.elements = filteredBodyElements as unknown as Record[]; + + const editor = makeSectionsEditor({ + paragraphSectPr: PREVIOUS_SECTION_SECT_PR, + bodySectPr: bodyWithoutRefs, + }); + return sectionsSetLinkToPreviousAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-1' }, kind: 'header', variant: 'default', linked: false }, + { changeMode: 'direct' }, + ); + }, + }, + 'sections.setPageBorders': { + throwCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetPageBordersAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-missing' }, borders: {} }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetPageBordersAdapter( + editor, + { + target: { kind: 'section', sectionId: 'section-0' }, + borders: { + display: 'allPages', + offsetFrom: 'page', + zOrder: 'front', + top: { style: 'single', size: 8, space: 0, color: '000000' }, + }, + }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const editor = makeSectionsEditor(); + return sectionsSetPageBordersAdapter( + editor, + { + target: { kind: 'section', sectionId: 'section-0' }, + borders: { + display: 'allPages', + offsetFrom: 'page', + zOrder: 'front', + top: { style: 'double', size: 12, space: 0, color: '000000' }, + }, + }, + { changeMode: 'direct' }, + ); + }, + }, + 'sections.clearPageBorders': { + throwCase: () => { + const editor = makeSectionsEditor(); + return sectionsClearPageBordersAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-missing' } }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const bodyWithoutBorders = clone(BASE_SECTION_BODY_SECT_PR); + bodyWithoutBorders.elements = ((bodyWithoutBorders.elements ?? []) as Array<{ name?: string }>).filter( + (element) => element.name !== 'w:pgBorders', + ) as unknown as Record[]; + const editor = makeSectionsEditor({ bodySectPr: bodyWithoutBorders }); + return sectionsClearPageBordersAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' } }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const editor = makeSectionsEditor(); + return sectionsClearPageBordersAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' } }, + { changeMode: 'direct' }, + ); + }, + }, 'lists.insert': { throwCase: () => { const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, numberingType: 'decimal' })]); @@ -2268,6 +3013,213 @@ const dryRunVectors: Partial unknown>> = { expect(insertHeadingAt).not.toHaveBeenCalled(); return result; }, + 'create.sectionBreak': () => { + const editor = makeSectionsEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = createSectionBreakAdapter( + editor, + { at: { kind: 'documentEnd' }, breakType: 'nextPage' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'sections.setBreakType': () => { + const editor = makeSectionsEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = sectionsSetBreakTypeAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, breakType: 'nextPage' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'sections.setPageMargins': () => { + const editor = makeSectionsEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = sectionsSetPageMarginsAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, top: 1.25 }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'sections.setHeaderFooterMargins': () => { + const editor = makeSectionsEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = sectionsSetHeaderFooterMarginsAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, header: 0.75 }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'sections.setPageSetup': () => { + const editor = makeSectionsEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = sectionsSetPageSetupAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, orientation: 'landscape' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'sections.setColumns': () => { + const editor = makeSectionsEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = sectionsSetColumnsAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, count: 2 }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'sections.setLineNumbering': () => { + const editor = makeSectionsEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = sectionsSetLineNumberingAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, enabled: false }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'sections.setPageNumbering': () => { + const editor = makeSectionsEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = sectionsSetPageNumberingAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, start: 2 }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'sections.setTitlePage': () => { + const editor = makeSectionsEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = sectionsSetTitlePageAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, enabled: false }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'sections.setOddEvenHeadersFooters': () => { + const editor = makeSectionsEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = sectionsSetOddEvenHeadersFootersAdapter( + editor, + { enabled: true }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'sections.setVerticalAlign': () => { + const editor = makeSectionsEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = sectionsSetVerticalAlignAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, value: 'center' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'sections.setSectionDirection': () => { + const editor = makeSectionsEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = sectionsSetSectionDirectionAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, direction: 'rtl' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'sections.setHeaderFooterRef': () => { + const editor = makeSectionsEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = sectionsSetHeaderFooterRefAdapter( + editor, + { + target: { kind: 'section', sectionId: 'section-0' }, + kind: 'header', + variant: 'default', + refId: 'rIdHeaderAlt', + }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'sections.clearHeaderFooterRef': () => { + const editor = makeSectionsEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = sectionsClearHeaderFooterRefAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' }, kind: 'header', variant: 'default' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'sections.setLinkToPrevious': () => { + const bodyWithoutRefs = clone(BASE_SECTION_BODY_SECT_PR); + bodyWithoutRefs.elements = ((bodyWithoutRefs.elements ?? []) as Array<{ name?: string }>).filter( + (element) => element.name !== 'w:headerReference' && element.name !== 'w:footerReference', + ) as unknown as Record[]; + const editor = makeSectionsEditor({ + paragraphSectPr: PREVIOUS_SECTION_SECT_PR, + bodySectPr: bodyWithoutRefs, + }); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = sectionsSetLinkToPreviousAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-1' }, kind: 'header', variant: 'default', linked: false }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'sections.setPageBorders': () => { + const editor = makeSectionsEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = sectionsSetPageBordersAdapter( + editor, + { + target: { kind: 'section', sectionId: 'section-0' }, + borders: { + display: 'allPages', + offsetFrom: 'page', + zOrder: 'front', + top: { style: 'double', size: 12, space: 0, color: '000000' }, + }, + }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'sections.clearPageBorders': () => { + const editor = makeSectionsEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = sectionsClearPageBordersAdapter( + editor, + { target: { kind: 'section', sectionId: 'section-0' } }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, 'lists.insert': () => { const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, numberingType: 'decimal' })]); const insertListItemAt = editor.commands!.insertListItemAt as ReturnType; diff --git a/packages/super-editor/src/document-api-adapters/assemble-adapters.test.ts b/packages/super-editor/src/document-api-adapters/assemble-adapters.test.ts index 88b601ddc1..35fe131a75 100644 --- a/packages/super-editor/src/document-api-adapters/assemble-adapters.test.ts +++ b/packages/super-editor/src/document-api-adapters/assemble-adapters.test.ts @@ -36,6 +36,7 @@ describe('assembleDocumentApiAdapters', () => { expect(adapters).toHaveProperty('trackChanges.rejectAll'); expect(adapters).toHaveProperty('create.paragraph'); expect(adapters).toHaveProperty('create.heading'); + expect(adapters).toHaveProperty('create.sectionBreak'); expect(adapters).toHaveProperty('lists.list'); expect(adapters).toHaveProperty('lists.get'); expect(adapters).toHaveProperty('lists.insert'); @@ -44,6 +45,24 @@ describe('assembleDocumentApiAdapters', () => { expect(adapters).toHaveProperty('lists.outdent'); expect(adapters).toHaveProperty('lists.restart'); expect(adapters).toHaveProperty('lists.exit'); + expect(adapters).toHaveProperty('sections.list'); + expect(adapters).toHaveProperty('sections.get'); + expect(adapters).toHaveProperty('sections.setBreakType'); + expect(adapters).toHaveProperty('sections.setPageMargins'); + expect(adapters).toHaveProperty('sections.setHeaderFooterMargins'); + expect(adapters).toHaveProperty('sections.setPageSetup'); + expect(adapters).toHaveProperty('sections.setColumns'); + expect(adapters).toHaveProperty('sections.setLineNumbering'); + expect(adapters).toHaveProperty('sections.setPageNumbering'); + expect(adapters).toHaveProperty('sections.setTitlePage'); + expect(adapters).toHaveProperty('sections.setOddEvenHeadersFooters'); + expect(adapters).toHaveProperty('sections.setVerticalAlign'); + expect(adapters).toHaveProperty('sections.setSectionDirection'); + expect(adapters).toHaveProperty('sections.setHeaderFooterRef'); + expect(adapters).toHaveProperty('sections.clearHeaderFooterRef'); + expect(adapters).toHaveProperty('sections.setLinkToPrevious'); + expect(adapters).toHaveProperty('sections.setPageBorders'); + expect(adapters).toHaveProperty('sections.clearPageBorders'); expect(adapters).toHaveProperty('tables.get'); expect(adapters).toHaveProperty('tables.getCells'); expect(adapters).toHaveProperty('tables.getProperties'); @@ -61,7 +80,11 @@ describe('assembleDocumentApiAdapters', () => { expect(typeof adapters.format.align).toBe('function'); expect(typeof adapters.create.paragraph).toBe('function'); expect(typeof adapters.create.heading).toBe('function'); + expect(typeof adapters.create.sectionBreak).toBe('function'); expect(typeof adapters.lists.insert).toBe('function'); + expect(typeof adapters.sections.list).toBe('function'); + expect(typeof adapters.sections.setBreakType).toBe('function'); + expect(typeof adapters.sections.setOddEvenHeadersFooters).toBe('function'); expect(typeof adapters.tables.get).toBe('function'); expect(typeof adapters.tables.getCells).toBe('function'); expect(typeof adapters.tables.getProperties).toBe('function'); 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 68d92a23fd..1b7249b16c 100644 --- a/packages/super-editor/src/document-api-adapters/assemble-adapters.ts +++ b/packages/super-editor/src/document-api-adapters/assemble-adapters.ts @@ -40,6 +40,27 @@ import { queryMatchAdapter } from './plan-engine/query-match-adapter.js'; import { initRevision, trackRevisions } from './plan-engine/revision-tracker.js'; import { registerBuiltInExecutors } from './plan-engine/register-executors.js'; import { createTableWrapper } from './plan-engine/create-table-wrapper.js'; +import { + createSectionBreakAdapter, + sectionsListAdapter, + sectionsGetAdapterByInput, + sectionsSetBreakTypeAdapter, + sectionsSetPageMarginsAdapter, + sectionsSetHeaderFooterMarginsAdapter, + sectionsSetPageSetupAdapter, + sectionsSetColumnsAdapter, + sectionsSetLineNumberingAdapter, + sectionsSetPageNumberingAdapter, + sectionsSetTitlePageAdapter, + sectionsSetOddEvenHeadersFootersAdapter, + sectionsSetVerticalAlignAdapter, + sectionsSetSectionDirectionAdapter, + sectionsSetHeaderFooterRefAdapter, + sectionsClearHeaderFooterRefAdapter, + sectionsSetLinkToPreviousAdapter, + sectionsSetPageBordersAdapter, + sectionsClearPageBordersAdapter, +} from './sections-adapter.js'; import { tablesDeleteWrapper, tablesClearContentsWrapper, @@ -138,6 +159,7 @@ export function assembleDocumentApiAdapters(editor: Editor): DocumentApiAdapters paragraph: (input, options) => createParagraphWrapper(editor, input, options), heading: (input, options) => createHeadingWrapper(editor, input, options), table: (input, options) => createTableWrapper(editor, input, options), + sectionBreak: (input, options) => createSectionBreakAdapter(editor, input, options), }, lists: { list: (query) => listsListWrapper(editor, query), @@ -149,6 +171,26 @@ export function assembleDocumentApiAdapters(editor: Editor): DocumentApiAdapters restart: (input, options) => listsRestartWrapper(editor, input, options), exit: (input, options) => listsExitWrapper(editor, input, options), }, + sections: { + list: (query) => sectionsListAdapter(editor, query), + get: (input) => sectionsGetAdapterByInput(editor, input), + setBreakType: (input, options) => sectionsSetBreakTypeAdapter(editor, input, options), + setPageMargins: (input, options) => sectionsSetPageMarginsAdapter(editor, input, options), + setHeaderFooterMargins: (input, options) => sectionsSetHeaderFooterMarginsAdapter(editor, input, options), + setPageSetup: (input, options) => sectionsSetPageSetupAdapter(editor, input, options), + setColumns: (input, options) => sectionsSetColumnsAdapter(editor, input, options), + setLineNumbering: (input, options) => sectionsSetLineNumberingAdapter(editor, input, options), + setPageNumbering: (input, options) => sectionsSetPageNumberingAdapter(editor, input, options), + setTitlePage: (input, options) => sectionsSetTitlePageAdapter(editor, input, options), + setOddEvenHeadersFooters: (input, options) => sectionsSetOddEvenHeadersFootersAdapter(editor, input, options), + setVerticalAlign: (input, options) => sectionsSetVerticalAlignAdapter(editor, input, options), + setSectionDirection: (input, options) => sectionsSetSectionDirectionAdapter(editor, input, options), + setHeaderFooterRef: (input, options) => sectionsSetHeaderFooterRefAdapter(editor, input, options), + clearHeaderFooterRef: (input, options) => sectionsClearHeaderFooterRefAdapter(editor, input, options), + setLinkToPrevious: (input, options) => sectionsSetLinkToPreviousAdapter(editor, input, options), + setPageBorders: (input, options) => sectionsSetPageBordersAdapter(editor, input, options), + clearPageBorders: (input, options) => sectionsClearPageBordersAdapter(editor, input, options), + }, tables: { convertFromText: (input, options) => tablesConvertFromTextWrapper(editor, input, options), delete: (input, options) => tablesDeleteWrapper(editor, input, options), diff --git a/packages/super-editor/src/document-api-adapters/capabilities-adapter.test.ts b/packages/super-editor/src/document-api-adapters/capabilities-adapter.test.ts index 9c6eb2c846..10cfab9c36 100644 --- a/packages/super-editor/src/document-api-adapters/capabilities-adapter.test.ts +++ b/packages/super-editor/src/document-api-adapters/capabilities-adapter.test.ts @@ -428,4 +428,42 @@ describe('getDocumentApiCapabilities', () => { const reasons = capabilities.operations['styles.apply'].reasons ?? []; expect(reasons).not.toContain('COMMAND_UNAVAILABLE'); }); + + it('marks sections.setOddEvenHeadersFooters as unavailable when converter is missing', () => { + const capabilities = getDocumentApiCapabilities(makeEditor()); + const reasons = capabilities.operations['sections.setOddEvenHeadersFooters'].reasons ?? []; + + expect(capabilities.operations['sections.setOddEvenHeadersFooters'].available).toBe(false); + expect(reasons).toContain('HELPER_UNAVAILABLE'); + expect(reasons).toContain('OPERATION_UNAVAILABLE'); + }); + + it('marks sections.setOddEvenHeadersFooters as available when converter is present', () => { + const editor = makeEditor(); + (editor as unknown as Record).converter = { convertedXml: {} }; + + const capabilities = getDocumentApiCapabilities(editor); + expect(capabilities.operations['sections.setOddEvenHeadersFooters'].available).toBe(true); + expect(capabilities.operations['sections.setOddEvenHeadersFooters'].dryRun).toBe(true); + expect(capabilities.operations['sections.setOddEvenHeadersFooters'].tracked).toBe(false); + }); + + it('marks sections.setHeaderFooterRef as unavailable when converter is missing', () => { + const capabilities = getDocumentApiCapabilities(makeEditor()); + const reasons = capabilities.operations['sections.setHeaderFooterRef'].reasons ?? []; + + expect(capabilities.operations['sections.setHeaderFooterRef'].available).toBe(false); + expect(reasons).toContain('HELPER_UNAVAILABLE'); + expect(reasons).toContain('OPERATION_UNAVAILABLE'); + }); + + it('marks sections.setHeaderFooterRef as available when converter is present', () => { + const editor = makeEditor(); + (editor as unknown as Record).converter = { convertedXml: {} }; + + const capabilities = getDocumentApiCapabilities(editor); + expect(capabilities.operations['sections.setHeaderFooterRef'].available).toBe(true); + expect(capabilities.operations['sections.setHeaderFooterRef'].dryRun).toBe(true); + expect(capabilities.operations['sections.setHeaderFooterRef'].tracked).toBe(false); + }); }); 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 61155e7d8b..a279933b26 100644 --- a/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts +++ b/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts @@ -17,6 +17,13 @@ import { TrackFormatMarkName } from '../extensions/track-changes/constants.js'; import { isCollaborationActive } from './collaboration-detection.js'; type EditorCommandName = string; +type EditorWithBlockNodeHelper = Editor & { + helpers?: { + blockNode?: { + getBlockNodeById?: unknown; + }; + }; +}; // Singleton write operations (insert, replace, delete) have no entry here because // they are backed by writeAdapter which is always available when the editor exists. @@ -102,7 +109,10 @@ function hasAllCommands(editor: Editor, operationId: OperationId): boolean { * Each entry maps an operation to a predicate that checks helper availability. */ const REQUIRED_HELPERS: Partial boolean>> = { - 'blocks.delete': (editor) => typeof (editor as any).helpers?.blockNode?.getBlockNodeById === 'function', + 'blocks.delete': (editor) => + typeof (editor as unknown as EditorWithBlockNodeHelper).helpers?.blockNode?.getBlockNodeById === 'function', + 'sections.setOddEvenHeadersFooters': (editor) => Boolean((editor as unknown as { converter?: unknown }).converter), + 'sections.setHeaderFooterRef': (editor) => Boolean((editor as unknown as { converter?: unknown }).converter), }; function hasRequiredHelpers(editor: Editor, operationId: OperationId): boolean { @@ -288,7 +298,24 @@ const SUPPORTED_STEP_OPS = [ 'assert', 'create.paragraph', 'create.heading', + 'create.sectionBreak', 'domain.command', + 'sections.setBreakType', + 'sections.setPageMargins', + 'sections.setHeaderFooterMargins', + 'sections.setPageSetup', + 'sections.setColumns', + 'sections.setLineNumbering', + 'sections.setPageNumbering', + 'sections.setTitlePage', + 'sections.setOddEvenHeadersFooters', + 'sections.setVerticalAlign', + 'sections.setSectionDirection', + 'sections.setHeaderFooterRef', + 'sections.clearHeaderFooterRef', + 'sections.setLinkToPrevious', + 'sections.setPageBorders', + 'sections.clearPageBorders', 'create.table', 'tables.delete', 'tables.clearContents', diff --git a/packages/super-editor/src/document-api-adapters/document-settings.ts b/packages/super-editor/src/document-api-adapters/document-settings.ts new file mode 100644 index 0000000000..7c091ad7df --- /dev/null +++ b/packages/super-editor/src/document-api-adapters/document-settings.ts @@ -0,0 +1,87 @@ +import type { XmlElement } from './helpers/sections-xml.js'; + +const SETTINGS_PART_PATH = 'word/settings.xml'; + +export interface ConverterWithDocumentSettings { + convertedXml?: Record; + pageStyles?: { + alternateHeaders?: boolean; + }; +} + +function createSettingsPart(): XmlElement { + return { + type: 'element', + name: 'document', + elements: [ + { + type: 'element', + name: 'w:settings', + elements: [], + }, + ], + }; +} + +function findSettingsRoot(part: XmlElement): XmlElement | null { + if (part.name === 'w:settings') return part; + if (!Array.isArray(part.elements)) return null; + return part.elements.find((entry) => entry.name === 'w:settings') ?? null; +} + +function ensureSettingsRootElements(settingsRoot: XmlElement): XmlElement[] { + if (!Array.isArray(settingsRoot.elements)) settingsRoot.elements = []; + return settingsRoot.elements; +} + +/** + * Read-only lookup: returns the existing settings root without creating parts. + * Returns null when word/settings.xml is absent. + */ +export function readSettingsRoot(converter: ConverterWithDocumentSettings): XmlElement | null { + const part = converter.convertedXml?.[SETTINGS_PART_PATH] as XmlElement | undefined; + if (!part) return null; + return findSettingsRoot(part); +} + +export function ensureSettingsRoot(converter: ConverterWithDocumentSettings): XmlElement { + if (!converter.convertedXml) converter.convertedXml = {}; + + let part = converter.convertedXml[SETTINGS_PART_PATH] as XmlElement | undefined; + if (!part) { + part = createSettingsPart(); + converter.convertedXml[SETTINGS_PART_PATH] = part; + } + + const settingsRoot = findSettingsRoot(part); + if (settingsRoot) return settingsRoot; + + const fallbackRoot: XmlElement = { + type: 'element', + name: 'w:settings', + elements: [], + }; + if (!Array.isArray(part.elements)) part.elements = []; + part.elements.push(fallbackRoot); + return fallbackRoot; +} + +export function hasOddEvenHeadersFooters(settingsRoot: XmlElement): boolean { + return settingsRoot.elements?.some((entry) => entry.name === 'w:evenAndOddHeaders') === true; +} + +export function setOddEvenHeadersFooters(settingsRoot: XmlElement, enabled: boolean): boolean { + const elements = ensureSettingsRootElements(settingsRoot); + const hadFlag = hasOddEvenHeadersFooters(settingsRoot); + + if (enabled) { + if (!hadFlag) { + elements.push({ type: 'element', name: 'w:evenAndOddHeaders', elements: [] }); + } + } else { + settingsRoot.elements = elements.filter((entry) => entry.name !== 'w:evenAndOddHeaders'); + } + + const hasFlag = hasOddEvenHeadersFooters(settingsRoot); + return hadFlag !== hasFlag; +} diff --git a/packages/super-editor/src/document-api-adapters/helpers/header-footer-parts.ts b/packages/super-editor/src/document-api-adapters/helpers/header-footer-parts.ts new file mode 100644 index 0000000000..5df5f7d4ee --- /dev/null +++ b/packages/super-editor/src/document-api-adapters/helpers/header-footer-parts.ts @@ -0,0 +1,367 @@ +import type { SectionHeaderFooterKind, SectionHeaderFooterVariant } from '@superdoc/document-api'; +import type { XmlElement } from './sections-xml.js'; + +const DOCUMENT_RELS_PATH = 'word/_rels/document.xml.rels'; +const RELS_XMLNS = 'http://schemas.openxmlformats.org/package/2006/relationships'; +const HEADER_RELATIONSHIP_TYPE = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/header'; +const FOOTER_RELATIONSHIP_TYPE = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer'; +const WORDPROCESSINGML_XMLNS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'; +const OFFICE_DOCUMENT_RELS_XMLNS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'; +const RELATIONSHIP_ID_PATTERN = /^rId(\d+)$/; +const HEADER_FILE_PATTERN = /^word\/header(\d+)\.xml$/; +const FOOTER_FILE_PATTERN = /^word\/footer(\d+)\.xml$/; + +type RelationshipElement = XmlElement & { + name: 'Relationship'; + attributes?: Record; +}; + +type HeaderFooterJsonDoc = { + type: 'doc'; + content: Array<{ + type: 'paragraph'; + content: unknown[]; + }>; +}; + +interface HeaderFooterVariantIds { + default?: string | null; + first?: string | null; + even?: string | null; + odd?: string | null; + ids?: string[]; + titlePg?: boolean; +} + +export interface ConverterWithHeaderFooterParts { + convertedXml?: Record; + headers?: Record; + footers?: Record; + headerIds?: HeaderFooterVariantIds; + footerIds?: HeaderFooterVariantIds; + headerFooterModified?: boolean; + documentModified?: boolean; +} + +interface SourcePartSnapshot { + xmlPart: Record | null; + xmlPartPath: string | null; + relsPart: Record | null; + relsPartPath: string | null; + jsonPart: Record | null; +} + +export interface CreateHeaderFooterPartInput { + kind: SectionHeaderFooterKind; + variant: SectionHeaderFooterVariant; + sourceRefId?: string; +} + +export interface CreateHeaderFooterPartResult { + refId: string; + relationshipTarget: string; +} + +export interface HeaderFooterRelationshipLookupInput { + kind: SectionHeaderFooterKind; + refId: string; +} + +function cloneValue(value: T): T { + return JSON.parse(JSON.stringify(value)) as T; +} + +function toRelationshipType(kind: SectionHeaderFooterKind): string { + return kind === 'header' ? HEADER_RELATIONSHIP_TYPE : FOOTER_RELATIONSHIP_TYPE; +} + +function toFilePattern(kind: SectionHeaderFooterKind): RegExp { + return kind === 'header' ? HEADER_FILE_PATTERN : FOOTER_FILE_PATTERN; +} + +function normalizeRelationshipTarget(target: string): string { + let normalized = target.replace(/^\.\//, ''); + if (normalized.startsWith('../')) normalized = normalized.slice(3); + if (normalized.startsWith('/')) normalized = normalized.slice(1); + if (!normalized.startsWith('word/')) normalized = `word/${normalized}`; + return normalized; +} + +function toRelsPathForPart(partPath: string): string { + const normalized = normalizeRelationshipTarget(partPath); + const fileName = normalized.split('/').pop(); + if (!fileName) return normalized; + return `word/_rels/${fileName}.rels`; +} + +function ensureConvertedXml(converter: ConverterWithHeaderFooterParts): Record { + if (!converter.convertedXml || typeof converter.convertedXml !== 'object') { + converter.convertedXml = {}; + } + return converter.convertedXml; +} + +function ensureRelationshipsRoot(converter: ConverterWithHeaderFooterParts): XmlElement { + const convertedXml = ensureConvertedXml(converter); + + let relsPart = convertedXml[DOCUMENT_RELS_PATH] as XmlElement | undefined; + if (!relsPart || typeof relsPart !== 'object') { + relsPart = { + name: 'document.xml.rels', + elements: [], + }; + convertedXml[DOCUMENT_RELS_PATH] = relsPart; + } + + if (!Array.isArray(relsPart.elements)) relsPart.elements = []; + let relationshipsRoot = relsPart.elements.find((entry) => entry.name === 'Relationships'); + if (!relationshipsRoot) { + relationshipsRoot = { + type: 'element', + name: 'Relationships', + attributes: { xmlns: RELS_XMLNS }, + elements: [], + }; + relsPart.elements.push(relationshipsRoot); + } + + if (!Array.isArray(relationshipsRoot.elements)) relationshipsRoot.elements = []; + if (!relationshipsRoot.attributes) relationshipsRoot.attributes = { xmlns: RELS_XMLNS }; + if (!relationshipsRoot.attributes.xmlns) relationshipsRoot.attributes.xmlns = RELS_XMLNS; + return relationshipsRoot; +} + +function readRelationshipsRoot(converter: ConverterWithHeaderFooterParts): XmlElement | null { + const relsPart = converter.convertedXml?.[DOCUMENT_RELS_PATH] as XmlElement | undefined; + if (!relsPart || typeof relsPart !== 'object' || !Array.isArray(relsPart.elements)) return null; + const relationshipsRoot = relsPart.elements.find((entry) => entry.name === 'Relationships'); + if (!relationshipsRoot || !Array.isArray(relationshipsRoot.elements)) return null; + return relationshipsRoot; +} + +function getRelationshipElements(root: XmlElement): RelationshipElement[] { + if (!Array.isArray(root.elements)) return []; + return root.elements.filter((entry): entry is RelationshipElement => entry.name === 'Relationship'); +} + +function findRelationshipById( + relationships: RelationshipElement[], + refId: string, + relationshipType: string, +): RelationshipElement | undefined { + return relationships.find( + (entry) => + String(entry.attributes?.Id ?? '') === refId && String(entry.attributes?.Type ?? '') === relationshipType, + ); +} + +export function hasHeaderFooterRelationship( + converter: ConverterWithHeaderFooterParts, + input: HeaderFooterRelationshipLookupInput, +): boolean { + const relationshipsRoot = readRelationshipsRoot(converter); + if (!relationshipsRoot) return false; + const relationships = getRelationshipElements(relationshipsRoot); + return findRelationshipById(relationships, input.refId, toRelationshipType(input.kind)) !== undefined; +} + +function nextRelationshipId(relationships: RelationshipElement[]): string { + const usedIds = new Set( + relationships.map((entry) => String(entry.attributes?.Id ?? '')).filter((value) => value.length > 0), + ); + + let largestNumericId = 0; + for (const id of usedIds) { + const match = id.match(RELATIONSHIP_ID_PATTERN); + if (!match) continue; + const numericId = Number(match[1]); + if (Number.isFinite(numericId) && numericId > largestNumericId) { + largestNumericId = numericId; + } + } + + let candidate = largestNumericId + 1; + while (usedIds.has(`rId${candidate}`)) candidate += 1; + return `rId${candidate}`; +} + +function nextHeaderFooterFilename( + kind: SectionHeaderFooterKind, + relationships: RelationshipElement[], + convertedXml: Record, +): string { + const relationshipType = toRelationshipType(kind); + const filePattern = toFilePattern(kind); + let largestIndex = 0; + + const candidatePaths = [ + ...relationships + .filter((entry) => String(entry.attributes?.Type ?? '') === relationshipType) + .map((entry) => normalizeRelationshipTarget(String(entry.attributes?.Target ?? ''))), + ...Object.keys(convertedXml), + ]; + + for (const path of candidatePaths) { + const match = path.match(filePattern); + if (!match) continue; + const numericIndex = Number(match[1]); + if (Number.isFinite(numericIndex) && numericIndex > largestIndex) { + largestIndex = numericIndex; + } + } + + let nextIndex = largestIndex + 1; + while (convertedXml[`word/${kind}${nextIndex}.xml`]) { + nextIndex += 1; + } + return `${kind}${nextIndex}.xml`; +} + +function createEmptyXmlPart(kind: SectionHeaderFooterKind): Record { + const rootName = kind === 'header' ? 'w:hdr' : 'w:ftr'; + return { + elements: [ + { + type: 'element', + name: rootName, + attributes: { + 'xmlns:w': WORDPROCESSINGML_XMLNS, + 'xmlns:r': OFFICE_DOCUMENT_RELS_XMLNS, + }, + elements: [{ type: 'element', name: 'w:p', elements: [] }], + }, + ], + }; +} + +function createEmptyJsonPart(): HeaderFooterJsonDoc { + return { + type: 'doc', + content: [{ type: 'paragraph', content: [] }], + }; +} + +function getCollection( + converter: ConverterWithHeaderFooterParts, + kind: SectionHeaderFooterKind, +): Record { + if (kind === 'header') { + if (!converter.headers || typeof converter.headers !== 'object') converter.headers = {}; + return converter.headers; + } + if (!converter.footers || typeof converter.footers !== 'object') converter.footers = {}; + return converter.footers; +} + +function getVariantIds( + converter: ConverterWithHeaderFooterParts, + kind: SectionHeaderFooterKind, +): HeaderFooterVariantIds { + if (kind === 'header') { + if (!converter.headerIds || typeof converter.headerIds !== 'object') converter.headerIds = {}; + return converter.headerIds; + } + if (!converter.footerIds || typeof converter.footerIds !== 'object') converter.footerIds = {}; + return converter.footerIds; +} + +function readSourceSnapshot( + converter: ConverterWithHeaderFooterParts, + kind: SectionHeaderFooterKind, + sourceRefId: string | undefined, + relationships: RelationshipElement[], +): SourcePartSnapshot { + const convertedXml = ensureConvertedXml(converter); + const collection = getCollection(converter, kind); + const relationshipType = toRelationshipType(kind); + + const sourceJsonPart = + sourceRefId && typeof collection[sourceRefId] === 'object' + ? (cloneValue(collection[sourceRefId]) as Record) + : null; + + if (!sourceRefId) { + return { + xmlPart: null, + xmlPartPath: null, + relsPart: null, + relsPartPath: null, + jsonPart: sourceJsonPart, + }; + } + + const sourceRelationship = findRelationshipById(relationships, sourceRefId, relationshipType); + const sourceTarget = sourceRelationship ? String(sourceRelationship.attributes?.Target ?? '') : ''; + if (!sourceTarget) { + return { + xmlPart: null, + xmlPartPath: null, + relsPart: null, + relsPartPath: null, + jsonPart: sourceJsonPart, + }; + } + + const sourcePartPath = normalizeRelationshipTarget(sourceTarget); + const sourcePart = convertedXml[sourcePartPath]; + const sourceRelsPath = toRelsPathForPart(sourcePartPath); + const sourceRelsPart = convertedXml[sourceRelsPath]; + + return { + xmlPart: sourcePart && typeof sourcePart === 'object' ? (cloneValue(sourcePart) as Record) : null, + xmlPartPath: sourcePartPath, + relsPart: + sourceRelsPart && typeof sourceRelsPart === 'object' + ? (cloneValue(sourceRelsPart) as Record) + : null, + relsPartPath: sourceRelsPart ? sourceRelsPath : null, + jsonPart: sourceJsonPart, + }; +} + +export function createHeaderFooterPart( + converter: ConverterWithHeaderFooterParts, + input: CreateHeaderFooterPartInput, +): CreateHeaderFooterPartResult { + const convertedXml = ensureConvertedXml(converter); + const relationshipsRoot = ensureRelationshipsRoot(converter); + const relationships = getRelationshipElements(relationshipsRoot); + + const newRefId = nextRelationshipId(relationships); + const relationshipType = toRelationshipType(input.kind); + const newFilename = nextHeaderFooterFilename(input.kind, relationships, convertedXml); + const newPartPath = `word/${newFilename}`; + const sourceSnapshot = readSourceSnapshot(converter, input.kind, input.sourceRefId, relationships); + + const partXml = sourceSnapshot.xmlPart ?? createEmptyXmlPart(input.kind); + convertedXml[newPartPath] = partXml; + + if (sourceSnapshot.relsPart && sourceSnapshot.xmlPartPath) { + convertedXml[toRelsPathForPart(newPartPath)] = sourceSnapshot.relsPart; + } + + relationshipsRoot.elements!.push({ + type: 'element', + name: 'Relationship', + attributes: { + Id: newRefId, + Type: relationshipType, + Target: newFilename, + }, + }); + + const collection = getCollection(converter, input.kind); + collection[newRefId] = sourceSnapshot.jsonPart ?? createEmptyJsonPart(); + + const variantIds = getVariantIds(converter, input.kind); + if (!Array.isArray(variantIds.ids)) variantIds.ids = []; + if (!variantIds.ids.includes(newRefId)) variantIds.ids.push(newRefId); + + converter.headerFooterModified = true; + converter.documentModified = true; + + return { + refId: newRefId, + relationshipTarget: newPartPath, + }; +} diff --git a/packages/super-editor/src/document-api-adapters/helpers/sections-resolver.ts b/packages/super-editor/src/document-api-adapters/helpers/sections-resolver.ts new file mode 100644 index 0000000000..bd02088604 --- /dev/null +++ b/packages/super-editor/src/document-api-adapters/helpers/sections-resolver.ts @@ -0,0 +1,374 @@ +import type { Node as ProseMirrorNode } from 'prosemirror-model'; +import type { + SectionAddress, + SectionDomain, + SectionInfo, + SectionPageMargins, + SectionsListQuery, + SectionsListResult, +} from '@superdoc/document-api'; +import { buildDiscoveryItem, buildDiscoveryResult, buildResolvedHandle } from '@superdoc/document-api'; +import { analyzeSectionRanges } from '@superdoc/pm-adapter/sections/analysis.js'; +import { SectionType, type SectionRange } from '@superdoc/pm-adapter/sections/types.js'; +import type { Editor } from '../../core/Editor.js'; +import { DocumentApiAdapterError } from '../errors.js'; +import { getRevision } from '../plan-engine/revision-tracker.js'; +import { paginate, validatePaginationInput } from './adapter-utils.js'; +import { toId } from './value-utils.js'; +import { + cloneXmlElement, + isSectPrElement, + readSectPrColumns, + readSectPrDirection, + readSectPrLineNumbering, + readSectPrMargins, + readSectPrPageBorders, + readSectPrPageNumbering, + readSectPrPageSetup, + readSectPrVerticalAlign, + type XmlElement, +} from './sections-xml.js'; + +export type SectionMutationTarget = + | { + kind: 'paragraph'; + paragraphIndex: number; + pos: number; + node: ProseMirrorNode; + nodeId: string; + } + | { + kind: 'body'; + }; + +export interface SectionProjection { + sectionId: string; + address: SectionAddress; + range: SectionRange; + target: SectionMutationTarget; + domain: SectionDomain; +} + +interface ParagraphSnapshot { + index: number; + pos: number; + node: ProseMirrorNode; + nodeId: string; +} + +interface ConverterWithSections { + bodySectPr?: unknown; + pageStyles?: { + alternateHeaders?: boolean; + }; + convertedXml?: Record; +} + +const PIXELS_PER_INCH = 96; + +function pxToInches(value: number | null | undefined): number | undefined { + if (typeof value !== 'number' || !Number.isFinite(value)) return undefined; + return value / PIXELS_PER_INCH; +} + +function buildSectionId(index: number): string { + return `section-${index}`; +} + +function getConverter(editor: Editor): ConverterWithSections | undefined { + return (editor as unknown as { converter?: ConverterWithSections }).converter; +} + +export function getBodySectPrFromEditor(editor: Editor): XmlElement | null { + const converter = getConverter(editor); + if (isSectPrElement(converter?.bodySectPr)) return cloneXmlElement(converter.bodySectPr); + + const docAttrs = (editor.state.doc.attrs ?? {}) as { bodySectPr?: unknown }; + if (isSectPrElement(docAttrs.bodySectPr)) return cloneXmlElement(docAttrs.bodySectPr); + return null; +} + +function resolveParagraphNodeId(node: ProseMirrorNode): string | undefined { + const attrs = (node.attrs ?? {}) as Record; + return toId(attrs.paraId) ?? toId(attrs.sdBlockId) ?? toId(attrs.blockId) ?? toId(attrs.id); +} + +function collectParagraphSnapshots(editor: Editor): ParagraphSnapshot[] { + const snapshots: ParagraphSnapshot[] = []; + let paragraphIndex = 0; + + editor.state.doc.descendants((node, pos) => { + if (node.type.name !== 'paragraph') return; + const nodeId = resolveParagraphNodeId(node); + if (!nodeId) { + paragraphIndex += 1; + return; + } + + snapshots.push({ + index: paragraphIndex, + pos, + node, + nodeId, + }); + paragraphIndex += 1; + }); + + return snapshots; +} + +function buildAnalysisDocFromParagraphs(paragraphs: ParagraphSnapshot[]): Parameters[0] { + return { + type: 'doc', + content: paragraphs.map((paragraph) => ({ + type: 'paragraph', + attrs: paragraph.node.attrs ?? {}, + })), + }; +} + +function resolveAnalysisDoc( + editor: Editor, + paragraphs: ParagraphSnapshot[], +): Parameters[0] { + const maybeJsonDoc = (editor.state.doc as unknown as { toJSON?: () => unknown }).toJSON?.(); + if ( + maybeJsonDoc && + typeof maybeJsonDoc === 'object' && + typeof (maybeJsonDoc as { type?: unknown }).type === 'string' && + Array.isArray((maybeJsonDoc as { content?: unknown[] }).content) + ) { + return maybeJsonDoc as Parameters[0]; + } + + return buildAnalysisDocFromParagraphs(paragraphs); +} + +function getSettingsRoot(editor: Editor): XmlElement | null { + const converter = getConverter(editor); + const settingsPart = converter?.convertedXml?.['word/settings.xml'] as XmlElement | undefined; + if (!settingsPart) return null; + + if (settingsPart.name === 'w:settings') return settingsPart; + if (!Array.isArray(settingsPart.elements)) return null; + const settingsRoot = settingsPart.elements.find((entry) => entry.name === 'w:settings'); + return settingsRoot ?? null; +} + +function readOddEvenHeadersFlag(editor: Editor): boolean { + const converter = getConverter(editor); + if (converter?.pageStyles?.alternateHeaders != null) return converter.pageStyles.alternateHeaders === true; + + const settingsRoot = getSettingsRoot(editor); + if (!settingsRoot?.elements) return false; + return settingsRoot.elements.some((entry) => entry.name === 'w:evenAndOddHeaders'); +} + +function createSyntheticRange(bodySectPr: XmlElement | null, paragraphCount: number): SectionRange { + return { + sectionIndex: 0, + startParagraphIndex: 0, + endParagraphIndex: Math.max(paragraphCount - 1, 0), + sectPr: (bodySectPr as unknown as SectionRange['sectPr']) ?? null, + margins: null, + pageSize: null, + orientation: null, + columns: null, + type: SectionType.CONTINUOUS, + titlePg: false, + headerRefs: undefined, + footerRefs: undefined, + numbering: undefined, + vAlign: undefined, + }; +} + +function projectSectionTarget( + range: SectionRange, + rangeIndex: number, + totalRanges: number, + hasBodySectPr: boolean, + paragraphs: ParagraphSnapshot[], +): SectionMutationTarget { + const paragraph = paragraphs.find((entry) => entry.index === range.endParagraphIndex); + const isFinalRange = rangeIndex === totalRanges - 1; + + if (paragraph && (!isFinalRange || !hasBodySectPr) && range.sectPr) { + return { + kind: 'paragraph', + paragraphIndex: paragraph.index, + pos: paragraph.pos, + node: paragraph.node, + nodeId: paragraph.nodeId, + }; + } + + return { kind: 'body' }; +} + +function toPageMargins(range: SectionRange, sectPr: XmlElement | null): SectionPageMargins | undefined { + const parsed = sectPr ? readSectPrMargins(sectPr) : {}; + const margins = { + top: pxToInches(range.margins?.top) ?? parsed.top, + right: pxToInches(range.margins?.right) ?? parsed.right, + bottom: pxToInches(range.margins?.bottom) ?? parsed.bottom, + left: pxToInches(range.margins?.left) ?? parsed.left, + gutter: parsed.gutter, + }; + + if ( + margins.top == null && + margins.right == null && + margins.bottom == null && + margins.left == null && + margins.gutter == null + ) { + return undefined; + } + + return margins; +} + +function sectionRangeToSectionDomain( + range: SectionRange, + address: SectionAddress, + oddEvenHeadersFooters: boolean, +): SectionDomain { + const sectPr = isSectPrElement(range.sectPr) ? range.sectPr : null; + const parsedSetup = sectPr ? readSectPrPageSetup(sectPr) : undefined; + const parsedColumns = sectPr ? readSectPrColumns(sectPr) : undefined; + const parsedLineNumbering = sectPr ? readSectPrLineNumbering(sectPr) : undefined; + const parsedPageNumbering = sectPr ? readSectPrPageNumbering(sectPr) : undefined; + const parsedDirection = sectPr ? readSectPrDirection(sectPr) : undefined; + const parsedVerticalAlign = sectPr ? readSectPrVerticalAlign(sectPr) : undefined; + const parsedBorders = sectPr ? readSectPrPageBorders(sectPr) : undefined; + const parsedMargins = sectPr ? readSectPrMargins(sectPr) : {}; + + const pageSetup = { + width: pxToInches(range.pageSize?.w) ?? parsedSetup?.width, + height: pxToInches(range.pageSize?.h) ?? parsedSetup?.height, + orientation: range.orientation ?? parsedSetup?.orientation, + paperSize: parsedSetup?.paperSize, + }; + + const margins = toPageMargins(range, sectPr); + const headerFooterMargins = { + header: pxToInches(range.margins?.header) ?? parsedMargins.header, + footer: pxToInches(range.margins?.footer) ?? parsedMargins.footer, + }; + + const columns = { + count: range.columns?.count ?? parsedColumns?.count, + gap: pxToInches(range.columns?.gap) ?? parsedColumns?.gap, + equalWidth: parsedColumns?.equalWidth, + }; + + const toRefs = ( + refs: SectionRange['headerRefs'] | undefined, + ): + | { + default?: string; + first?: string; + even?: string; + } + | undefined => { + if (!refs) return undefined; + const mapped = { + default: refs.default ?? refs.odd, + first: refs.first, + even: refs.even, + }; + if (mapped.default == null && mapped.first == null && mapped.even == null) return undefined; + return mapped; + }; + + const hasPageSetup = + pageSetup.width != null || pageSetup.height != null || pageSetup.orientation != null || pageSetup.paperSize != null; + const hasHeaderFooterMargins = headerFooterMargins.header != null || headerFooterMargins.footer != null; + const hasColumns = columns.count != null || columns.gap != null || columns.equalWidth != null; + + return { + address, + index: range.sectionIndex, + range: { + startParagraphIndex: range.startParagraphIndex, + endParagraphIndex: range.endParagraphIndex, + }, + breakType: range.type, + pageSetup: hasPageSetup ? pageSetup : undefined, + margins, + headerFooterMargins: hasHeaderFooterMargins ? headerFooterMargins : undefined, + columns: hasColumns ? columns : undefined, + lineNumbering: parsedLineNumbering, + pageNumbering: range.numbering ?? parsedPageNumbering, + titlePage: range.titlePg, + oddEvenHeadersFooters, + verticalAlign: range.vAlign ?? parsedVerticalAlign, + sectionDirection: parsedDirection, + headerRefs: toRefs(range.headerRefs), + footerRefs: toRefs(range.footerRefs), + pageBorders: parsedBorders, + }; +} + +export function resolveSectionProjections(editor: Editor): SectionProjection[] { + const paragraphs = collectParagraphSnapshots(editor); + const bodySectPr = getBodySectPrFromEditor(editor); + const oddEvenHeadersFooters = readOddEvenHeadersFlag(editor); + const analysisDoc = resolveAnalysisDoc(editor, paragraphs); + const analyzed = analyzeSectionRanges(analysisDoc, bodySectPr ?? undefined); + const ranges = analyzed.length > 0 ? analyzed : [createSyntheticRange(bodySectPr, paragraphs.length)]; + + return ranges.map((range, index) => { + const sectionId = buildSectionId(index); + const address: SectionAddress = { kind: 'section', sectionId }; + const target = projectSectionTarget(range, index, ranges.length, bodySectPr != null, paragraphs); + const domain = sectionRangeToSectionDomain(range, address, oddEvenHeadersFooters); + return { sectionId, address, range, target, domain }; + }); +} + +export function resolveSectionProjectionByAddress(editor: Editor, address: SectionAddress): SectionProjection { + const match = resolveSectionProjections(editor).find((projection) => projection.sectionId === address.sectionId); + if (!match) { + throw new DocumentApiAdapterError('TARGET_NOT_FOUND', 'Section address was not found.', { target: address }); + } + return match; +} + +export function resolveSectionProjectionByIndex(editor: Editor, index: number): SectionProjection | undefined { + return resolveSectionProjections(editor).find((projection) => projection.range.sectionIndex === index); +} + +export function getDefaultSectionAddress(editor: Editor): SectionAddress { + const first = resolveSectionProjections(editor)[0]; + return first?.address ?? { kind: 'section', sectionId: buildSectionId(0) }; +} + +export function sectionsListAdapter(editor: Editor, query?: SectionsListQuery): SectionsListResult { + validatePaginationInput(query?.offset, query?.limit); + const projections = resolveSectionProjections(editor); + const offset = query?.offset ?? 0; + const { total, items: paged } = paginate(projections, offset, query?.limit); + const evaluatedRevision = getRevision(editor); + + const items = paged.map((projection) => { + const handle = buildResolvedHandle(projection.sectionId, 'ephemeral', 'section'); + return buildDiscoveryItem(projection.sectionId, handle, projection.domain); + }); + + return buildDiscoveryResult({ + evaluatedRevision, + total, + items, + page: { + limit: query?.limit ?? total, + offset, + returned: items.length, + }, + }); +} + +export function sectionsGetAdapter(editor: Editor, address: SectionAddress): SectionInfo { + return resolveSectionProjectionByAddress(editor, address).domain; +} diff --git a/packages/super-editor/src/document-api-adapters/helpers/sections-xml.ts b/packages/super-editor/src/document-api-adapters/helpers/sections-xml.ts new file mode 100644 index 0000000000..373257c1b2 --- /dev/null +++ b/packages/super-editor/src/document-api-adapters/helpers/sections-xml.ts @@ -0,0 +1,526 @@ +import type { + SectionBorderSpec, + SectionBreakType, + SectionColumns, + SectionDirection, + SectionHeaderFooterKind, + SectionHeaderFooterRefs, + SectionHeaderFooterVariant, + SectionLineNumbering, + SectionLineNumberRestart, + SectionOrientation, + SectionPageBorders, + SectionPageMargins, + SectionPageNumbering, + SectionPageNumberingFormat, + SectionPageSetup, + SectionVerticalAlign, +} from '@superdoc/document-api'; +import { inchesToTwips, twipsToInches } from '../../core/super-converter/helpers.js'; + +export interface XmlElement { + type?: string; + name: string; + attributes?: Record; + elements?: XmlElement[]; +} + +const LINE_NUMBER_RESTART_VALUES: readonly SectionLineNumberRestart[] = [ + 'continuous', + 'newPage', + 'newSection', +] as const; +const PAGE_NUMBER_FORMAT_VALUES: readonly SectionPageNumberingFormat[] = [ + 'decimal', + 'lowerLetter', + 'upperLetter', + 'lowerRoman', + 'upperRoman', + 'numberInDash', +] as const; +const SECTION_ORIENTATION_VALUES: readonly SectionOrientation[] = ['portrait', 'landscape'] as const; +const SECTION_VERTICAL_ALIGN_VALUES: readonly SectionVerticalAlign[] = ['top', 'center', 'bottom', 'both'] as const; + +function toNumber(value: unknown): number | undefined { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : undefined; +} + +function toPositiveInteger(value: unknown): number | undefined { + const parsed = toNumber(value); + if (parsed == null || !Number.isInteger(parsed) || parsed <= 0) return undefined; + return parsed; +} + +function toNonNegativeNumber(value: unknown): number | undefined { + const parsed = toNumber(value); + if (parsed == null || parsed < 0) return undefined; + return parsed; +} + +function toInchesFromTwips(value: unknown): number | undefined { + const twips = toNumber(value); + if (twips == null) return undefined; + return twipsToInches(twips); +} + +function toTwipsString(valueInches: number): string { + return String(inchesToTwips(valueInches)); +} + +function asString(value: unknown): string | undefined { + return typeof value === 'string' ? value : undefined; +} + +function setStringAttr(element: XmlElement, key: string, value: unknown): void { + if (!element.attributes) element.attributes = {}; + if (value === undefined || value === null) { + delete element.attributes[key]; + return; + } + element.attributes[key] = String(value); +} + +function setBooleanAttr(element: XmlElement, key: string, value: boolean | undefined): void { + if (value === undefined) return; + setStringAttr(element, key, value ? '1' : '0'); +} + +function toBooleanAttr(value: unknown): boolean | undefined { + if (value === undefined) return undefined; + const normalized = String(value).toLowerCase(); + if (normalized === '1' || normalized === 'true' || normalized === 'on') return true; + if (normalized === '0' || normalized === 'false' || normalized === 'off') return false; + return undefined; +} + +function isKnownValue(value: unknown, allowed: readonly T[]): value is T { + return typeof value === 'string' && (allowed as readonly string[]).includes(value); +} + +function ensureElements(element: XmlElement): XmlElement[] { + if (!Array.isArray(element.elements)) element.elements = []; + return element.elements; +} + +export function cloneXmlElement(value: T): T { + return JSON.parse(JSON.stringify(value)) as T; +} + +export function isSectPrElement(value: unknown): value is XmlElement { + return Boolean(value && typeof value === 'object' && (value as XmlElement).name === 'w:sectPr'); +} + +export function createSectPrElement(): XmlElement { + return { type: 'element', name: 'w:sectPr', elements: [] }; +} + +export function ensureSectPrElement(current: unknown): XmlElement { + if (isSectPrElement(current)) return cloneXmlElement(current); + return createSectPrElement(); +} + +export function findChild(element: XmlElement, childName: string): XmlElement | undefined { + return element.elements?.find((entry) => entry?.name === childName); +} + +export function ensureChild(element: XmlElement, childName: string): XmlElement { + const existing = findChild(element, childName); + if (existing) return existing; + + const created: XmlElement = { type: 'element', name: childName, attributes: {}, elements: [] }; + ensureElements(element).push(created); + return created; +} + +export function removeChildren( + element: XmlElement, + predicate: (entry: XmlElement) => boolean, +): { removed: boolean; element: XmlElement } { + if (!Array.isArray(element.elements) || element.elements.length === 0) return { removed: false, element }; + const kept = element.elements.filter((entry) => !predicate(entry)); + const removed = kept.length !== element.elements.length; + element.elements = kept; + return { removed, element }; +} + +function sectionRefElementName(kind: SectionHeaderFooterKind): 'w:headerReference' | 'w:footerReference' { + return kind === 'header' ? 'w:headerReference' : 'w:footerReference'; +} + +function toRefVariant(value: unknown): SectionHeaderFooterVariant | undefined { + return isKnownValue(value, ['default', 'first', 'even'] as const) ? value : undefined; +} + +function toPageBorderDisplay(value: unknown): SectionPageBorders['display'] { + return isKnownValue(value, ['allPages', 'firstPage', 'notFirstPage'] as const) ? value : undefined; +} + +function toPageBorderOffsetFrom(value: unknown): SectionPageBorders['offsetFrom'] { + return isKnownValue(value, ['page', 'text'] as const) ? value : undefined; +} + +function toPageBorderZOrder(value: unknown): SectionPageBorders['zOrder'] { + return isKnownValue(value, ['front', 'back'] as const) ? value : undefined; +} + +export function readSectPrBreakType(sectPr: XmlElement): SectionBreakType | undefined { + const typeNode = findChild(sectPr, 'w:type'); + const value = asString(typeNode?.attributes?.['w:val']); + return isKnownValue(value, ['continuous', 'nextPage', 'evenPage', 'oddPage'] as const) ? value : undefined; +} + +export function writeSectPrBreakType(sectPr: XmlElement, breakType: SectionBreakType): void { + const typeNode = ensureChild(sectPr, 'w:type'); + setStringAttr(typeNode, 'w:val', breakType); +} + +export function readSectPrMargins(sectPr: XmlElement): SectionPageMargins & { header?: number; footer?: number } { + const pgMar = findChild(sectPr, 'w:pgMar'); + if (!pgMar) return {}; + + return { + top: toInchesFromTwips(pgMar.attributes?.['w:top']), + right: toInchesFromTwips(pgMar.attributes?.['w:right']), + bottom: toInchesFromTwips(pgMar.attributes?.['w:bottom']), + left: toInchesFromTwips(pgMar.attributes?.['w:left']), + gutter: toInchesFromTwips(pgMar.attributes?.['w:gutter']), + header: toInchesFromTwips(pgMar.attributes?.['w:header']), + footer: toInchesFromTwips(pgMar.attributes?.['w:footer']), + }; +} + +export function writeSectPrPageMargins(sectPr: XmlElement, margins: SectionPageMargins): void { + const pgMar = ensureChild(sectPr, 'w:pgMar'); + if (margins.top !== undefined) setStringAttr(pgMar, 'w:top', toTwipsString(margins.top)); + if (margins.right !== undefined) setStringAttr(pgMar, 'w:right', toTwipsString(margins.right)); + if (margins.bottom !== undefined) setStringAttr(pgMar, 'w:bottom', toTwipsString(margins.bottom)); + if (margins.left !== undefined) setStringAttr(pgMar, 'w:left', toTwipsString(margins.left)); + if (margins.gutter !== undefined) setStringAttr(pgMar, 'w:gutter', toTwipsString(margins.gutter)); +} + +export function writeSectPrHeaderFooterMargins( + sectPr: XmlElement, + margins: { header?: number; footer?: number }, +): void { + const pgMar = ensureChild(sectPr, 'w:pgMar'); + if (margins.header !== undefined) setStringAttr(pgMar, 'w:header', toTwipsString(margins.header)); + if (margins.footer !== undefined) setStringAttr(pgMar, 'w:footer', toTwipsString(margins.footer)); +} + +export function readSectPrPageSetup(sectPr: XmlElement): SectionPageSetup | undefined { + const pgSz = findChild(sectPr, 'w:pgSz'); + if (!pgSz) return undefined; + + const width = toInchesFromTwips(pgSz.attributes?.['w:w']); + const height = toInchesFromTwips(pgSz.attributes?.['w:h']); + const orientationRaw = asString(pgSz.attributes?.['w:orient']); + const orientation = isKnownValue(orientationRaw, SECTION_ORIENTATION_VALUES) ? orientationRaw : undefined; + const paperSize = asString(pgSz.attributes?.['w:code']); + + if (width == null && height == null && orientation == null && paperSize == null) return undefined; + return { width, height, orientation, paperSize }; +} + +export function writeSectPrPageSetup(sectPr: XmlElement, setup: SectionPageSetup): void { + const pgSz = ensureChild(sectPr, 'w:pgSz'); + if (setup.width !== undefined) setStringAttr(pgSz, 'w:w', toTwipsString(setup.width)); + if (setup.height !== undefined) setStringAttr(pgSz, 'w:h', toTwipsString(setup.height)); + if (setup.orientation !== undefined) setStringAttr(pgSz, 'w:orient', setup.orientation); + if (setup.paperSize !== undefined) setStringAttr(pgSz, 'w:code', setup.paperSize); +} + +export function readSectPrColumns(sectPr: XmlElement): SectionColumns | undefined { + const cols = findChild(sectPr, 'w:cols'); + if (!cols) return undefined; + + const count = toPositiveInteger(cols.attributes?.['w:num']); + const gap = toInchesFromTwips(cols.attributes?.['w:space']); + const equalWidth = toBooleanAttr(cols.attributes?.['w:equalWidth']); + if (count == null && gap == null && equalWidth == null) return undefined; + return { count, gap, equalWidth }; +} + +export function writeSectPrColumns(sectPr: XmlElement, columns: SectionColumns): void { + const cols = ensureChild(sectPr, 'w:cols'); + if (columns.count !== undefined) setStringAttr(cols, 'w:num', columns.count); + if (columns.gap !== undefined) setStringAttr(cols, 'w:space', toTwipsString(columns.gap)); + if (columns.equalWidth !== undefined) setBooleanAttr(cols, 'w:equalWidth', columns.equalWidth); +} + +export function readSectPrLineNumbering(sectPr: XmlElement): SectionLineNumbering | undefined { + const lnNumType = findChild(sectPr, 'w:lnNumType'); + if (!lnNumType) return undefined; + + const restartRaw = asString(lnNumType.attributes?.['w:restart']); + const restart = isKnownValue(restartRaw, LINE_NUMBER_RESTART_VALUES) ? restartRaw : undefined; + + return { + enabled: true, + countBy: toPositiveInteger(lnNumType.attributes?.['w:countBy']), + start: toPositiveInteger(lnNumType.attributes?.['w:start']), + distance: toInchesFromTwips(lnNumType.attributes?.['w:distance']), + restart, + }; +} + +export function writeSectPrLineNumbering(sectPr: XmlElement, numbering: SectionLineNumbering): void { + if (!numbering.enabled) { + removeChildren(sectPr, (entry) => entry.name === 'w:lnNumType'); + return; + } + + const lnNumType = ensureChild(sectPr, 'w:lnNumType'); + if (numbering.countBy !== undefined) setStringAttr(lnNumType, 'w:countBy', numbering.countBy); + if (numbering.start !== undefined) setStringAttr(lnNumType, 'w:start', numbering.start); + if (numbering.distance !== undefined) setStringAttr(lnNumType, 'w:distance', toTwipsString(numbering.distance)); + if (numbering.restart !== undefined) setStringAttr(lnNumType, 'w:restart', numbering.restart); +} + +export function readSectPrPageNumbering(sectPr: XmlElement): SectionPageNumbering | undefined { + const pgNumType = findChild(sectPr, 'w:pgNumType'); + if (!pgNumType) return undefined; + + const formatRaw = asString(pgNumType.attributes?.['w:fmt']); + const format = isKnownValue(formatRaw, PAGE_NUMBER_FORMAT_VALUES) ? formatRaw : undefined; + const start = toPositiveInteger(pgNumType.attributes?.['w:start']); + + if (format == null && start == null) return undefined; + return { format, start }; +} + +export function writeSectPrPageNumbering(sectPr: XmlElement, numbering: SectionPageNumbering): void { + if (numbering.start === undefined && numbering.format === undefined) return; + const pgNumType = ensureChild(sectPr, 'w:pgNumType'); + if (numbering.start !== undefined) setStringAttr(pgNumType, 'w:start', numbering.start); + if (numbering.format !== undefined) setStringAttr(pgNumType, 'w:fmt', numbering.format); +} + +export function readSectPrTitlePage(sectPr: XmlElement): boolean { + return Boolean(findChild(sectPr, 'w:titlePg')); +} + +export function writeSectPrTitlePage(sectPr: XmlElement, enabled: boolean): void { + if (enabled) { + ensureChild(sectPr, 'w:titlePg'); + return; + } + removeChildren(sectPr, (entry) => entry.name === 'w:titlePg'); +} + +export function readSectPrVerticalAlign(sectPr: XmlElement): SectionVerticalAlign | undefined { + const vAlign = findChild(sectPr, 'w:vAlign'); + const raw = asString(vAlign?.attributes?.['w:val']); + return isKnownValue(raw, SECTION_VERTICAL_ALIGN_VALUES) ? raw : undefined; +} + +export function writeSectPrVerticalAlign(sectPr: XmlElement, value: SectionVerticalAlign): void { + const vAlign = ensureChild(sectPr, 'w:vAlign'); + setStringAttr(vAlign, 'w:val', value); +} + +export function readSectPrDirection(sectPr: XmlElement): SectionDirection | undefined { + const bidi = findChild(sectPr, 'w:bidi'); + if (!bidi) return undefined; + const value = toBooleanAttr(bidi.attributes?.['w:val']); + return value === false ? 'ltr' : 'rtl'; +} + +export function writeSectPrDirection(sectPr: XmlElement, direction: SectionDirection): void { + if (direction === 'ltr') { + removeChildren(sectPr, (entry) => entry.name === 'w:bidi'); + return; + } + const bidi = ensureChild(sectPr, 'w:bidi'); + setStringAttr(bidi, 'w:val', '1'); +} + +function readSectPrRefByVariant( + sectPr: XmlElement, + kind: SectionHeaderFooterKind, + variant: SectionHeaderFooterVariant, +): string | undefined { + const elementName = sectionRefElementName(kind); + const refNode = sectPr.elements?.find( + (entry) => entry.name === elementName && toRefVariant(entry.attributes?.['w:type']) === variant, + ); + return asString(refNode?.attributes?.['r:id']); +} + +export function readSectPrRefsByKind( + sectPr: XmlElement, + kind: SectionHeaderFooterKind, +): SectionHeaderFooterRefs | undefined { + const refs: SectionHeaderFooterRefs = { + default: readSectPrRefByVariant(sectPr, kind, 'default'), + first: readSectPrRefByVariant(sectPr, kind, 'first'), + even: readSectPrRefByVariant(sectPr, kind, 'even'), + }; + + if (refs.default == null && refs.first == null && refs.even == null) return undefined; + return refs; +} + +/** + * Canonical adapter-facing alias used by section mutation adapters. + */ +export const readSectPrHeaderFooterRefs = readSectPrRefsByKind; + +export function setSectPrHeaderFooterRef( + sectPr: XmlElement, + kind: SectionHeaderFooterKind, + variant: SectionHeaderFooterVariant, + refId: string, +): void { + const elementName = sectionRefElementName(kind); + removeChildren( + sectPr, + (entry) => entry.name === elementName && toRefVariant(entry.attributes?.['w:type']) === variant, + ); + ensureElements(sectPr).push({ + type: 'element', + name: elementName, + attributes: { + 'w:type': variant, + 'r:id': refId, + }, + elements: [], + }); +} + +export function clearSectPrHeaderFooterRef( + sectPr: XmlElement, + kind: SectionHeaderFooterKind, + variant: SectionHeaderFooterVariant, +): boolean { + const elementName = sectionRefElementName(kind); + const { removed } = removeChildren( + sectPr, + (entry) => entry.name === elementName && toRefVariant(entry.attributes?.['w:type']) === variant, + ); + return removed; +} + +export function getSectPrHeaderFooterRef( + sectPr: XmlElement, + kind: SectionHeaderFooterKind, + variant: SectionHeaderFooterVariant, +): string | undefined { + return readSectPrRefByVariant(sectPr, kind, variant); +} + +function readBorderSpec(element: XmlElement | undefined): SectionBorderSpec | undefined { + if (!element?.attributes) return undefined; + const attributes = element.attributes; + const style = asString(attributes['w:val']); + const size = toNonNegativeNumber(attributes['w:sz']); + const space = toNonNegativeNumber(attributes['w:space']); + const color = asString(attributes['w:color']); + const shadow = toBooleanAttr(attributes['w:shadow']); + const frame = toBooleanAttr(attributes['w:frame']); + + if (style == null && size == null && space == null && color == null && shadow == null && frame == null) + return undefined; + return { style, size, space, color, shadow, frame }; +} + +function writeBorderSpec( + parent: XmlElement, + edge: 'top' | 'right' | 'bottom' | 'left', + border: SectionBorderSpec, +): void { + if ( + border.style === undefined && + border.size === undefined && + border.space === undefined && + border.color === undefined && + border.shadow === undefined && + border.frame === undefined + ) { + return; + } + + const edgeElement = ensureChild(parent, `w:${edge}`); + if (border.style !== undefined) setStringAttr(edgeElement, 'w:val', border.style); + if (border.size !== undefined) setStringAttr(edgeElement, 'w:sz', border.size); + if (border.space !== undefined) setStringAttr(edgeElement, 'w:space', border.space); + if (border.color !== undefined) setStringAttr(edgeElement, 'w:color', border.color); + if (border.shadow !== undefined) setBooleanAttr(edgeElement, 'w:shadow', border.shadow); + if (border.frame !== undefined) setBooleanAttr(edgeElement, 'w:frame', border.frame); +} + +export function readSectPrPageBorders(sectPr: XmlElement): SectionPageBorders | undefined { + const pgBorders = findChild(sectPr, 'w:pgBorders'); + if (!pgBorders) return undefined; + + const top = readBorderSpec(findChild(pgBorders, 'w:top')); + const right = readBorderSpec(findChild(pgBorders, 'w:right')); + const bottom = readBorderSpec(findChild(pgBorders, 'w:bottom')); + const left = readBorderSpec(findChild(pgBorders, 'w:left')); + + const display = toPageBorderDisplay(pgBorders.attributes?.['w:display']); + const offsetFrom = toPageBorderOffsetFrom(pgBorders.attributes?.['w:offsetFrom']); + const zOrder = toPageBorderZOrder(pgBorders.attributes?.['w:zOrder']); + + if (display == null && offsetFrom == null && zOrder == null && !top && !right && !bottom && !left) return undefined; + return { display, offsetFrom, zOrder, top, right, bottom, left }; +} + +export function writeSectPrPageBorders(sectPr: XmlElement, borders: SectionPageBorders): void { + const hasRootAttributes = + borders.display !== undefined || borders.offsetFrom !== undefined || borders.zOrder !== undefined; + const hasTop = Boolean( + borders.top && + (borders.top.style !== undefined || + borders.top.size !== undefined || + borders.top.space !== undefined || + borders.top.color !== undefined || + borders.top.shadow !== undefined || + borders.top.frame !== undefined), + ); + const hasRight = Boolean( + borders.right && + (borders.right.style !== undefined || + borders.right.size !== undefined || + borders.right.space !== undefined || + borders.right.color !== undefined || + borders.right.shadow !== undefined || + borders.right.frame !== undefined), + ); + const hasBottom = Boolean( + borders.bottom && + (borders.bottom.style !== undefined || + borders.bottom.size !== undefined || + borders.bottom.space !== undefined || + borders.bottom.color !== undefined || + borders.bottom.shadow !== undefined || + borders.bottom.frame !== undefined), + ); + const hasLeft = Boolean( + borders.left && + (borders.left.style !== undefined || + borders.left.size !== undefined || + borders.left.space !== undefined || + borders.left.color !== undefined || + borders.left.shadow !== undefined || + borders.left.frame !== undefined), + ); + + if (!hasRootAttributes && !hasTop && !hasRight && !hasBottom && !hasLeft) { + return; + } + + const pgBorders = ensureChild(sectPr, 'w:pgBorders'); + if (borders.display !== undefined) setStringAttr(pgBorders, 'w:display', borders.display); + if (borders.offsetFrom !== undefined) setStringAttr(pgBorders, 'w:offsetFrom', borders.offsetFrom); + if (borders.zOrder !== undefined) setStringAttr(pgBorders, 'w:zOrder', borders.zOrder); + if (hasTop && borders.top) writeBorderSpec(pgBorders, 'top', borders.top); + if (hasRight && borders.right) writeBorderSpec(pgBorders, 'right', borders.right); + if (hasBottom && borders.bottom) writeBorderSpec(pgBorders, 'bottom', borders.bottom); + if (hasLeft && borders.left) writeBorderSpec(pgBorders, 'left', borders.left); +} + +export function clearSectPrPageBorders(sectPr: XmlElement): boolean { + const { removed } = removeChildren(sectPr, (entry) => entry.name === 'w:pgBorders'); + return removed; +} diff --git a/packages/super-editor/src/document-api-adapters/out-of-band-mutation.ts b/packages/super-editor/src/document-api-adapters/out-of-band-mutation.ts index 4e4e40f85b..dbf27ff00d 100644 --- a/packages/super-editor/src/document-api-adapters/out-of-band-mutation.ts +++ b/packages/super-editor/src/document-api-adapters/out-of-band-mutation.ts @@ -20,7 +20,7 @@ import { checkRevision, incrementRevision } from './plan-engine/revision-tracker interface ConverterForMutation { documentModified: boolean; documentGuid: string | null; - promoteToGuid(): string; + promoteToGuid?: () => string; } /** Result returned by the mutation function passed to `executeOutOfBandMutation`. */ @@ -62,7 +62,7 @@ export function executeOutOfBandMutation( const converter = (editor as unknown as { converter?: ConverterForMutation }).converter; if (converter) { converter.documentModified = true; - if (!converter.documentGuid) { + if (!converter.documentGuid && typeof converter.promoteToGuid === 'function') { converter.promoteToGuid(); } } diff --git a/packages/super-editor/src/document-api-adapters/sections-adapter.integration.test.ts b/packages/super-editor/src/document-api-adapters/sections-adapter.integration.test.ts new file mode 100644 index 0000000000..bd5e047ef0 --- /dev/null +++ b/packages/super-editor/src/document-api-adapters/sections-adapter.integration.test.ts @@ -0,0 +1,316 @@ +/* @vitest-environment jsdom */ + +import { afterEach, beforeAll, describe, expect, it } from 'vitest'; +import { initTestEditor, loadTestDataForEditorTests } from '@tests/helpers/helpers.js'; +import DocxZipper from '@core/DocxZipper.js'; +import type { Editor } from '../core/Editor.js'; +import { + createSectionBreakAdapter, + sectionsClearHeaderFooterRefAdapter, + sectionsSetHeaderFooterRefAdapter, + sectionsSetLinkToPreviousAdapter, + sectionsSetOddEvenHeadersFootersAdapter, +} from './sections-adapter.js'; +import { resolveSectionProjections } from './helpers/sections-resolver.js'; + +type LoadedDocData = Awaited>; + +const DIRECT_MUTATION_OPTIONS = { changeMode: 'direct' } as const; + +function mapExportedFiles(files: Array<{ name: string; content: string }>): Record { + const byName: Record = {}; + for (const file of files) { + byName[file.name] = file.content; + } + return byName; +} + +async function exportDocxFiles(editor: Editor): Promise> { + const zipper = new DocxZipper(); + const exportedBuffer = await editor.exportDocx(); + const exportedFiles = await zipper.getDocxData(exportedBuffer, true); + return mapExportedFiles(exportedFiles); +} + +function getSectionAddressByIndex(editor: Editor, index: number): { kind: 'section'; sectionId: string } { + const section = resolveSectionProjections(editor).find((entry) => entry.range.sectionIndex === index); + if (!section) { + throw new Error(`Expected section index ${index} to exist.`); + } + return section.address; +} + +describe('sections adapter DOCX integration', () => { + let docData: LoadedDocData; + let editor: Editor | undefined; + + beforeAll(async () => { + docData = await loadTestDataForEditorTests('blank-doc.docx'); + }); + + afterEach(() => { + editor?.destroy(); + editor = undefined; + }); + + it('persists odd/even header-footer settings to word/settings.xml', async () => { + ({ editor } = initTestEditor({ + content: docData.docx, + media: docData.media, + mediaFiles: docData.mediaFiles, + fonts: docData.fonts, + useImmediateSetTimeout: false, + })); + + const enableResult = sectionsSetOddEvenHeadersFootersAdapter(editor, { enabled: true }, DIRECT_MUTATION_OPTIONS); + expect(enableResult.success).toBe(true); + + let exportedFiles = await exportDocxFiles(editor); + expect(exportedFiles['word/settings.xml']).toContain('w:evenAndOddHeaders'); + + const disableResult = sectionsSetOddEvenHeadersFootersAdapter(editor, { enabled: false }, DIRECT_MUTATION_OPTIONS); + expect(disableResult.success).toBe(true); + + exportedFiles = await exportDocxFiles(editor); + expect(exportedFiles['word/settings.xml']).not.toContain('w:evenAndOddHeaders'); + }); + + it('creates explicit header parts/relationships when unlinking without inherited refs', async () => { + ({ editor } = initTestEditor({ + content: docData.docx, + media: docData.media, + mediaFiles: docData.mediaFiles, + fonts: docData.fonts, + useImmediateSetTimeout: false, + })); + + const sectionBreakResult = createSectionBreakAdapter( + editor, + { at: { kind: 'documentEnd' }, breakType: 'nextPage' }, + DIRECT_MUTATION_OPTIONS, + ); + expect(sectionBreakResult.success).toBe(true); + + const targetSection = getSectionAddressByIndex(editor, 1); + const unlinkResult = sectionsSetLinkToPreviousAdapter( + editor, + { + target: targetSection, + kind: 'header', + variant: 'default', + linked: false, + }, + DIRECT_MUTATION_OPTIONS, + ); + expect(unlinkResult.success).toBe(true); + + const exportedFiles = await exportDocxFiles(editor); + const documentXml = exportedFiles['word/document.xml']; + const documentRelsXml = exportedFiles['word/_rels/document.xml.rels']; + const headerRefMatch = documentXml.match(/]*w:type="default"[^>]*r:id="([^"]+)"/); + const newHeaderRefId = headerRefMatch?.[1]; + + expect(typeof newHeaderRefId).toBe('string'); + + expect(documentXml).toContain('w:headerReference'); + expect(documentXml).toContain(`r:id="${newHeaderRefId}"`); + expect(documentRelsXml).toContain(`Id="${newHeaderRefId}"`); + expect(documentRelsXml).toContain('/relationships/header'); + + const relationshipMatch = documentRelsXml.match(new RegExp(`Id="${newHeaderRefId}"[^>]*Target="([^"]+)"`)); + expect(relationshipMatch?.[1]).toBeTruthy(); + + const relationshipTarget = relationshipMatch![1]!; + const headerPartPath = relationshipTarget.startsWith('word/') ? relationshipTarget : `word/${relationshipTarget}`; + expect(exportedFiles[headerPartPath]).toContain(' { + ({ editor } = initTestEditor({ + content: docData.docx, + media: docData.media, + mediaFiles: docData.mediaFiles, + fonts: docData.fonts, + useImmediateSetTimeout: false, + })); + + const createBreak = createSectionBreakAdapter( + editor, + { at: { kind: 'documentEnd' }, breakType: 'nextPage' }, + DIRECT_MUTATION_OPTIONS, + ); + expect(createBreak.success).toBe(true); + + const generatedSourceSection = getSectionAddressByIndex(editor, 1); + const unlinkResult = sectionsSetLinkToPreviousAdapter( + editor, + { + target: generatedSourceSection, + kind: 'footer', + variant: 'default', + linked: false, + }, + DIRECT_MUTATION_OPTIONS, + ); + expect(unlinkResult.success).toBe(true); + + const generatedFooterRefId = resolveSectionProjections(editor).find((entry) => entry.range.sectionIndex === 1) + ?.domain.footerRefs?.default; + expect(generatedFooterRefId).toBeTruthy(); + + const targetSection = getSectionAddressByIndex(editor, 0); + + const setResult = sectionsSetHeaderFooterRefAdapter( + editor, + { + target: targetSection, + kind: 'footer', + variant: 'default', + refId: generatedFooterRefId!, + }, + DIRECT_MUTATION_OPTIONS, + ); + expect(setResult.success).toBe(true); + + const converterBodySectPr = JSON.stringify( + (editor as unknown as { converter?: { bodySectPr?: unknown } }).converter?.bodySectPr, + ); + expect(converterBodySectPr).toContain(generatedFooterRefId!); + + const clearResult = sectionsClearHeaderFooterRefAdapter( + editor, + { + target: targetSection, + kind: 'footer', + variant: 'default', + }, + DIRECT_MUTATION_OPTIONS, + ); + expect(clearResult.success).toBe(true); + + const exportedFiles = await exportDocxFiles(editor); + const refIdMatches = exportedFiles['word/document.xml'].match(new RegExp(`r:id="${generatedFooterRefId!}"`, 'g')); + expect(refIdMatches?.length ?? 0).toBe(1); + }); + + it('dry-run setLinkToPrevious does not allocate header/footer parts or relationships', () => { + ({ editor } = initTestEditor({ + content: docData.docx, + media: docData.media, + mediaFiles: docData.mediaFiles, + fonts: docData.fonts, + useImmediateSetTimeout: false, + })); + + const sectionBreakResult = createSectionBreakAdapter( + editor, + { at: { kind: 'documentEnd' }, breakType: 'nextPage' }, + DIRECT_MUTATION_OPTIONS, + ); + expect(sectionBreakResult.success).toBe(true); + + const converter = (editor as unknown as { converter?: { convertedXml?: Record } }).converter!; + const xmlKeysBefore = Object.keys(converter.convertedXml ?? {}).sort(); + const relsBefore = JSON.stringify(converter.convertedXml?.['word/_rels/document.xml.rels']); + + const targetSection = getSectionAddressByIndex(editor, 1); + const dryRunResult = sectionsSetLinkToPreviousAdapter( + editor, + { + target: targetSection, + kind: 'header', + variant: 'default', + linked: false, + }, + { ...DIRECT_MUTATION_OPTIONS, dryRun: true }, + ); + expect(dryRunResult.success).toBe(true); + + // Converter state must be untouched — no new parts, no new relationships. + const xmlKeysAfter = Object.keys(converter.convertedXml ?? {}).sort(); + const relsAfter = JSON.stringify(converter.convertedXml?.['word/_rels/document.xml.rels']); + expect(xmlKeysAfter).toEqual(xmlKeysBefore); + expect(relsAfter).toEqual(relsBefore); + }); + + it('dry-run setOddEvenHeadersFooters does not create word/settings.xml when absent', () => { + ({ editor } = initTestEditor({ + content: docData.docx, + media: docData.media, + mediaFiles: docData.mediaFiles, + fonts: docData.fonts, + useImmediateSetTimeout: false, + })); + + const converter = (editor as unknown as { converter?: { convertedXml?: Record } }).converter!; + + // Remove word/settings.xml if it exists so we can verify it is not re-created. + if (converter.convertedXml) { + delete converter.convertedXml['word/settings.xml']; + } + + const dryRunResult = sectionsSetOddEvenHeadersFootersAdapter( + editor, + { enabled: true }, + { ...DIRECT_MUTATION_OPTIONS, dryRun: true }, + ); + expect(dryRunResult.success).toBe(true); + + // settings.xml must NOT have been created during dry-run. + expect(converter.convertedXml?.['word/settings.xml']).toBeUndefined(); + }); + + it('NO_OP setOddEvenHeadersFooters does not create word/settings.xml when absent', () => { + ({ editor } = initTestEditor({ + content: docData.docx, + media: docData.media, + mediaFiles: docData.mediaFiles, + fonts: docData.fonts, + useImmediateSetTimeout: false, + })); + + const converter = (editor as unknown as { converter?: { convertedXml?: Record } }).converter!; + + // Remove word/settings.xml so the NO_OP path (enabled: false when already false) is tested. + if (converter.convertedXml) { + delete converter.convertedXml['word/settings.xml']; + } + + // Odd/even is already false (absent), requesting false → NO_OP. + const noOpResult = sectionsSetOddEvenHeadersFootersAdapter(editor, { enabled: false }, DIRECT_MUTATION_OPTIONS); + expect(noOpResult.success).toBe(false); + if (!noOpResult.success) { + expect(noOpResult.failure.code).toBe('NO_OP'); + } + + // settings.xml must NOT have been created for a NO_OP. + expect(converter.convertedXml?.['word/settings.xml']).toBeUndefined(); + }); + + it('rejects header/footer refs that are missing from document relationships', () => { + ({ editor } = initTestEditor({ + content: docData.docx, + media: docData.media, + mediaFiles: docData.mediaFiles, + fonts: docData.fonts, + useImmediateSetTimeout: false, + })); + + const targetSection = getSectionAddressByIndex(editor, 0); + const setResult = sectionsSetHeaderFooterRefAdapter( + editor, + { + target: targetSection, + kind: 'header', + variant: 'default', + refId: 'rIdMissingRelationship', + }, + DIRECT_MUTATION_OPTIONS, + ); + + expect(setResult.success).toBe(false); + if (!setResult.success) { + expect(setResult.failure.code).toBe('INVALID_TARGET'); + } + }); +}); diff --git a/packages/super-editor/src/document-api-adapters/sections-adapter.ts b/packages/super-editor/src/document-api-adapters/sections-adapter.ts new file mode 100644 index 0000000000..199a330341 --- /dev/null +++ b/packages/super-editor/src/document-api-adapters/sections-adapter.ts @@ -0,0 +1,696 @@ +import type { + CreateSectionBreakInput, + CreateSectionBreakResult, + DocumentMutationResult, + MutationOptions, + SectionAddress, + SectionMutationResult, + SectionsClearHeaderFooterRefInput, + SectionsClearPageBordersInput, + SectionsGetInput, + SectionsListQuery, + SectionsListResult, + SectionsSetBreakTypeInput, + SectionsSetColumnsInput, + SectionsSetHeaderFooterMarginsInput, + SectionsSetHeaderFooterRefInput, + SectionsSetLineNumberingInput, + SectionsSetLinkToPreviousInput, + SectionsSetOddEvenHeadersFootersInput, + SectionsSetPageBordersInput, + SectionsSetPageMarginsInput, + SectionsSetPageNumberingInput, + SectionsSetPageSetupInput, + SectionsSetSectionDirectionInput, + SectionsSetTitlePageInput, + SectionsSetVerticalAlignInput, + SectionInfo, +} from '@superdoc/document-api'; +import type { Node as ProseMirrorNode } from 'prosemirror-model'; +import type { Editor } from '../core/Editor.js'; +import { DocumentApiAdapterError } from './errors.js'; +import { applyDirectMutationMeta } from './helpers/transaction-meta.js'; +import { checkRevision } from './plan-engine/revision-tracker.js'; +import { resolveBlockInsertionPos } from './plan-engine/create-insertion.js'; +import { clearIndexCache } from './helpers/index-cache.js'; +import { rejectTrackedMode } from './helpers/mutation-helpers.js'; +import { executeOutOfBandMutation } from './out-of-band-mutation.js'; +import { + ensureSettingsRoot, + readSettingsRoot, + hasOddEvenHeadersFooters, + setOddEvenHeadersFooters as setOddEvenHeadersInSettings, + type ConverterWithDocumentSettings, +} from './document-settings.js'; +import { + getBodySectPrFromEditor, + getDefaultSectionAddress, + resolveSectionProjections, + sectionsGetAdapter, + sectionsListAdapter as listSectionsFromProjection, + type SectionProjection, +} from './helpers/sections-resolver.js'; +import { + createHeaderFooterPart, + hasHeaderFooterRelationship, + type ConverterWithHeaderFooterParts, +} from './helpers/header-footer-parts.js'; +import { + clearSectPrHeaderFooterRef, + clearSectPrPageBorders, + cloneXmlElement, + createSectPrElement, + ensureSectPrElement, + getSectPrHeaderFooterRef, + readSectPrHeaderFooterRefs, + readSectPrMargins, + setSectPrHeaderFooterRef, + writeSectPrBreakType, + writeSectPrColumns, + writeSectPrDirection, + writeSectPrHeaderFooterMargins, + writeSectPrLineNumbering, + writeSectPrPageBorders, + writeSectPrPageMargins, + writeSectPrPageNumbering, + writeSectPrPageSetup, + writeSectPrTitlePage, + writeSectPrVerticalAlign, + type XmlElement, +} from './helpers/sections-xml.js'; + +interface ConverterWithSections extends ConverterWithDocumentSettings, ConverterWithHeaderFooterParts { + bodySectPr?: unknown; + savedTagsToRestore?: Array<{ + name?: string; + elements?: Array<{ + name?: string; + [key: string]: unknown; + }>; + [key: string]: unknown; + }>; + pageStyles?: { + pageMargins?: { + top?: number; + right?: number; + bottom?: number; + left?: number; + header?: number; + footer?: number; + gutter?: number; + }; + alternateHeaders?: boolean; + }; +} + +function getConverter(editor: Editor): ConverterWithSections | undefined { + return (editor as unknown as { converter?: ConverterWithSections }).converter; +} + +function toSectionFailure( + code: 'NO_OP' | 'INVALID_TARGET' | 'CAPABILITY_UNAVAILABLE', + message: string, +): SectionMutationResult { + return { + success: false, + failure: { + code, + message, + }, + }; +} + +function toSectionSuccess(section: SectionAddress): SectionMutationResult { + return { + success: true, + section, + }; +} + +function toDocumentSuccess(): DocumentMutationResult { + return { success: true }; +} + +function toCreateFailure(code: 'INVALID_TARGET' | 'CAPABILITY_UNAVAILABLE', message: string): CreateSectionBreakResult { + return { + success: false, + failure: { + code, + message, + }, + }; +} + +function toCreateSuccess( + section: SectionAddress, + breakParagraphId: string, +): Extract { + return { + success: true, + section, + breakParagraph: { + kind: 'block', + nodeType: 'paragraph', + nodeId: breakParagraphId, + }, + }; +} + +function createSectionBreakId(): string { + const randomUuid = globalThis.crypto?.randomUUID?.(); + if (randomUuid) return randomUuid; + return `section-break-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; +} + +function readParagraphSectPr(node: ProseMirrorNode): XmlElement | null { + const attrs = (node.attrs ?? {}) as { + paragraphProperties?: { + sectPr?: unknown; + }; + }; + const sectPr = attrs.paragraphProperties?.sectPr; + return sectPr && typeof sectPr === 'object' ? (sectPr as XmlElement) : null; +} + +function readTargetSectPr(editor: Editor, projection: SectionProjection): XmlElement | null { + if (projection.target.kind === 'paragraph') { + return readParagraphSectPr(projection.target.node); + } + return getBodySectPrFromEditor(editor); +} + +function buildSectionMarginsForAttrs(sectPr: XmlElement): Record { + const margins = readSectPrMargins(sectPr); + return { + top: margins.top ?? null, + right: margins.right ?? null, + bottom: margins.bottom ?? null, + left: margins.left ?? null, + header: margins.header ?? null, + footer: margins.footer ?? null, + }; +} + +function syncConverterBodySection(editor: Editor, sectPr: XmlElement): void { + const converter = getConverter(editor); + if (!converter) return; + converter.bodySectPr = cloneXmlElement(sectPr); + + const savedBodyNode = converter.savedTagsToRestore?.find((entry) => entry?.name === 'w:body'); + if (savedBodyNode && Array.isArray(savedBodyNode.elements)) { + const preservedChildren = savedBodyNode.elements.filter((entry) => entry?.name !== 'w:sectPr'); + preservedChildren.push(cloneXmlElement(sectPr) as unknown as { name?: string; [key: string]: unknown }); + savedBodyNode.elements = preservedChildren; + } + + const margins = readSectPrMargins(sectPr); + if (!converter.pageStyles) converter.pageStyles = {}; + if (!converter.pageStyles.pageMargins) converter.pageStyles.pageMargins = {}; + const pageMargins = converter.pageStyles.pageMargins; + if (margins.top !== undefined) pageMargins.top = margins.top; + if (margins.right !== undefined) pageMargins.right = margins.right; + if (margins.bottom !== undefined) pageMargins.bottom = margins.bottom; + if (margins.left !== undefined) pageMargins.left = margins.left; + if (margins.header !== undefined) pageMargins.header = margins.header; + if (margins.footer !== undefined) pageMargins.footer = margins.footer; + if (margins.gutter !== undefined) pageMargins.gutter = margins.gutter; +} + +function applySectPrToProjection(editor: Editor, projection: SectionProjection, sectPr: XmlElement): void { + if (projection.target.kind === 'paragraph') { + const paragraph = projection.target.node; + const attrs = (paragraph.attrs ?? {}) as Record; + const paragraphProperties = { + ...((attrs.paragraphProperties ?? {}) as Record), + sectPr, + }; + const nextAttrs: Record = { + ...attrs, + paragraphProperties, + pageBreakSource: 'sectPr', + sectionMargins: buildSectionMarginsForAttrs(sectPr), + }; + + const tr = applyDirectMutationMeta(editor.state.tr); + tr.setNodeMarkup(projection.target.pos, undefined, nextAttrs, paragraph.marks); + tr.setMeta('forceUpdatePagination', true); + editor.dispatch(tr); + return; + } + + const docAttrs = (editor.state.doc.attrs ?? {}) as Record; + const tr = applyDirectMutationMeta(editor.state.tr); + tr.setNodeMarkup(0, undefined, { ...docAttrs, bodySectPr: sectPr }); + tr.setMeta('forceUpdatePagination', true); + editor.dispatch(tr); + syncConverterBodySection(editor, sectPr); +} + +function sectionMutationBySectPr( + editor: Editor, + input: TInput, + options: MutationOptions | undefined, + operationName: string, + mutate: ( + sectPr: XmlElement, + projection: SectionProjection, + sections: SectionProjection[], + dryRun: boolean, + ) => SectionMutationResult | void, +): SectionMutationResult { + rejectTrackedMode(operationName, options); + checkRevision(editor, options?.expectedRevision); + + const sections = resolveSectionProjections(editor); + const projection = sections.find((entry) => entry.sectionId === input.target.sectionId); + if (!projection) { + throw new DocumentApiAdapterError('TARGET_NOT_FOUND', 'Section target was not found.', { target: input.target }); + } + + const dryRun = options?.dryRun === true; + + const currentSectPr = readTargetSectPr(editor, projection); + const nextSectPr = ensureSectPrElement(currentSectPr); + const before = JSON.stringify(nextSectPr); + const earlyResult = mutate(nextSectPr, projection, sections, dryRun); + if (earlyResult) return earlyResult; + + const changed = before !== JSON.stringify(nextSectPr); + if (!changed) { + return toSectionFailure('NO_OP', `${operationName} did not produce a section change.`); + } + + if (options?.dryRun) { + return toSectionSuccess(projection.address); + } + + applySectPrToProjection(editor, projection, nextSectPr); + clearIndexCache(editor); + return toSectionSuccess(projection.address); +} + +function resolveInsertPosition(editor: Editor, location: CreateSectionBreakInput['at']): number { + const target = location ?? { kind: 'documentEnd' }; + if (target.kind === 'documentStart') return 0; + if (target.kind === 'documentEnd') return editor.state.doc.content.size; + return resolveBlockInsertionPos(editor, target.target.nodeId, target.kind); +} + +function buildSectPrFromCreateInput(input: CreateSectionBreakInput): XmlElement { + const sectPr = createSectPrElement(); + if (input.breakType) writeSectPrBreakType(sectPr, input.breakType); + if (input.pageMargins) writeSectPrPageMargins(sectPr, input.pageMargins); + if (input.headerFooterMargins) writeSectPrHeaderFooterMargins(sectPr, input.headerFooterMargins); + return sectPr; +} + +function createSectionBreakNode( + editor: Editor, + breakParagraphId: string, + input: CreateSectionBreakInput, +): ProseMirrorNode { + const paragraphType = editor.state.schema.nodes.paragraph; + if (!paragraphType) { + throw new DocumentApiAdapterError('CAPABILITY_UNAVAILABLE', 'create.sectionBreak requires a paragraph node type.'); + } + + const sectPr = buildSectPrFromCreateInput(input); + const attrs = { + sdBlockId: breakParagraphId, + paragraphProperties: { + sectPr, + }, + pageBreakSource: 'sectPr', + sectionMargins: buildSectionMarginsForAttrs(sectPr), + }; + + const paragraphNode = paragraphType.createAndFill(attrs, undefined) ?? paragraphType.create(attrs, undefined); + if (!paragraphNode) { + throw new DocumentApiAdapterError('INVALID_TARGET', 'Unable to construct a section-break paragraph node.'); + } + return paragraphNode; +} + +function updateGlobalTitlePageFlag(editor: Editor): void { + const converter = getConverter(editor); + if (!converter) return; + + const anyTitlePage = resolveSectionProjections(editor).some((entry) => entry.domain.titlePage === true); + if (!converter.headerIds) converter.headerIds = {}; + if (!converter.footerIds) converter.footerIds = {}; + converter.headerIds.titlePg = anyTitlePage; + converter.footerIds.titlePg = anyTitlePage; +} + +function createExplicitHeaderFooterReference( + editor: Editor, + input: { + kind: SectionsSetLinkToPreviousInput['kind']; + variant: SectionsSetLinkToPreviousInput['variant']; + sourceRefId?: string; + }, +): string | null { + const converter = getConverter(editor); + + // Fallback path when no converter is available: reuse an inherited reference if present. + if (!converter) { + return input.sourceRefId ?? null; + } + + try { + const { refId } = createHeaderFooterPart(converter, { + kind: input.kind, + variant: input.variant, + sourceRefId: input.sourceRefId, + }); + return refId; + } catch { + return null; + } +} + +export function createSectionBreakAdapter( + editor: Editor, + input: CreateSectionBreakInput, + options?: MutationOptions, +): CreateSectionBreakResult { + rejectTrackedMode('create.sectionBreak', options); + checkRevision(editor, options?.expectedRevision); + + const breakParagraphId = options?.dryRun ? '(dry-run)' : createSectionBreakId(); + const insertPos = resolveInsertPosition(editor, input.at); + const paragraphNode = createSectionBreakNode(editor, breakParagraphId, input); + + try { + const testTr = editor.state.tr.insert(insertPos, paragraphNode); + if (options?.dryRun) { + void testTr; + return toCreateSuccess({ kind: 'section', sectionId: 'section-(dry-run)' }, breakParagraphId); + } + } catch { + return toCreateFailure('INVALID_TARGET', 'create.sectionBreak could not insert at the requested location.'); + } + + const tr = applyDirectMutationMeta(editor.state.tr.insert(insertPos, paragraphNode)); + tr.setMeta('forceUpdatePagination', true); + editor.dispatch(tr); + clearIndexCache(editor); + + const createdSection = resolveSectionProjections(editor).find( + (projection) => projection.target.kind === 'paragraph' && projection.target.nodeId === breakParagraphId, + ); + return toCreateSuccess(createdSection?.address ?? getDefaultSectionAddress(editor), breakParagraphId); +} + +export function sectionsListAdapter(editor: Editor, query?: SectionsListQuery): SectionsListResult { + return listSectionsFromProjection(editor, query); +} + +export function sectionsGetAdapterByInput(editor: Editor, input: SectionsGetInput): SectionInfo { + return sectionsGetAdapter(editor, input.address); +} + +export function sectionsSetBreakTypeAdapter( + editor: Editor, + input: SectionsSetBreakTypeInput, + options?: MutationOptions, +): SectionMutationResult { + return sectionMutationBySectPr(editor, input, options, 'sections.setBreakType', (sectPr) => { + writeSectPrBreakType(sectPr, input.breakType); + }); +} + +export function sectionsSetPageMarginsAdapter( + editor: Editor, + input: SectionsSetPageMarginsInput, + options?: MutationOptions, +): SectionMutationResult { + return sectionMutationBySectPr(editor, input, options, 'sections.setPageMargins', (sectPr) => { + writeSectPrPageMargins(sectPr, input); + }); +} + +export function sectionsSetHeaderFooterMarginsAdapter( + editor: Editor, + input: SectionsSetHeaderFooterMarginsInput, + options?: MutationOptions, +): SectionMutationResult { + return sectionMutationBySectPr(editor, input, options, 'sections.setHeaderFooterMargins', (sectPr) => { + writeSectPrHeaderFooterMargins(sectPr, input); + }); +} + +export function sectionsSetPageSetupAdapter( + editor: Editor, + input: SectionsSetPageSetupInput, + options?: MutationOptions, +): SectionMutationResult { + return sectionMutationBySectPr(editor, input, options, 'sections.setPageSetup', (sectPr) => { + writeSectPrPageSetup(sectPr, input); + }); +} + +export function sectionsSetColumnsAdapter( + editor: Editor, + input: SectionsSetColumnsInput, + options?: MutationOptions, +): SectionMutationResult { + return sectionMutationBySectPr(editor, input, options, 'sections.setColumns', (sectPr) => { + writeSectPrColumns(sectPr, input); + }); +} + +export function sectionsSetLineNumberingAdapter( + editor: Editor, + input: SectionsSetLineNumberingInput, + options?: MutationOptions, +): SectionMutationResult { + return sectionMutationBySectPr(editor, input, options, 'sections.setLineNumbering', (sectPr) => { + writeSectPrLineNumbering(sectPr, input); + }); +} + +export function sectionsSetPageNumberingAdapter( + editor: Editor, + input: SectionsSetPageNumberingInput, + options?: MutationOptions, +): SectionMutationResult { + return sectionMutationBySectPr(editor, input, options, 'sections.setPageNumbering', (sectPr) => { + writeSectPrPageNumbering(sectPr, input); + }); +} + +export function sectionsSetTitlePageAdapter( + editor: Editor, + input: SectionsSetTitlePageInput, + options?: MutationOptions, +): SectionMutationResult { + const result = sectionMutationBySectPr(editor, input, options, 'sections.setTitlePage', (sectPr) => { + writeSectPrTitlePage(sectPr, input.enabled); + }); + if (result.success && !options?.dryRun) { + updateGlobalTitlePageFlag(editor); + } + return result; +} + +export function sectionsSetOddEvenHeadersFootersAdapter( + editor: Editor, + input: SectionsSetOddEvenHeadersFootersInput, + options?: MutationOptions, +): DocumentMutationResult { + rejectTrackedMode('sections.setOddEvenHeadersFooters', options); + + const converter = getConverter(editor); + if (!converter) { + throw new DocumentApiAdapterError( + 'CAPABILITY_UNAVAILABLE', + 'sections.setOddEvenHeadersFooters requires an active document converter.', + ); + } + + return executeOutOfBandMutation( + editor, + (dryRun) => { + // Read-only check first — avoids creating word/settings.xml on dry-run or NO_OP paths. + const existingRoot = readSettingsRoot(converter); + const before = existingRoot ? hasOddEvenHeadersFooters(existingRoot) : false; + const changed = before !== input.enabled; + + if (!changed) { + return { + changed: false, + payload: toSectionFailure( + 'NO_OP', + 'sections.setOddEvenHeadersFooters did not produce a document settings change.', + ), + }; + } + + if (!dryRun) { + // Only now create the settings part if needed. + const settingsRoot = ensureSettingsRoot(converter); + setOddEvenHeadersInSettings(settingsRoot, input.enabled); + if (!converter.pageStyles) converter.pageStyles = {}; + converter.pageStyles.alternateHeaders = input.enabled; + } + + return { + changed, + payload: toDocumentSuccess(), + }; + }, + { + dryRun: options?.dryRun === true, + expectedRevision: options?.expectedRevision, + }, + ); +} + +export function sectionsSetVerticalAlignAdapter( + editor: Editor, + input: SectionsSetVerticalAlignInput, + options?: MutationOptions, +): SectionMutationResult { + return sectionMutationBySectPr(editor, input, options, 'sections.setVerticalAlign', (sectPr) => { + writeSectPrVerticalAlign(sectPr, input.value); + }); +} + +export function sectionsSetSectionDirectionAdapter( + editor: Editor, + input: SectionsSetSectionDirectionInput, + options?: MutationOptions, +): SectionMutationResult { + return sectionMutationBySectPr(editor, input, options, 'sections.setSectionDirection', (sectPr) => { + writeSectPrDirection(sectPr, input.direction); + }); +} + +export function sectionsSetHeaderFooterRefAdapter( + editor: Editor, + input: SectionsSetHeaderFooterRefInput, + options?: MutationOptions, +): SectionMutationResult { + return sectionMutationBySectPr(editor, input, options, 'sections.setHeaderFooterRef', (sectPr) => { + const converter = getConverter(editor); + if (!converter) { + return toSectionFailure( + 'CAPABILITY_UNAVAILABLE', + 'sections.setHeaderFooterRef requires an active document converter to validate relationship references.', + ); + } + + const relationshipExists = hasHeaderFooterRelationship(converter, { + kind: input.kind, + refId: input.refId, + }); + if (!relationshipExists) { + return toSectionFailure( + 'INVALID_TARGET', + `sections.setHeaderFooterRef could not find ${input.kind} relationship "${input.refId}" in word/_rels/document.xml.rels.`, + ); + } + + const currentRef = getSectPrHeaderFooterRef(sectPr, input.kind, input.variant); + if (currentRef === input.refId) { + return toSectionFailure('NO_OP', 'sections.setHeaderFooterRef already matches the requested reference.'); + } + setSectPrHeaderFooterRef(sectPr, input.kind, input.variant, input.refId); + }); +} + +export function sectionsClearHeaderFooterRefAdapter( + editor: Editor, + input: SectionsClearHeaderFooterRefInput, + options?: MutationOptions, +): SectionMutationResult { + return sectionMutationBySectPr(editor, input, options, 'sections.clearHeaderFooterRef', (sectPr) => { + clearSectPrHeaderFooterRef(sectPr, input.kind, input.variant); + }); +} + +export function sectionsSetLinkToPreviousAdapter( + editor: Editor, + input: SectionsSetLinkToPreviousInput, + options?: MutationOptions, +): SectionMutationResult { + return sectionMutationBySectPr( + editor, + input, + options, + 'sections.setLinkToPrevious', + (sectPr, projection, sections, dryRun) => { + if (projection.range.sectionIndex === 0) { + return toSectionFailure('INVALID_TARGET', 'sections.setLinkToPrevious cannot target the first section.'); + } + + if (input.linked) { + const removed = clearSectPrHeaderFooterRef(sectPr, input.kind, input.variant); + if (!removed) { + return toSectionFailure('NO_OP', 'sections.setLinkToPrevious found no explicit reference to remove.'); + } + return; + } + + const existing = getSectPrHeaderFooterRef(sectPr, input.kind, input.variant); + if (existing) { + return toSectionFailure('NO_OP', 'sections.setLinkToPrevious already has an explicit reference.'); + } + + const previous = sections.find((entry) => entry.range.sectionIndex === projection.range.sectionIndex - 1); + if (!previous) { + return toSectionFailure('INVALID_TARGET', 'sections.setLinkToPrevious requires a previous section.'); + } + + const previousSectPr = readTargetSectPr(editor, previous); + if (!previousSectPr) { + return toSectionFailure('INVALID_TARGET', 'Previous section has no reference to inherit.'); + } + + const refs = readSectPrHeaderFooterRefs(previousSectPr, input.kind); + const inheritedRef = refs?.[input.variant] ?? refs?.default; + + // During dry-run, skip part allocation to avoid mutating converter state. + // Use a sentinel ref ID so the sectPr change is still detected. + if (dryRun) { + setSectPrHeaderFooterRef(sectPr, input.kind, input.variant, '(dry-run)'); + return; + } + + const explicitRefId = createExplicitHeaderFooterReference(editor, { + kind: input.kind, + variant: input.variant, + sourceRefId: inheritedRef, + }); + if (!explicitRefId) { + return toSectionFailure( + 'CAPABILITY_UNAVAILABLE', + 'sections.setLinkToPrevious could not allocate an explicit header/footer reference for this section.', + ); + } + + setSectPrHeaderFooterRef(sectPr, input.kind, input.variant, explicitRefId); + }, + ); +} + +export function sectionsSetPageBordersAdapter( + editor: Editor, + input: SectionsSetPageBordersInput, + options?: MutationOptions, +): SectionMutationResult { + return sectionMutationBySectPr(editor, input, options, 'sections.setPageBorders', (sectPr) => { + writeSectPrPageBorders(sectPr, input.borders); + }); +} + +export function sectionsClearPageBordersAdapter( + editor: Editor, + input: SectionsClearPageBordersInput, + options?: MutationOptions, +): SectionMutationResult { + return sectionMutationBySectPr(editor, input, options, 'sections.clearPageBorders', (sectPr) => { + clearSectPrPageBorders(sectPr); + }); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c02a67941..468f0d4d6d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -422,6 +422,9 @@ importers: '@superdoc/document-api': specifier: workspace:* version: link:../../packages/document-api + '@superdoc/pm-adapter': + specifier: workspace:* + version: link:../../packages/layout-engine/pm-adapter '@superdoc/super-editor': specifier: workspace:* version: link:../../packages/super-editor