From d6c8aee19ec412d6e3dc9feda3599a84445a2279 Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Fri, 27 Feb 2026 15:22:40 -0800 Subject: [PATCH 1/3] feat(document-api): format.paragraph for w:pPr formatting --- apps/cli/scripts/export-sdk-contract.ts | 1 - .../src/__tests__/conformance/scenarios.ts | 239 ++++- apps/cli/src/cli/operation-hints.ts | 181 +++- apps/cli/src/cli/operation-params.ts | 1 - apps/cli/src/lib/invoke-input.ts | 1 - apps/cli/src/lib/special-handlers.ts | 1 - .../document-api/available-operations.mdx | 24 +- .../reference/_generated-manifest.json | 58 +- .../reference/capabilities/get.mdx | 886 +++++++++++++++++- .../document-api/reference/format/align.mdx | 243 ----- .../document-api/reference/format/index.mdx | 1 - .../format/paragraph/clear-alignment.mdx | 333 +++++++ .../format/paragraph/clear-all-tab-stops.mdx | 333 +++++++ .../format/paragraph/clear-border.mdx | 347 +++++++ .../format/paragraph/clear-indentation.mdx | 333 +++++++ .../format/paragraph/clear-shading.mdx | 333 +++++++ .../format/paragraph/clear-spacing.mdx | 333 +++++++ .../format/paragraph/clear-tab-stop.mdx | 340 +++++++ .../reference/format/paragraph/index.mdx | 34 + .../paragraph/reset-direct-formatting.mdx | 333 +++++++ .../format/paragraph/set-alignment.mdx | 344 +++++++ .../reference/format/paragraph/set-border.mdx | 370 ++++++++ .../format/paragraph/set-flow-options.mdx | 368 ++++++++ .../format/paragraph/set-indentation.mdx | 384 ++++++++ .../format/paragraph/set-keep-options.mdx | 368 ++++++++ .../format/paragraph/set-outline-level.mdx | 348 +++++++ .../format/paragraph/set-shading.mdx | 371 ++++++++ .../format/paragraph/set-spacing.mdx | 391 ++++++++ .../format/paragraph/set-tab-stop.mdx | 364 +++++++ apps/docs/document-api/reference/index.mdx | 34 +- .../styles/paragraph/clear-style.mdx | 333 +++++++ .../reference/styles/paragraph/index.mdx | 19 + .../reference/styles/paragraph/set-style.mdx | 340 +++++++ apps/docs/document-engine/sdks.mdx | 1 - .../src/contract/contract.test.ts | 2 + .../src/contract/operation-definitions.ts | 320 ++++++- .../src/contract/operation-registry.ts | 123 ++- .../src/contract/reference-doc-map.ts | 10 + packages/document-api/src/contract/schemas.ts | 276 +++++- .../document-api/src/contract/types.test.ts | 10 +- packages/document-api/src/contract/types.ts | 4 +- .../document-api/src/format/format.test.ts | 43 +- packages/document-api/src/format/format.ts | 49 - packages/document-api/src/index.test.ts | 24 - packages/document-api/src/index.ts | 167 +++- .../document-api/src/invoke/invoke.test.ts | 13 - packages/document-api/src/invoke/invoke.ts | 24 +- .../document-api/src/paragraphs/paragraphs.ts | 674 +++++++++++++ .../src/paragraphs/paragraphs.types.ts | 190 ++++ .../contract-conformance.test.ts | 609 +++++++++++- .../assemble-adapters.test.ts | 21 +- .../assemble-adapters.ts | 46 +- .../capabilities-adapter.test.ts | 21 +- .../capabilities-adapter.ts | 22 +- .../helpers/node-info-mapper.ts | 14 +- .../plan-engine/format-value-wrappers.ts | 57 -- .../plan-engine/paragraphs-wrappers.ts | 624 ++++++++++++ .../src/extensions/types/node-attributes.ts | 6 +- .../tests/formatting/inline-formatting.ts | 42 +- 59 files changed, 11109 insertions(+), 672 deletions(-) delete mode 100644 apps/docs/document-api/reference/format/align.mdx create mode 100644 apps/docs/document-api/reference/format/paragraph/clear-alignment.mdx create mode 100644 apps/docs/document-api/reference/format/paragraph/clear-all-tab-stops.mdx create mode 100644 apps/docs/document-api/reference/format/paragraph/clear-border.mdx create mode 100644 apps/docs/document-api/reference/format/paragraph/clear-indentation.mdx create mode 100644 apps/docs/document-api/reference/format/paragraph/clear-shading.mdx create mode 100644 apps/docs/document-api/reference/format/paragraph/clear-spacing.mdx create mode 100644 apps/docs/document-api/reference/format/paragraph/clear-tab-stop.mdx create mode 100644 apps/docs/document-api/reference/format/paragraph/index.mdx create mode 100644 apps/docs/document-api/reference/format/paragraph/reset-direct-formatting.mdx create mode 100644 apps/docs/document-api/reference/format/paragraph/set-alignment.mdx create mode 100644 apps/docs/document-api/reference/format/paragraph/set-border.mdx create mode 100644 apps/docs/document-api/reference/format/paragraph/set-flow-options.mdx create mode 100644 apps/docs/document-api/reference/format/paragraph/set-indentation.mdx create mode 100644 apps/docs/document-api/reference/format/paragraph/set-keep-options.mdx create mode 100644 apps/docs/document-api/reference/format/paragraph/set-outline-level.mdx create mode 100644 apps/docs/document-api/reference/format/paragraph/set-shading.mdx create mode 100644 apps/docs/document-api/reference/format/paragraph/set-spacing.mdx create mode 100644 apps/docs/document-api/reference/format/paragraph/set-tab-stop.mdx create mode 100644 apps/docs/document-api/reference/styles/paragraph/clear-style.mdx create mode 100644 apps/docs/document-api/reference/styles/paragraph/index.mdx create mode 100644 apps/docs/document-api/reference/styles/paragraph/set-style.mdx create mode 100644 packages/document-api/src/paragraphs/paragraphs.ts create mode 100644 packages/document-api/src/paragraphs/paragraphs.types.ts create mode 100644 packages/super-editor/src/document-api-adapters/plan-engine/paragraphs-wrappers.ts diff --git a/apps/cli/scripts/export-sdk-contract.ts b/apps/cli/scripts/export-sdk-contract.ts index e8c15a0430..a1613efab3 100644 --- a/apps/cli/scripts/export-sdk-contract.ts +++ b/apps/cli/scripts/export-sdk-contract.ts @@ -63,7 +63,6 @@ const INTENT_NAMES = { 'doc.format.fontSize': 'format_font_size', 'doc.format.fontFamily': 'format_font_family', 'doc.format.color': 'format_color', - 'doc.format.align': 'format_align', 'doc.styles.apply': 'styles_apply', 'doc.create.paragraph': 'create_paragraph', 'doc.create.heading': 'create_heading', diff --git a/apps/cli/src/__tests__/conformance/scenarios.ts b/apps/cli/src/__tests__/conformance/scenarios.ts index c6993055b7..3f07539de0 100644 --- a/apps/cli/src/__tests__/conformance/scenarios.ts +++ b/apps/cli/src/__tests__/conformance/scenarios.ts @@ -138,6 +138,89 @@ function sectionMutationScenario( }; } +// --------------------------------------------------------------------------- +// Paragraph scenario helpers (DRY builder for format.paragraph.* / styles.paragraph.*) +// --------------------------------------------------------------------------- + +function paragraphMutationScenario( + 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); + const { address } = await harness.firstBlockMatch(docPath, stateDir); + return { + stateDir, + args: [ + ...commandTokens(operationId), + docPath, + '--target-json', + JSON.stringify(address), + ...extraArgs, + '--out', + harness.createOutputPath(`${label}-output`), + ], + }; + }; +} + +/** + * Clear-style paragraph scenario: pre-seeds the target paragraph with a "set" + * operation so the subsequent "clear" actually removes something (not a NO_OP). + * + * After seeding we re-discover the block address because the output doc may + * assign new node IDs after serialization. + */ +function paragraphClearScenario( + operationId: CliOperationId, + label: string, + clearArgs: string[], + seedOperationId: CliOperationId, + seedArgs: string[], +): (harness: ConformanceHarness) => Promise { + return async (harness) => { + const stateDir = await harness.createStateDir(`${label}-success`); + const sourceDoc = await harness.copyFixtureDoc(`${label}-source`); + const { address } = await harness.firstBlockMatch(sourceDoc, stateDir); + + // Pre-seed: run a set operation so there is something to clear. + const seededDoc = harness.createOutputPath(`${label}-seeded`); + const seed = await harness.runCli( + [ + ...commandTokens(seedOperationId), + sourceDoc, + '--target-json', + JSON.stringify(address), + ...seedArgs, + '--out', + seededDoc, + ], + stateDir, + ); + if (seed.result.code !== 0) { + throw new Error(`Pre-seed failed for ${label}: ${seed.result.stderr}`); + } + + // Re-discover target in the seeded doc (node IDs may change after export). + const { address: seededAddress } = await harness.firstBlockMatch(seededDoc, stateDir); + + return { + stateDir, + args: [ + ...commandTokens(operationId), + seededDoc, + '--target-json', + JSON.stringify(seededAddress), + ...clearArgs, + '--out', + harness.createOutputPath(`${label}-output`), + ], + }; + }; +} + // --------------------------------------------------------------------------- // Table scenario helpers (DRY builders for the 40 table operations) // --------------------------------------------------------------------------- @@ -1051,25 +1134,6 @@ export const SUCCESS_SCENARIOS = { ], }; }, - 'doc.format.align': async (harness: ConformanceHarness): Promise => { - const stateDir = await harness.createStateDir('doc-format-align-success'); - const docPath = await harness.copyFixtureDoc('doc-format-align'); - const target = await harness.firstTextRange(docPath, stateDir); - return { - stateDir, - args: [ - 'format', - 'align', - docPath, - '--target-json', - JSON.stringify(target), - '--alignment-json', - JSON.stringify('center'), - '--out', - harness.createOutputPath('doc-format-align-output'), - ], - }; - }, 'doc.styles.apply': async (harness: ConformanceHarness): Promise => { const stateDir = await harness.createStateDir('doc-styles-apply-success'); const docPath = await harness.copyFixtureDoc('doc-styles-apply'); @@ -1311,6 +1375,143 @@ export const SUCCESS_SCENARIOS = { 'doc.tables.get': tableReadScenario('tables.get'), 'doc.tables.getCells': tableReadScenario('tables.getCells'), 'doc.tables.getProperties': tableReadScenario('tables.getProperties'), + + // Paragraphs — styles.paragraph.* + 'doc.styles.paragraph.setStyle': paragraphMutationScenario('doc.styles.paragraph.setStyle', 'para-set-style', [ + '--style-id', + 'Normal', + ]), + 'doc.styles.paragraph.clearStyle': async (harness) => { + // Target a heading node (which natively carries a styleId) rather than + // pre-seeding, because styleId doesn't survive the export round-trip. + const label = 'para-clear-style'; + const stateDir = await harness.createStateDir(`${label}-success`); + const docPath = await harness.copyFixtureDoc(`${label}-source`); + const { result, envelope } = await harness.runCli( + ['find', docPath, '--type', 'node', '--node-type', 'heading', '--limit', '1'], + stateDir, + ); + if (result.code !== 0 || !envelope.ok) { + throw new Error(`No heading found for ${label}`); + } + const address = (envelope.data as { result?: { items?: Array<{ address?: Record }> } }).result + ?.items?.[0]?.address; + if (!address) throw new Error(`No heading address for ${label}`); + return { + stateDir, + args: [ + ...commandTokens('doc.styles.paragraph.clearStyle'), + docPath, + '--target-json', + JSON.stringify(address), + '--out', + harness.createOutputPath(`${label}-output`), + ], + }; + }, + + // Paragraphs — format.paragraph.* + 'doc.format.paragraph.resetDirectFormatting': paragraphClearScenario( + 'doc.format.paragraph.resetDirectFormatting', + 'para-reset-direct', + [], + 'doc.format.paragraph.setAlignment', + ['--alignment', 'right'], + ), + 'doc.format.paragraph.setAlignment': paragraphMutationScenario( + 'doc.format.paragraph.setAlignment', + 'para-set-alignment', + ['--alignment', 'right'], + ), + 'doc.format.paragraph.clearAlignment': paragraphClearScenario( + 'doc.format.paragraph.clearAlignment', + 'para-clear-alignment', + [], + 'doc.format.paragraph.setAlignment', + ['--alignment', 'right'], + ), + 'doc.format.paragraph.setIndentation': paragraphMutationScenario( + 'doc.format.paragraph.setIndentation', + 'para-set-indent', + ['--left', '720'], + ), + 'doc.format.paragraph.clearIndentation': paragraphClearScenario( + 'doc.format.paragraph.clearIndentation', + 'para-clear-indent', + [], + 'doc.format.paragraph.setIndentation', + ['--left', '720'], + ), + 'doc.format.paragraph.setSpacing': paragraphMutationScenario('doc.format.paragraph.setSpacing', 'para-set-spacing', [ + '--before', + '240', + ]), + 'doc.format.paragraph.clearSpacing': paragraphClearScenario( + 'doc.format.paragraph.clearSpacing', + 'para-clear-spacing', + [], + 'doc.format.paragraph.setSpacing', + ['--before', '240'], + ), + 'doc.format.paragraph.setKeepOptions': paragraphMutationScenario( + 'doc.format.paragraph.setKeepOptions', + 'para-set-keep', + ['--keep-next', 'true'], + ), + 'doc.format.paragraph.setOutlineLevel': paragraphMutationScenario( + 'doc.format.paragraph.setOutlineLevel', + 'para-set-outline', + ['--outline-level-json', '1'], + ), + 'doc.format.paragraph.setFlowOptions': paragraphMutationScenario( + 'doc.format.paragraph.setFlowOptions', + 'para-set-flow', + ['--page-break-before', 'true'], + ), + 'doc.format.paragraph.setTabStop': paragraphMutationScenario('doc.format.paragraph.setTabStop', 'para-set-tab', [ + '--position', + '720', + '--alignment', + 'left', + ]), + 'doc.format.paragraph.clearTabStop': paragraphClearScenario( + 'doc.format.paragraph.clearTabStop', + 'para-clear-tab', + ['--position', '720'], + 'doc.format.paragraph.setTabStop', + ['--position', '720', '--alignment', 'left'], + ), + 'doc.format.paragraph.clearAllTabStops': paragraphClearScenario( + 'doc.format.paragraph.clearAllTabStops', + 'para-clear-all-tabs', + [], + 'doc.format.paragraph.setTabStop', + ['--position', '720', '--alignment', 'left'], + ), + 'doc.format.paragraph.setBorder': paragraphMutationScenario('doc.format.paragraph.setBorder', 'para-set-border', [ + '--side', + 'top', + '--style', + 'single', + ]), + 'doc.format.paragraph.clearBorder': paragraphClearScenario( + 'doc.format.paragraph.clearBorder', + 'para-clear-border', + ['--side', 'all'], + 'doc.format.paragraph.setBorder', + ['--side', 'top', '--style', 'single'], + ), + 'doc.format.paragraph.setShading': paragraphMutationScenario('doc.format.paragraph.setShading', 'para-set-shading', [ + '--fill', + 'FF0000', + ]), + 'doc.format.paragraph.clearShading': paragraphClearScenario( + 'doc.format.paragraph.clearShading', + 'para-clear-shading', + [], + 'doc.format.paragraph.setShading', + ['--fill', 'FF0000'], + ), } as const satisfies Record Promise>; export const OPERATION_SCENARIOS = (Object.keys(SUCCESS_SCENARIOS) as CliOperationId[]).map((operationId) => { diff --git a/apps/cli/src/cli/operation-hints.ts b/apps/cli/src/cli/operation-hints.ts index b002e3fdcf..6492b4c93f 100644 --- a/apps/cli/src/cli/operation-hints.ts +++ b/apps/cli/src/cli/operation-hints.ts @@ -40,7 +40,6 @@ export const SUCCESS_VERB: Record = { 'format.fontSize': 'set font size', 'format.fontFamily': 'set font family', 'format.color': 'set text color', - 'format.align': 'set alignment', 'styles.apply': 'applied stylesheet defaults', 'create.paragraph': 'created paragraph', 'create.heading': 'created heading', @@ -106,6 +105,50 @@ export const SUCCESS_VERB: Record = { 'tables.get': 'resolved table', 'tables.getCells': 'listed cells', 'tables.getProperties': 'resolved table properties', + + // Sections + 'create.sectionBreak': 'created section break', + 'sections.list': 'listed sections', + 'sections.get': 'resolved section', + 'sections.setBreakType': 'set break type', + 'sections.setPageMargins': 'set page margins', + 'sections.setHeaderFooterMargins': 'set header/footer margins', + 'sections.setPageSetup': 'set page setup', + 'sections.setColumns': 'set columns', + 'sections.setLineNumbering': 'set line numbering', + 'sections.setPageNumbering': 'set page numbering', + 'sections.setTitlePage': 'set title page', + 'sections.setOddEvenHeadersFooters': 'set odd/even headers/footers', + 'sections.setVerticalAlign': 'set vertical alignment', + 'sections.setSectionDirection': 'set section direction', + 'sections.setHeaderFooterRef': 'set header/footer reference', + 'sections.clearHeaderFooterRef': 'cleared header/footer reference', + 'sections.setLinkToPrevious': 'set link to previous', + 'sections.setPageBorders': 'set page borders', + 'sections.clearPageBorders': 'cleared page borders', + + // Paragraphs — format.paragraph.* + 'format.paragraph.resetDirectFormatting': 'reset direct formatting', + 'format.paragraph.setAlignment': 'set paragraph alignment', + 'format.paragraph.clearAlignment': 'cleared paragraph alignment', + 'format.paragraph.setIndentation': 'set paragraph indentation', + 'format.paragraph.clearIndentation': 'cleared paragraph indentation', + 'format.paragraph.setSpacing': 'set paragraph spacing', + 'format.paragraph.clearSpacing': 'cleared paragraph spacing', + 'format.paragraph.setKeepOptions': 'set paragraph keep options', + 'format.paragraph.setOutlineLevel': 'set outline level', + 'format.paragraph.setFlowOptions': 'set paragraph flow options', + 'format.paragraph.setTabStop': 'set tab stop', + 'format.paragraph.clearTabStop': 'cleared tab stop', + 'format.paragraph.clearAllTabStops': 'cleared all tab stops', + 'format.paragraph.setBorder': 'set paragraph border', + 'format.paragraph.clearBorder': 'cleared paragraph border', + 'format.paragraph.setShading': 'set paragraph shading', + 'format.paragraph.clearShading': 'cleared paragraph shading', + + // Paragraphs — styles.paragraph.* + 'styles.paragraph.setStyle': 'set paragraph style', + 'styles.paragraph.clearStyle': 'cleared paragraph style', }; // --------------------------------------------------------------------------- @@ -149,7 +192,6 @@ export const OUTPUT_FORMAT: Record = { 'format.fontSize': 'mutationReceipt', 'format.fontFamily': 'mutationReceipt', 'format.color': 'mutationReceipt', - 'format.align': 'mutationReceipt', 'styles.apply': 'receipt', 'create.paragraph': 'createResult', 'create.heading': 'createResult', @@ -215,6 +257,50 @@ export const OUTPUT_FORMAT: Record = { 'tables.get': 'tableInfo', 'tables.getCells': 'tableCellList', 'tables.getProperties': 'tablePropertiesInfo', + + // Sections + 'create.sectionBreak': 'createResult', + 'sections.list': 'plain', + 'sections.get': 'plain', + 'sections.setBreakType': 'receipt', + 'sections.setPageMargins': 'receipt', + 'sections.setHeaderFooterMargins': 'receipt', + 'sections.setPageSetup': 'receipt', + 'sections.setColumns': 'receipt', + 'sections.setLineNumbering': 'receipt', + 'sections.setPageNumbering': 'receipt', + 'sections.setTitlePage': 'receipt', + 'sections.setOddEvenHeadersFooters': 'receipt', + 'sections.setVerticalAlign': 'receipt', + 'sections.setSectionDirection': 'receipt', + 'sections.setHeaderFooterRef': 'receipt', + 'sections.clearHeaderFooterRef': 'receipt', + 'sections.setLinkToPrevious': 'receipt', + 'sections.setPageBorders': 'receipt', + 'sections.clearPageBorders': 'receipt', + + // Paragraphs — format.paragraph.* + 'format.paragraph.resetDirectFormatting': 'receipt', + 'format.paragraph.setAlignment': 'receipt', + 'format.paragraph.clearAlignment': 'receipt', + 'format.paragraph.setIndentation': 'receipt', + 'format.paragraph.clearIndentation': 'receipt', + 'format.paragraph.setSpacing': 'receipt', + 'format.paragraph.clearSpacing': 'receipt', + 'format.paragraph.setKeepOptions': 'receipt', + 'format.paragraph.setOutlineLevel': 'receipt', + 'format.paragraph.setFlowOptions': 'receipt', + 'format.paragraph.setTabStop': 'receipt', + 'format.paragraph.clearTabStop': 'receipt', + 'format.paragraph.clearAllTabStops': 'receipt', + 'format.paragraph.setBorder': 'receipt', + 'format.paragraph.clearBorder': 'receipt', + 'format.paragraph.setShading': 'receipt', + 'format.paragraph.clearShading': 'receipt', + + // Paragraphs — styles.paragraph.* + 'styles.paragraph.setStyle': 'receipt', + 'styles.paragraph.clearStyle': 'receipt', }; // --------------------------------------------------------------------------- @@ -242,7 +328,6 @@ export const RESPONSE_ENVELOPE_KEY: Record 'format.fontSize': null, 'format.fontFamily': null, 'format.color': null, - 'format.align': null, 'styles.apply': 'receipt', 'create.paragraph': 'result', 'create.heading': 'result', @@ -308,6 +393,50 @@ export const RESPONSE_ENVELOPE_KEY: Record 'tables.get': 'result', 'tables.getCells': 'result', 'tables.getProperties': 'result', + + // Sections + 'create.sectionBreak': 'result', + 'sections.list': 'result', + 'sections.get': 'result', + 'sections.setBreakType': 'result', + 'sections.setPageMargins': 'result', + 'sections.setHeaderFooterMargins': 'result', + 'sections.setPageSetup': 'result', + 'sections.setColumns': 'result', + 'sections.setLineNumbering': 'result', + 'sections.setPageNumbering': 'result', + 'sections.setTitlePage': 'result', + 'sections.setOddEvenHeadersFooters': 'result', + 'sections.setVerticalAlign': 'result', + 'sections.setSectionDirection': 'result', + 'sections.setHeaderFooterRef': 'result', + 'sections.clearHeaderFooterRef': 'result', + 'sections.setLinkToPrevious': 'result', + 'sections.setPageBorders': 'result', + 'sections.clearPageBorders': 'result', + + // Paragraphs — format.paragraph.* + 'format.paragraph.resetDirectFormatting': 'result', + 'format.paragraph.setAlignment': 'result', + 'format.paragraph.clearAlignment': 'result', + 'format.paragraph.setIndentation': 'result', + 'format.paragraph.clearIndentation': 'result', + 'format.paragraph.setSpacing': 'result', + 'format.paragraph.clearSpacing': 'result', + 'format.paragraph.setKeepOptions': 'result', + 'format.paragraph.setOutlineLevel': 'result', + 'format.paragraph.setFlowOptions': 'result', + 'format.paragraph.setTabStop': 'result', + 'format.paragraph.clearTabStop': 'result', + 'format.paragraph.clearAllTabStops': 'result', + 'format.paragraph.setBorder': 'result', + 'format.paragraph.clearBorder': 'result', + 'format.paragraph.setShading': 'result', + 'format.paragraph.clearShading': 'result', + + // Paragraphs — styles.paragraph.* + 'styles.paragraph.setStyle': 'result', + 'styles.paragraph.clearStyle': 'result', }; // --------------------------------------------------------------------------- @@ -329,7 +458,6 @@ export const RESPONSE_VALIDATION_KEY: Partial = 'format.fontSize': 'textMutation', 'format.fontFamily': 'textMutation', 'format.color': 'textMutation', - 'format.align': 'textMutation', 'styles.apply': 'general', 'create.paragraph': 'create', 'create.heading': 'create', @@ -431,4 +558,48 @@ export const OPERATION_FAMILY: Record = 'tables.get': 'tables', 'tables.getCells': 'tables', 'tables.getProperties': 'tables', + + // Sections + 'create.sectionBreak': 'create', + 'sections.list': 'general', + 'sections.get': 'general', + 'sections.setBreakType': 'general', + 'sections.setPageMargins': 'general', + 'sections.setHeaderFooterMargins': 'general', + 'sections.setPageSetup': 'general', + 'sections.setColumns': 'general', + 'sections.setLineNumbering': 'general', + 'sections.setPageNumbering': 'general', + 'sections.setTitlePage': 'general', + 'sections.setOddEvenHeadersFooters': 'general', + 'sections.setVerticalAlign': 'general', + 'sections.setSectionDirection': 'general', + 'sections.setHeaderFooterRef': 'general', + 'sections.clearHeaderFooterRef': 'general', + 'sections.setLinkToPrevious': 'general', + 'sections.setPageBorders': 'general', + 'sections.clearPageBorders': 'general', + + // Paragraphs — format.paragraph.* + 'format.paragraph.resetDirectFormatting': 'general', + 'format.paragraph.setAlignment': 'general', + 'format.paragraph.clearAlignment': 'general', + 'format.paragraph.setIndentation': 'general', + 'format.paragraph.clearIndentation': 'general', + 'format.paragraph.setSpacing': 'general', + 'format.paragraph.clearSpacing': 'general', + 'format.paragraph.setKeepOptions': 'general', + 'format.paragraph.setOutlineLevel': 'general', + 'format.paragraph.setFlowOptions': 'general', + 'format.paragraph.setTabStop': 'general', + 'format.paragraph.clearTabStop': 'general', + 'format.paragraph.clearAllTabStops': 'general', + 'format.paragraph.setBorder': 'general', + 'format.paragraph.clearBorder': 'general', + 'format.paragraph.setShading': 'general', + 'format.paragraph.clearShading': 'general', + + // Paragraphs — styles.paragraph.* + 'styles.paragraph.setStyle': 'general', + 'styles.paragraph.clearStyle': 'general', }; diff --git a/apps/cli/src/cli/operation-params.ts b/apps/cli/src/cli/operation-params.ts index 15f615fb99..e2702d2e7a 100644 --- a/apps/cli/src/cli/operation-params.ts +++ b/apps/cli/src/cli/operation-params.ts @@ -350,7 +350,6 @@ const EXTRA_CLI_PARAMS: Partial> = { 'doc.format.fontSize': [...TEXT_TARGET_FLAT_PARAMS], 'doc.format.fontFamily': [...TEXT_TARGET_FLAT_PARAMS], 'doc.format.color': [...TEXT_TARGET_FLAT_PARAMS], - 'doc.format.align': [...TEXT_TARGET_FLAT_PARAMS], 'doc.styles.apply': [ { name: 'target', kind: 'jsonFlag', flag: 'target-json', type: 'json' }, { name: 'patch', kind: 'jsonFlag', flag: 'patch-json', type: 'json' }, diff --git a/apps/cli/src/lib/invoke-input.ts b/apps/cli/src/lib/invoke-input.ts index 80c66a7d83..813cd1737c 100644 --- a/apps/cli/src/lib/invoke-input.ts +++ b/apps/cli/src/lib/invoke-input.ts @@ -77,7 +77,6 @@ const TEXT_TARGET_OPERATIONS = new Set([ 'format.fontSize', 'format.fontFamily', 'format.color', - 'format.align', 'comments.create', 'comments.patch', ]); diff --git a/apps/cli/src/lib/special-handlers.ts b/apps/cli/src/lib/special-handlers.ts index fe37441e4b..78fb318f62 100644 --- a/apps/cli/src/lib/special-handlers.ts +++ b/apps/cli/src/lib/special-handlers.ts @@ -247,7 +247,6 @@ export const POST_INVOKE_HOOKS: Partial { const record = asRecord(result); diff --git a/apps/docs/document-api/available-operations.mdx b/apps/docs/document-api/available-operations.mdx index 8cf00b3f6b..4f92d3df93 100644 --- a/apps/docs/document-api/available-operations.mdx +++ b/apps/docs/document-api/available-operations.mdx @@ -19,9 +19,11 @@ Use the tables below to see what operations are available and where each one is | Comments | 5 | 0 | 5 | [Reference](/document-api/reference/comments/index) | | Core | 8 | 0 | 8 | [Reference](/document-api/reference/core/index) | | Create | 4 | 0 | 4 | [Reference](/document-api/reference/create/index) | -| Format | 5 | 4 | 9 | [Reference](/document-api/reference/format/index) | +| Format | 4 | 4 | 8 | [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) | +| Paragraph Formatting | 17 | 0 | 17 | [Reference](/document-api/reference/format/paragraph/index) | +| Paragraph Styles | 2 | 0 | 2 | [Reference](/document-api/reference/styles/paragraph/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) | @@ -53,7 +55,6 @@ Use the tables below to see what operations are available and where each one is | editor.doc.format.fontSize(...) | [`format.fontSize`](/document-api/reference/format/font-size) | | editor.doc.format.fontFamily(...) | [`format.fontFamily`](/document-api/reference/format/font-family) | | editor.doc.format.color(...) | [`format.color`](/document-api/reference/format/color) | -| editor.doc.format.align(...) | [`format.align`](/document-api/reference/format/align) | | editor.doc.format.bold(...) | [`format.apply`](/document-api/reference/format/apply) | | editor.doc.format.italic(...) | [`format.apply`](/document-api/reference/format/apply) | | editor.doc.format.underline(...) | [`format.apply`](/document-api/reference/format/apply) | @@ -68,6 +69,25 @@ Use the tables below to see what operations are available and where each one is | editor.doc.lists.exit(...) | [`lists.exit`](/document-api/reference/lists/exit) | | editor.doc.mutations.preview(...) | [`mutations.preview`](/document-api/reference/mutations/preview) | | editor.doc.mutations.apply(...) | [`mutations.apply`](/document-api/reference/mutations/apply) | +| editor.doc.format.paragraph.resetDirectFormatting(...) | [`format.paragraph.resetDirectFormatting`](/document-api/reference/format/paragraph/reset-direct-formatting) | +| editor.doc.format.paragraph.setAlignment(...) | [`format.paragraph.setAlignment`](/document-api/reference/format/paragraph/set-alignment) | +| editor.doc.format.paragraph.clearAlignment(...) | [`format.paragraph.clearAlignment`](/document-api/reference/format/paragraph/clear-alignment) | +| editor.doc.format.paragraph.setIndentation(...) | [`format.paragraph.setIndentation`](/document-api/reference/format/paragraph/set-indentation) | +| editor.doc.format.paragraph.clearIndentation(...) | [`format.paragraph.clearIndentation`](/document-api/reference/format/paragraph/clear-indentation) | +| editor.doc.format.paragraph.setSpacing(...) | [`format.paragraph.setSpacing`](/document-api/reference/format/paragraph/set-spacing) | +| editor.doc.format.paragraph.clearSpacing(...) | [`format.paragraph.clearSpacing`](/document-api/reference/format/paragraph/clear-spacing) | +| editor.doc.format.paragraph.setKeepOptions(...) | [`format.paragraph.setKeepOptions`](/document-api/reference/format/paragraph/set-keep-options) | +| editor.doc.format.paragraph.setOutlineLevel(...) | [`format.paragraph.setOutlineLevel`](/document-api/reference/format/paragraph/set-outline-level) | +| editor.doc.format.paragraph.setFlowOptions(...) | [`format.paragraph.setFlowOptions`](/document-api/reference/format/paragraph/set-flow-options) | +| editor.doc.format.paragraph.setTabStop(...) | [`format.paragraph.setTabStop`](/document-api/reference/format/paragraph/set-tab-stop) | +| editor.doc.format.paragraph.clearTabStop(...) | [`format.paragraph.clearTabStop`](/document-api/reference/format/paragraph/clear-tab-stop) | +| editor.doc.format.paragraph.clearAllTabStops(...) | [`format.paragraph.clearAllTabStops`](/document-api/reference/format/paragraph/clear-all-tab-stops) | +| editor.doc.format.paragraph.setBorder(...) | [`format.paragraph.setBorder`](/document-api/reference/format/paragraph/set-border) | +| editor.doc.format.paragraph.clearBorder(...) | [`format.paragraph.clearBorder`](/document-api/reference/format/paragraph/clear-border) | +| editor.doc.format.paragraph.setShading(...) | [`format.paragraph.setShading`](/document-api/reference/format/paragraph/set-shading) | +| editor.doc.format.paragraph.clearShading(...) | [`format.paragraph.clearShading`](/document-api/reference/format/paragraph/clear-shading) | +| editor.doc.styles.paragraph.setStyle(...) | [`styles.paragraph.setStyle`](/document-api/reference/styles/paragraph/set-style) | +| editor.doc.styles.paragraph.clearStyle(...) | [`styles.paragraph.clearStyle`](/document-api/reference/styles/paragraph/clear-style) | | 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) | diff --git a/apps/docs/document-api/reference/_generated-manifest.json b/apps/docs/document-api/reference/_generated-manifest.json index c7f7325269..f2891033e7 100644 --- a/apps/docs/document-api/reference/_generated-manifest.json +++ b/apps/docs/document-api/reference/_generated-manifest.json @@ -19,12 +19,29 @@ "apps/docs/document-api/reference/create/table.mdx", "apps/docs/document-api/reference/delete.mdx", "apps/docs/document-api/reference/find.mdx", - "apps/docs/document-api/reference/format/align.mdx", "apps/docs/document-api/reference/format/apply.mdx", "apps/docs/document-api/reference/format/color.mdx", "apps/docs/document-api/reference/format/font-family.mdx", "apps/docs/document-api/reference/format/font-size.mdx", "apps/docs/document-api/reference/format/index.mdx", + "apps/docs/document-api/reference/format/paragraph/clear-alignment.mdx", + "apps/docs/document-api/reference/format/paragraph/clear-all-tab-stops.mdx", + "apps/docs/document-api/reference/format/paragraph/clear-border.mdx", + "apps/docs/document-api/reference/format/paragraph/clear-indentation.mdx", + "apps/docs/document-api/reference/format/paragraph/clear-shading.mdx", + "apps/docs/document-api/reference/format/paragraph/clear-spacing.mdx", + "apps/docs/document-api/reference/format/paragraph/clear-tab-stop.mdx", + "apps/docs/document-api/reference/format/paragraph/index.mdx", + "apps/docs/document-api/reference/format/paragraph/reset-direct-formatting.mdx", + "apps/docs/document-api/reference/format/paragraph/set-alignment.mdx", + "apps/docs/document-api/reference/format/paragraph/set-border.mdx", + "apps/docs/document-api/reference/format/paragraph/set-flow-options.mdx", + "apps/docs/document-api/reference/format/paragraph/set-indentation.mdx", + "apps/docs/document-api/reference/format/paragraph/set-keep-options.mdx", + "apps/docs/document-api/reference/format/paragraph/set-outline-level.mdx", + "apps/docs/document-api/reference/format/paragraph/set-shading.mdx", + "apps/docs/document-api/reference/format/paragraph/set-spacing.mdx", + "apps/docs/document-api/reference/format/paragraph/set-tab-stop.mdx", "apps/docs/document-api/reference/get-node-by-id.mdx", "apps/docs/document-api/reference/get-node.mdx", "apps/docs/document-api/reference/get-text.mdx", @@ -67,6 +84,9 @@ "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/styles/paragraph/clear-style.mdx", + "apps/docs/document-api/reference/styles/paragraph/index.mdx", + "apps/docs/document-api/reference/styles/paragraph/set-style.mdx", "apps/docs/document-api/reference/tables/apply-border-preset.mdx", "apps/docs/document-api/reference/tables/clear-border.mdx", "apps/docs/document-api/reference/tables/clear-cell-spacing.mdx", @@ -171,7 +191,7 @@ { "aliasMemberPaths": ["format.bold", "format.italic", "format.underline", "format.strikethrough"], "key": "format", - "operationIds": ["format.apply", "format.fontSize", "format.fontFamily", "format.color", "format.align"], + "operationIds": ["format.apply", "format.fontSize", "format.fontFamily", "format.color"], "pagePath": "apps/docs/document-api/reference/format/index.mdx", "title": "Format" }, @@ -226,6 +246,38 @@ "pagePath": "apps/docs/document-api/reference/mutations/index.mdx", "title": "Mutations" }, + { + "aliasMemberPaths": [], + "key": "format.paragraph", + "operationIds": [ + "format.paragraph.resetDirectFormatting", + "format.paragraph.setAlignment", + "format.paragraph.clearAlignment", + "format.paragraph.setIndentation", + "format.paragraph.clearIndentation", + "format.paragraph.setSpacing", + "format.paragraph.clearSpacing", + "format.paragraph.setKeepOptions", + "format.paragraph.setOutlineLevel", + "format.paragraph.setFlowOptions", + "format.paragraph.setTabStop", + "format.paragraph.clearTabStop", + "format.paragraph.clearAllTabStops", + "format.paragraph.setBorder", + "format.paragraph.clearBorder", + "format.paragraph.setShading", + "format.paragraph.clearShading" + ], + "pagePath": "apps/docs/document-api/reference/format/paragraph/index.mdx", + "title": "Paragraph Formatting" + }, + { + "aliasMemberPaths": [], + "key": "styles.paragraph", + "operationIds": ["styles.paragraph.setStyle", "styles.paragraph.clearStyle"], + "pagePath": "apps/docs/document-api/reference/styles/paragraph/index.mdx", + "title": "Paragraph Styles" + }, { "aliasMemberPaths": [], "key": "tables", @@ -275,5 +327,5 @@ } ], "marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}", - "sourceHash": "8b0c27a3eff9d8548a4de7b931f4a577774027635048dc42492f0500d00963b1" + "sourceHash": "84c0ed0bd327fcc839d8e96cc05e9461f3ac16ac599e13bc82b380bbd9b3b36a" } diff --git a/apps/docs/document-api/reference/capabilities/get.mdx b/apps/docs/document-api/reference/capabilities/get.mdx index f705fe7e39..cebef5061a 100644 --- a/apps/docs/document-api/reference/capabilities/get.mdx +++ b/apps/docs/document-api/reference/capabilities/get.mdx @@ -194,7 +194,7 @@ _No fields._ ], "tracked": true }, - "format.align": { + "format.apply": { "available": true, "dryRun": true, "reasons": [ @@ -202,7 +202,7 @@ _No fields._ ], "tracked": true }, - "format.apply": { + "format.color": { "available": true, "dryRun": true, "reasons": [ @@ -210,7 +210,7 @@ _No fields._ ], "tracked": true }, - "format.color": { + "format.fontFamily": { "available": true, "dryRun": true, "reasons": [ @@ -218,7 +218,7 @@ _No fields._ ], "tracked": true }, - "format.fontFamily": { + "format.fontSize": { "available": true, "dryRun": true, "reasons": [ @@ -226,7 +226,135 @@ _No fields._ ], "tracked": true }, - "format.fontSize": { + "format.paragraph.clearAlignment": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.paragraph.clearAllTabStops": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.paragraph.clearBorder": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.paragraph.clearIndentation": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.paragraph.clearShading": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.paragraph.clearSpacing": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.paragraph.clearTabStop": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.paragraph.resetDirectFormatting": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.paragraph.setAlignment": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.paragraph.setBorder": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.paragraph.setFlowOptions": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.paragraph.setIndentation": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.paragraph.setKeepOptions": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.paragraph.setOutlineLevel": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.paragraph.setShading": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.paragraph.setSpacing": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.paragraph.setTabStop": { "available": true, "dryRun": true, "reasons": [ @@ -522,6 +650,22 @@ _No fields._ ], "tracked": true }, + "styles.paragraph.clearStyle": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "styles.paragraph.setStyle": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, "tables.applyBorderPreset": { "available": true, "dryRun": true, @@ -1570,7 +1714,7 @@ _No fields._ ], "type": "object" }, - "format.align": { + "format.apply": { "additionalProperties": false, "properties": { "available": { @@ -1605,7 +1749,7 @@ _No fields._ ], "type": "object" }, - "format.apply": { + "format.color": { "additionalProperties": false, "properties": { "available": { @@ -1640,7 +1784,7 @@ _No fields._ ], "type": "object" }, - "format.color": { + "format.fontFamily": { "additionalProperties": false, "properties": { "available": { @@ -1675,7 +1819,7 @@ _No fields._ ], "type": "object" }, - "format.fontFamily": { + "format.fontSize": { "additionalProperties": false, "properties": { "available": { @@ -1710,7 +1854,7 @@ _No fields._ ], "type": "object" }, - "format.fontSize": { + "format.paragraph.clearAlignment": { "additionalProperties": false, "properties": { "available": { @@ -1745,7 +1889,7 @@ _No fields._ ], "type": "object" }, - "getNode": { + "format.paragraph.clearAllTabStops": { "additionalProperties": false, "properties": { "available": { @@ -1780,7 +1924,7 @@ _No fields._ ], "type": "object" }, - "getNodeById": { + "format.paragraph.clearBorder": { "additionalProperties": false, "properties": { "available": { @@ -1815,7 +1959,7 @@ _No fields._ ], "type": "object" }, - "getText": { + "format.paragraph.clearIndentation": { "additionalProperties": false, "properties": { "available": { @@ -1850,7 +1994,7 @@ _No fields._ ], "type": "object" }, - "info": { + "format.paragraph.clearShading": { "additionalProperties": false, "properties": { "available": { @@ -1885,7 +2029,7 @@ _No fields._ ], "type": "object" }, - "insert": { + "format.paragraph.clearSpacing": { "additionalProperties": false, "properties": { "available": { @@ -1920,7 +2064,7 @@ _No fields._ ], "type": "object" }, - "lists.exit": { + "format.paragraph.clearTabStop": { "additionalProperties": false, "properties": { "available": { @@ -1955,7 +2099,7 @@ _No fields._ ], "type": "object" }, - "lists.get": { + "format.paragraph.resetDirectFormatting": { "additionalProperties": false, "properties": { "available": { @@ -1990,7 +2134,7 @@ _No fields._ ], "type": "object" }, - "lists.indent": { + "format.paragraph.setAlignment": { "additionalProperties": false, "properties": { "available": { @@ -2025,7 +2169,7 @@ _No fields._ ], "type": "object" }, - "lists.insert": { + "format.paragraph.setBorder": { "additionalProperties": false, "properties": { "available": { @@ -2060,7 +2204,7 @@ _No fields._ ], "type": "object" }, - "lists.list": { + "format.paragraph.setFlowOptions": { "additionalProperties": false, "properties": { "available": { @@ -2095,7 +2239,7 @@ _No fields._ ], "type": "object" }, - "lists.outdent": { + "format.paragraph.setIndentation": { "additionalProperties": false, "properties": { "available": { @@ -2130,7 +2274,7 @@ _No fields._ ], "type": "object" }, - "lists.restart": { + "format.paragraph.setKeepOptions": { "additionalProperties": false, "properties": { "available": { @@ -2165,7 +2309,7 @@ _No fields._ ], "type": "object" }, - "lists.setType": { + "format.paragraph.setOutlineLevel": { "additionalProperties": false, "properties": { "available": { @@ -2200,7 +2344,7 @@ _No fields._ ], "type": "object" }, - "mutations.apply": { + "format.paragraph.setShading": { "additionalProperties": false, "properties": { "available": { @@ -2235,7 +2379,7 @@ _No fields._ ], "type": "object" }, - "mutations.preview": { + "format.paragraph.setSpacing": { "additionalProperties": false, "properties": { "available": { @@ -2270,7 +2414,7 @@ _No fields._ ], "type": "object" }, - "query.match": { + "format.paragraph.setTabStop": { "additionalProperties": false, "properties": { "available": { @@ -2305,7 +2449,7 @@ _No fields._ ], "type": "object" }, - "replace": { + "getNode": { "additionalProperties": false, "properties": { "available": { @@ -2340,7 +2484,7 @@ _No fields._ ], "type": "object" }, - "sections.clearHeaderFooterRef": { + "getNodeById": { "additionalProperties": false, "properties": { "available": { @@ -2375,7 +2519,7 @@ _No fields._ ], "type": "object" }, - "sections.clearPageBorders": { + "getText": { "additionalProperties": false, "properties": { "available": { @@ -2410,7 +2554,7 @@ _No fields._ ], "type": "object" }, - "sections.get": { + "info": { "additionalProperties": false, "properties": { "available": { @@ -2445,7 +2589,7 @@ _No fields._ ], "type": "object" }, - "sections.list": { + "insert": { "additionalProperties": false, "properties": { "available": { @@ -2480,7 +2624,7 @@ _No fields._ ], "type": "object" }, - "sections.setBreakType": { + "lists.exit": { "additionalProperties": false, "properties": { "available": { @@ -2515,7 +2659,7 @@ _No fields._ ], "type": "object" }, - "sections.setColumns": { + "lists.get": { "additionalProperties": false, "properties": { "available": { @@ -2550,7 +2694,7 @@ _No fields._ ], "type": "object" }, - "sections.setHeaderFooterMargins": { + "lists.indent": { "additionalProperties": false, "properties": { "available": { @@ -2585,7 +2729,7 @@ _No fields._ ], "type": "object" }, - "sections.setHeaderFooterRef": { + "lists.insert": { "additionalProperties": false, "properties": { "available": { @@ -2620,7 +2764,7 @@ _No fields._ ], "type": "object" }, - "sections.setLineNumbering": { + "lists.list": { "additionalProperties": false, "properties": { "available": { @@ -2655,7 +2799,7 @@ _No fields._ ], "type": "object" }, - "sections.setLinkToPrevious": { + "lists.outdent": { "additionalProperties": false, "properties": { "available": { @@ -2690,7 +2834,7 @@ _No fields._ ], "type": "object" }, - "sections.setOddEvenHeadersFooters": { + "lists.restart": { "additionalProperties": false, "properties": { "available": { @@ -2725,7 +2869,7 @@ _No fields._ ], "type": "object" }, - "sections.setPageBorders": { + "lists.setType": { "additionalProperties": false, "properties": { "available": { @@ -2760,7 +2904,7 @@ _No fields._ ], "type": "object" }, - "sections.setPageMargins": { + "mutations.apply": { "additionalProperties": false, "properties": { "available": { @@ -2795,7 +2939,7 @@ _No fields._ ], "type": "object" }, - "sections.setPageNumbering": { + "mutations.preview": { "additionalProperties": false, "properties": { "available": { @@ -2830,7 +2974,7 @@ _No fields._ ], "type": "object" }, - "sections.setPageSetup": { + "query.match": { "additionalProperties": false, "properties": { "available": { @@ -2865,7 +3009,7 @@ _No fields._ ], "type": "object" }, - "sections.setSectionDirection": { + "replace": { "additionalProperties": false, "properties": { "available": { @@ -2900,7 +3044,7 @@ _No fields._ ], "type": "object" }, - "sections.setTitlePage": { + "sections.clearHeaderFooterRef": { "additionalProperties": false, "properties": { "available": { @@ -2935,7 +3079,7 @@ _No fields._ ], "type": "object" }, - "sections.setVerticalAlign": { + "sections.clearPageBorders": { "additionalProperties": false, "properties": { "available": { @@ -2970,7 +3114,637 @@ _No fields._ ], "type": "object" }, - "styles.apply": { + "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": { + "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.paragraph.clearStyle": { + "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.paragraph.setStyle": { "additionalProperties": false, "properties": { "available": { @@ -4490,7 +5264,6 @@ _No fields._ "format.fontSize", "format.fontFamily", "format.color", - "format.align", "styles.apply", "create.paragraph", "create.heading", @@ -4513,6 +5286,25 @@ _No fields._ "sections.setLinkToPrevious", "sections.setPageBorders", "sections.clearPageBorders", + "styles.paragraph.setStyle", + "styles.paragraph.clearStyle", + "format.paragraph.resetDirectFormatting", + "format.paragraph.setAlignment", + "format.paragraph.clearAlignment", + "format.paragraph.setIndentation", + "format.paragraph.clearIndentation", + "format.paragraph.setSpacing", + "format.paragraph.clearSpacing", + "format.paragraph.setKeepOptions", + "format.paragraph.setOutlineLevel", + "format.paragraph.setFlowOptions", + "format.paragraph.setTabStop", + "format.paragraph.clearTabStop", + "format.paragraph.clearAllTabStops", + "format.paragraph.setBorder", + "format.paragraph.clearBorder", + "format.paragraph.setShading", + "format.paragraph.clearShading", "lists.list", "lists.get", "lists.insert", diff --git a/apps/docs/document-api/reference/format/align.mdx b/apps/docs/document-api/reference/format/align.mdx deleted file mode 100644 index b347b66417..0000000000 --- a/apps/docs/document-api/reference/format/align.mdx +++ /dev/null @@ -1,243 +0,0 @@ ---- -title: format.align -sidebarTitle: 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`. */} - -> Alpha: Document API is currently alpha and subject to breaking changes. - -## 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` -- Idempotency: `conditional` -- Supports tracked mode: `no` -- 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 | -| --- | --- | --- | --- | -| `alignment` | enum \\| null | yes | One of: enum, null | -| `target` | TextAddress | yes | TextAddress | - -### Example request - -```json -{ - "alignment": "left", - "target": { - "blockId": "block-abc123", - "kind": "text", - "range": { - "end": 10, - "start": 0 - } - } -} -``` - -## Output fields - -_No fields._ - -### Example response - -```json -{ - "inserted": [ - { - "entityId": "entity-789", - "entityType": "comment", - "kind": "entity" - } - ], - "resolution": { - "range": { - "from": 0, - "to": 10 - }, - "requestedTarget": { - "blockId": "block-abc123", - "kind": "text", - "range": { - "end": 10, - "start": 0 - } - }, - "target": { - "blockId": "block-abc123", - "kind": "text", - "range": { - "end": 10, - "start": 0 - } - }, - "text": "Hello, world." - }, - "success": true, - "updated": [ - { - "entityId": "entity-789", - "entityType": "comment", - "kind": "entity" - } - ] -} -``` - -## Pre-apply throws - -- `TARGET_NOT_FOUND` -- `CAPABILITY_UNAVAILABLE` -- `INVALID_TARGET` -- `INVALID_INPUT` - -## Non-applied failure codes - -- `INVALID_TARGET` -- `NO_OP` - -## Raw schemas - - -```json -{ - "additionalProperties": false, - "properties": { - "alignment": { - "oneOf": [ - { - "enum": [ - "left", - "center", - "right", - "justify" - ] - }, - { - "type": "null" - } - ] - }, - "target": { - "$ref": "#/$defs/TextAddress" - } - }, - "required": [ - "target", - "alignment" - ], - "type": "object" -} -``` - - - -```json -{ - "oneOf": [ - { - "$ref": "#/$defs/TextMutationSuccess" - }, - { - "additionalProperties": false, - "properties": { - "failure": { - "additionalProperties": false, - "properties": { - "code": { - "enum": [ - "INVALID_TARGET", - "NO_OP" - ] - }, - "details": {}, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "message" - ], - "type": "object" - }, - "resolution": { - "$ref": "#/$defs/TextMutationResolution" - }, - "success": { - "const": false - } - }, - "required": [ - "success", - "failure", - "resolution" - ], - "type": "object" - } - ] -} -``` - - - -```json -{ - "$ref": "#/$defs/TextMutationSuccess" -} -``` - - - -```json -{ - "additionalProperties": false, - "properties": { - "failure": { - "additionalProperties": false, - "properties": { - "code": { - "enum": [ - "INVALID_TARGET", - "NO_OP" - ] - }, - "details": {}, - "message": { - "type": "string" - } - }, - "required": [ - "code", - "message" - ], - "type": "object" - }, - "resolution": { - "$ref": "#/$defs/TextMutationResolution" - }, - "success": { - "const": false - } - }, - "required": [ - "success", - "failure", - "resolution" - ], - "type": "object" -} -``` - diff --git a/apps/docs/document-api/reference/format/index.mdx b/apps/docs/document-api/reference/format/index.mdx index 477b407ff1..6ba65d1581 100644 --- a/apps/docs/document-api/reference/format/index.mdx +++ b/apps/docs/document-api/reference/format/index.mdx @@ -18,7 +18,6 @@ Canonical formatting mutation with directive semantics ('on', 'off', 'clear'). | format.fontSize | `format.fontSize` | Yes | `conditional` | No | Yes | | format.fontFamily | `format.fontFamily` | Yes | `conditional` | No | Yes | | format.color | `format.color` | Yes | `conditional` | No | Yes | -| format.align | `format.align` | Yes | `conditional` | No | Yes | ## Convenience aliases diff --git a/apps/docs/document-api/reference/format/paragraph/clear-alignment.mdx b/apps/docs/document-api/reference/format/paragraph/clear-alignment.mdx new file mode 100644 index 0000000000..990351ee6a --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/clear-alignment.mdx @@ -0,0 +1,333 @@ +--- +title: format.paragraph.clearAlignment +sidebarTitle: format.paragraph.clearAlignment +description: Remove direct paragraph alignment, reverting to style-defined or default alignment. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Remove direct paragraph alignment, reverting to style-defined or default alignment. + +- Operation ID: `format.paragraph.clearAlignment` +- API member path: `editor.doc.format.paragraph.clearAlignment(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a ParagraphMutationResult; reports NO_OP if no direct alignment is set. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | + +### Example request + +```json +{ + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "resolution": { + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } + }, + "success": true, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/paragraph/clear-all-tab-stops.mdx b/apps/docs/document-api/reference/format/paragraph/clear-all-tab-stops.mdx new file mode 100644 index 0000000000..cef851c6ad --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/clear-all-tab-stops.mdx @@ -0,0 +1,333 @@ +--- +title: format.paragraph.clearAllTabStops +sidebarTitle: format.paragraph.clearAllTabStops +description: Remove all tab stops from a paragraph. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Remove all tab stops from a paragraph. + +- Operation ID: `format.paragraph.clearAllTabStops` +- API member path: `editor.doc.format.paragraph.clearAllTabStops(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a ParagraphMutationResult; reports NO_OP if no tab stops exist. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | + +### Example request + +```json +{ + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "resolution": { + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } + }, + "success": true, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/paragraph/clear-border.mdx b/apps/docs/document-api/reference/format/paragraph/clear-border.mdx new file mode 100644 index 0000000000..d9b7803094 --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/clear-border.mdx @@ -0,0 +1,347 @@ +--- +title: format.paragraph.clearBorder +sidebarTitle: format.paragraph.clearBorder +description: Remove border for a specific side or all sides of a paragraph. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Remove border for a specific side or all sides of a paragraph. + +- Operation ID: `format.paragraph.clearBorder` +- API member path: `editor.doc.format.paragraph.clearBorder(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a ParagraphMutationResult; reports NO_OP if the border is already absent. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `side` | enum | yes | `"top"`, `"bottom"`, `"left"`, `"right"`, `"between"`, `"bar"`, `"all"` | +| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | + +### Example request + +```json +{ + "side": "top", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "resolution": { + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } + }, + "success": true, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "side": { + "enum": [ + "top", + "bottom", + "left", + "right", + "between", + "bar", + "all" + ] + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target", + "side" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/paragraph/clear-indentation.mdx b/apps/docs/document-api/reference/format/paragraph/clear-indentation.mdx new file mode 100644 index 0000000000..2ae0f044e7 --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/clear-indentation.mdx @@ -0,0 +1,333 @@ +--- +title: format.paragraph.clearIndentation +sidebarTitle: format.paragraph.clearIndentation +description: Remove all direct paragraph indentation. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Remove all direct paragraph indentation. + +- Operation ID: `format.paragraph.clearIndentation` +- API member path: `editor.doc.format.paragraph.clearIndentation(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a ParagraphMutationResult; reports NO_OP if no direct indentation is set. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | + +### Example request + +```json +{ + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "resolution": { + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } + }, + "success": true, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/paragraph/clear-shading.mdx b/apps/docs/document-api/reference/format/paragraph/clear-shading.mdx new file mode 100644 index 0000000000..6f897c8f38 --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/clear-shading.mdx @@ -0,0 +1,333 @@ +--- +title: format.paragraph.clearShading +sidebarTitle: format.paragraph.clearShading +description: Remove all paragraph shading. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Remove all paragraph shading. + +- Operation ID: `format.paragraph.clearShading` +- API member path: `editor.doc.format.paragraph.clearShading(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a ParagraphMutationResult; reports NO_OP if no shading is set. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | + +### Example request + +```json +{ + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "resolution": { + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } + }, + "success": true, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/paragraph/clear-spacing.mdx b/apps/docs/document-api/reference/format/paragraph/clear-spacing.mdx new file mode 100644 index 0000000000..1b6592ef1d --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/clear-spacing.mdx @@ -0,0 +1,333 @@ +--- +title: format.paragraph.clearSpacing +sidebarTitle: format.paragraph.clearSpacing +description: Remove all direct paragraph spacing. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Remove all direct paragraph spacing. + +- Operation ID: `format.paragraph.clearSpacing` +- API member path: `editor.doc.format.paragraph.clearSpacing(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a ParagraphMutationResult; reports NO_OP if no direct spacing is set. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | + +### Example request + +```json +{ + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "resolution": { + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } + }, + "success": true, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/paragraph/clear-tab-stop.mdx b/apps/docs/document-api/reference/format/paragraph/clear-tab-stop.mdx new file mode 100644 index 0000000000..a1c0d80f0e --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/clear-tab-stop.mdx @@ -0,0 +1,340 @@ +--- +title: format.paragraph.clearTabStop +sidebarTitle: format.paragraph.clearTabStop +description: Remove a tab stop at a given position. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Remove a tab stop at a given position. + +- Operation ID: `format.paragraph.clearTabStop` +- API member path: `editor.doc.format.paragraph.clearTabStop(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a ParagraphMutationResult; reports NO_OP if no tab stop exists at that position. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `position` | integer | yes | | +| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | + +### Example request + +```json +{ + "position": 1, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "resolution": { + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } + }, + "success": true, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "position": { + "minimum": 0, + "type": "integer" + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target", + "position" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/paragraph/index.mdx b/apps/docs/document-api/reference/format/paragraph/index.mdx new file mode 100644 index 0000000000..f7caa498c2 --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/index.mdx @@ -0,0 +1,34 @@ +--- +title: Paragraph Formatting operations +sidebarTitle: Paragraph Formatting +description: Paragraph Formatting 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) + +Paragraph-level direct formatting: alignment, indentation, spacing, borders, shading, and more. + +| Operation | Member path | Mutates | Idempotency | Tracked | Dry run | +| --- | --- | --- | --- | --- | --- | +| format.paragraph.resetDirectFormatting | `format.paragraph.resetDirectFormatting` | Yes | `conditional` | No | Yes | +| format.paragraph.setAlignment | `format.paragraph.setAlignment` | Yes | `conditional` | No | Yes | +| format.paragraph.clearAlignment | `format.paragraph.clearAlignment` | Yes | `conditional` | No | Yes | +| format.paragraph.setIndentation | `format.paragraph.setIndentation` | Yes | `conditional` | No | Yes | +| format.paragraph.clearIndentation | `format.paragraph.clearIndentation` | Yes | `conditional` | No | Yes | +| format.paragraph.setSpacing | `format.paragraph.setSpacing` | Yes | `conditional` | No | Yes | +| format.paragraph.clearSpacing | `format.paragraph.clearSpacing` | Yes | `conditional` | No | Yes | +| format.paragraph.setKeepOptions | `format.paragraph.setKeepOptions` | Yes | `conditional` | No | Yes | +| format.paragraph.setOutlineLevel | `format.paragraph.setOutlineLevel` | Yes | `conditional` | No | Yes | +| format.paragraph.setFlowOptions | `format.paragraph.setFlowOptions` | Yes | `conditional` | No | Yes | +| format.paragraph.setTabStop | `format.paragraph.setTabStop` | Yes | `conditional` | No | Yes | +| format.paragraph.clearTabStop | `format.paragraph.clearTabStop` | Yes | `conditional` | No | Yes | +| format.paragraph.clearAllTabStops | `format.paragraph.clearAllTabStops` | Yes | `conditional` | No | Yes | +| format.paragraph.setBorder | `format.paragraph.setBorder` | Yes | `conditional` | No | Yes | +| format.paragraph.clearBorder | `format.paragraph.clearBorder` | Yes | `conditional` | No | Yes | +| format.paragraph.setShading | `format.paragraph.setShading` | Yes | `conditional` | No | Yes | +| format.paragraph.clearShading | `format.paragraph.clearShading` | Yes | `conditional` | No | Yes | + diff --git a/apps/docs/document-api/reference/format/paragraph/reset-direct-formatting.mdx b/apps/docs/document-api/reference/format/paragraph/reset-direct-formatting.mdx new file mode 100644 index 0000000000..5fff145a71 --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/reset-direct-formatting.mdx @@ -0,0 +1,333 @@ +--- +title: format.paragraph.resetDirectFormatting +sidebarTitle: format.paragraph.resetDirectFormatting +description: Strip all direct paragraph formatting while preserving style reference, numbering, and section metadata. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Strip all direct paragraph formatting while preserving style reference, numbering, and section metadata. + +- Operation ID: `format.paragraph.resetDirectFormatting` +- API member path: `editor.doc.format.paragraph.resetDirectFormatting(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a ParagraphMutationResult; reports NO_OP if no direct formatting is present. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | + +### Example request + +```json +{ + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "resolution": { + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } + }, + "success": true, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/paragraph/set-alignment.mdx b/apps/docs/document-api/reference/format/paragraph/set-alignment.mdx new file mode 100644 index 0000000000..d0db290f8e --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/set-alignment.mdx @@ -0,0 +1,344 @@ +--- +title: format.paragraph.setAlignment +sidebarTitle: format.paragraph.setAlignment +description: Set paragraph alignment (justification) on a paragraph-like block. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set paragraph alignment (justification) on a paragraph-like block. + +- Operation ID: `format.paragraph.setAlignment` +- API member path: `editor.doc.format.paragraph.setAlignment(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a ParagraphMutationResult; reports NO_OP if the alignment already matches. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `alignment` | enum | yes | `"left"`, `"center"`, `"right"`, `"justify"` | +| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | + +### Example request + +```json +{ + "alignment": "left", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "resolution": { + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } + }, + "success": true, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "alignment": { + "enum": [ + "left", + "center", + "right", + "justify" + ] + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target", + "alignment" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/paragraph/set-border.mdx b/apps/docs/document-api/reference/format/paragraph/set-border.mdx new file mode 100644 index 0000000000..af5dcfb813 --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/set-border.mdx @@ -0,0 +1,370 @@ +--- +title: format.paragraph.setBorder +sidebarTitle: format.paragraph.setBorder +description: Set border properties for a specific side of a paragraph. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set border properties for a specific side of a paragraph. + +- Operation ID: `format.paragraph.setBorder` +- API member path: `editor.doc.format.paragraph.setBorder(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a ParagraphMutationResult; reports NO_OP if the border already matches. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `color` | string | no | | +| `side` | enum | yes | `"top"`, `"bottom"`, `"left"`, `"right"`, `"between"`, `"bar"` | +| `size` | integer | no | | +| `space` | integer | no | | +| `style` | string | yes | | +| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | + +### Example request + +```json +{ + "color": "example", + "side": "top", + "size": 1, + "style": "example", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "resolution": { + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } + }, + "success": true, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "color": { + "minLength": 1, + "type": "string" + }, + "side": { + "enum": [ + "top", + "bottom", + "left", + "right", + "between", + "bar" + ] + }, + "size": { + "minimum": 0, + "type": "integer" + }, + "space": { + "minimum": 0, + "type": "integer" + }, + "style": { + "minLength": 1, + "type": "string" + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target", + "side", + "style" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/paragraph/set-flow-options.mdx b/apps/docs/document-api/reference/format/paragraph/set-flow-options.mdx new file mode 100644 index 0000000000..aef6a6ecda --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/set-flow-options.mdx @@ -0,0 +1,368 @@ +--- +title: format.paragraph.setFlowOptions +sidebarTitle: format.paragraph.setFlowOptions +description: Set contextual spacing, page-break-before, and suppress-auto-hyphens flags. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set contextual spacing, page-break-before, and suppress-auto-hyphens flags. + +- Operation ID: `format.paragraph.setFlowOptions` +- API member path: `editor.doc.format.paragraph.setFlowOptions(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a ParagraphMutationResult; reports NO_OP if all flags already match. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `contextualSpacing` | boolean | no | | +| `pageBreakBefore` | boolean | no | | +| `suppressAutoHyphens` | boolean | no | | +| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | + +### Example request + +```json +{ + "contextualSpacing": true, + "pageBreakBefore": true, + "suppressAutoHyphens": true, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "resolution": { + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } + }, + "success": true, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "target", + "contextualSpacing" + ] + }, + { + "required": [ + "target", + "pageBreakBefore" + ] + }, + { + "required": [ + "target", + "suppressAutoHyphens" + ] + } + ], + "properties": { + "contextualSpacing": { + "type": "boolean" + }, + "pageBreakBefore": { + "type": "boolean" + }, + "suppressAutoHyphens": { + "type": "boolean" + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/paragraph/set-indentation.mdx b/apps/docs/document-api/reference/format/paragraph/set-indentation.mdx new file mode 100644 index 0000000000..214d215670 --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/set-indentation.mdx @@ -0,0 +1,384 @@ +--- +title: format.paragraph.setIndentation +sidebarTitle: format.paragraph.setIndentation +description: Set paragraph indentation properties (left, right, firstLine, hanging) in twips. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set paragraph indentation properties (left, right, firstLine, hanging) in twips. + +- Operation ID: `format.paragraph.setIndentation` +- API member path: `editor.doc.format.paragraph.setIndentation(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a ParagraphMutationResult; reports NO_OP if indentation already matches. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `firstLine` | integer | no | | +| `hanging` | integer | no | | +| `left` | integer | no | | +| `right` | integer | no | | +| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | + +### Example request + +```json +{ + "firstLine": 1, + "left": 1, + "right": 1, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "resolution": { + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } + }, + "success": true, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "left" + ] + }, + { + "required": [ + "right" + ] + }, + { + "required": [ + "firstLine" + ] + }, + { + "required": [ + "hanging" + ] + } + ], + "not": { + "required": [ + "firstLine", + "hanging" + ] + }, + "properties": { + "firstLine": { + "minimum": 0, + "type": "integer" + }, + "hanging": { + "minimum": 0, + "type": "integer" + }, + "left": { + "minimum": 0, + "type": "integer" + }, + "right": { + "minimum": 0, + "type": "integer" + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/paragraph/set-keep-options.mdx b/apps/docs/document-api/reference/format/paragraph/set-keep-options.mdx new file mode 100644 index 0000000000..38c38251ed --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/set-keep-options.mdx @@ -0,0 +1,368 @@ +--- +title: format.paragraph.setKeepOptions +sidebarTitle: format.paragraph.setKeepOptions +description: Set keep-with-next, keep-lines-together, and widow/orphan control flags. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set keep-with-next, keep-lines-together, and widow/orphan control flags. + +- Operation ID: `format.paragraph.setKeepOptions` +- API member path: `editor.doc.format.paragraph.setKeepOptions(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a ParagraphMutationResult; reports NO_OP if all flags already match. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `keepLines` | boolean | no | | +| `keepNext` | boolean | no | | +| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | +| `widowControl` | boolean | no | | + +### Example request + +```json +{ + "keepLines": true, + "keepNext": true, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + }, + "widowControl": true +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "resolution": { + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } + }, + "success": true, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "target", + "keepNext" + ] + }, + { + "required": [ + "target", + "keepLines" + ] + }, + { + "required": [ + "target", + "widowControl" + ] + } + ], + "properties": { + "keepLines": { + "type": "boolean" + }, + "keepNext": { + "type": "boolean" + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + }, + "widowControl": { + "type": "boolean" + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/paragraph/set-outline-level.mdx b/apps/docs/document-api/reference/format/paragraph/set-outline-level.mdx new file mode 100644 index 0000000000..cacb17ede1 --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/set-outline-level.mdx @@ -0,0 +1,348 @@ +--- +title: format.paragraph.setOutlineLevel +sidebarTitle: format.paragraph.setOutlineLevel +description: Set the paragraph outline level (0–9) or null to clear. +--- + +{/* 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 paragraph outline level (0–9) or null to clear. + +- Operation ID: `format.paragraph.setOutlineLevel` +- API member path: `editor.doc.format.paragraph.setOutlineLevel(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a ParagraphMutationResult; reports NO_OP if outline level already matches. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `outlineLevel` | integer \\| null | yes | One of: integer, null | +| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | + +### Example request + +```json +{ + "outlineLevel": 1, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "resolution": { + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } + }, + "success": true, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "outlineLevel": { + "oneOf": [ + { + "maximum": 9, + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target", + "outlineLevel" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/paragraph/set-shading.mdx b/apps/docs/document-api/reference/format/paragraph/set-shading.mdx new file mode 100644 index 0000000000..c350d8f878 --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/set-shading.mdx @@ -0,0 +1,371 @@ +--- +title: format.paragraph.setShading +sidebarTitle: format.paragraph.setShading +description: Set paragraph shading (background fill, pattern color, pattern 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 paragraph shading (background fill, pattern color, pattern type). + +- Operation ID: `format.paragraph.setShading` +- API member path: `editor.doc.format.paragraph.setShading(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a ParagraphMutationResult; reports NO_OP if the shading already matches. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `color` | string | no | | +| `fill` | string | no | | +| `pattern` | string | no | | +| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | + +### Example request + +```json +{ + "color": "example", + "fill": "example", + "pattern": "hello world", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "resolution": { + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } + }, + "success": true, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "target", + "fill" + ] + }, + { + "required": [ + "target", + "color" + ] + }, + { + "required": [ + "target", + "pattern" + ] + } + ], + "properties": { + "color": { + "minLength": 1, + "type": "string" + }, + "fill": { + "minLength": 1, + "type": "string" + }, + "pattern": { + "minLength": 1, + "type": "string" + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/paragraph/set-spacing.mdx b/apps/docs/document-api/reference/format/paragraph/set-spacing.mdx new file mode 100644 index 0000000000..cc59d77c75 --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/set-spacing.mdx @@ -0,0 +1,391 @@ +--- +title: format.paragraph.setSpacing +sidebarTitle: format.paragraph.setSpacing +description: Set paragraph spacing properties (before, after, line, lineRule) in twips. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set paragraph spacing properties (before, after, line, lineRule) in twips. + +- Operation ID: `format.paragraph.setSpacing` +- API member path: `editor.doc.format.paragraph.setSpacing(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a ParagraphMutationResult; reports NO_OP if spacing already matches. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `after` | integer | no | | +| `before` | integer | no | | +| `line` | integer | no | | +| `lineRule` | enum | no | `"auto"`, `"exact"`, `"atLeast"` | +| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | + +### Example request + +```json +{ + "after": 1, + "before": 1, + "line": 1, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "resolution": { + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } + }, + "success": true, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "before" + ] + }, + { + "required": [ + "after" + ] + }, + { + "required": [ + "line" + ] + }, + { + "required": [ + "lineRule" + ] + } + ], + "if": { + "required": [ + "line" + ] + }, + "properties": { + "after": { + "minimum": 0, + "type": "integer" + }, + "before": { + "minimum": 0, + "type": "integer" + }, + "line": { + "minimum": 1, + "type": "integer" + }, + "lineRule": { + "enum": [ + "auto", + "exact", + "atLeast" + ] + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "then": { + "required": [ + "lineRule" + ] + }, + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/paragraph/set-tab-stop.mdx b/apps/docs/document-api/reference/format/paragraph/set-tab-stop.mdx new file mode 100644 index 0000000000..dc94206ade --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/set-tab-stop.mdx @@ -0,0 +1,364 @@ +--- +title: format.paragraph.setTabStop +sidebarTitle: format.paragraph.setTabStop +description: Add or replace a tab stop at a given position. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Add or replace a tab stop at a given position. + +- Operation ID: `format.paragraph.setTabStop` +- API member path: `editor.doc.format.paragraph.setTabStop(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a ParagraphMutationResult; reports NO_OP if an identical tab stop already exists. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `alignment` | enum | yes | `"left"`, `"center"`, `"right"`, `"decimal"`, `"bar"` | +| `leader` | enum | no | `"none"`, `"dot"`, `"hyphen"`, `"underscore"`, `"heavy"`, `"middleDot"` | +| `position` | integer | yes | | +| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | + +### Example request + +```json +{ + "alignment": "left", + "leader": "none", + "position": 1, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "resolution": { + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } + }, + "success": true, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "alignment": { + "enum": [ + "left", + "center", + "right", + "decimal", + "bar" + ] + }, + "leader": { + "enum": [ + "none", + "dot", + "hyphen", + "underscore", + "heavy", + "middleDot" + ] + }, + "position": { + "minimum": 0, + "type": "integer" + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target", + "position", + "alignment" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/index.mdx b/apps/docs/document-api/reference/index.mdx index ae1ef0a061..611e21283c 100644 --- a/apps/docs/document-api/reference/index.mdx +++ b/apps/docs/document-api/reference/index.mdx @@ -25,13 +25,15 @@ Document API is currently alpha and subject to breaking changes. | Capabilities | 1 | 0 | 1 | [Open](/document-api/reference/capabilities/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) | +| Format | 4 | 4 | 8 | [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) | | Comments | 5 | 0 | 5 | [Open](/document-api/reference/comments/index) | | Track Changes | 3 | 0 | 3 | [Open](/document-api/reference/track-changes/index) | | Query | 1 | 0 | 1 | [Open](/document-api/reference/query/index) | | Mutations | 2 | 0 | 2 | [Open](/document-api/reference/mutations/index) | +| Paragraph Formatting | 17 | 0 | 17 | [Open](/document-api/reference/format/paragraph/index) | +| Paragraph Styles | 2 | 0 | 2 | [Open](/document-api/reference/styles/paragraph/index) | | Tables | 39 | 0 | 39 | [Open](/document-api/reference/tables/index) | ## Available operations @@ -103,7 +105,6 @@ The tables below are grouped by namespace. | format.fontSize | editor.doc.format.fontSize(...) | Set or unset the font size on the target text range. Pass null to remove. | | format.fontFamily | editor.doc.format.fontFamily(...) | Set or unset the font family on the target text range. Pass null to remove. | | format.color | editor.doc.format.color(...) | Set or unset the text color on the target text range. Pass null to remove. | -| format.align | editor.doc.format.align(...) | Set or unset paragraph alignment on the block containing the target. Pass null to reset to default. | | format.bold | editor.doc.format.bold(...) | Convenience alias for `format.apply` with `inline.bold: 'on'`. | | format.italic | editor.doc.format.italic(...) | Convenience alias for `format.apply` with `inline.italic: 'on'`. | | format.underline | editor.doc.format.underline(...) | Convenience alias for `format.apply` with `inline.underline: 'on'`. | @@ -159,6 +160,35 @@ The tables below are grouped by namespace. | mutations.preview | editor.doc.mutations.preview(...) | Dry-run a mutation plan, returning resolved targets without applying changes. | | mutations.apply | editor.doc.mutations.apply(...) | Execute a mutation plan atomically against the document. | +#### Paragraph Formatting + +| Operation | API member path | Description | +| --- | --- | --- | +| format.paragraph.resetDirectFormatting | editor.doc.format.paragraph.resetDirectFormatting(...) | Strip all direct paragraph formatting while preserving style reference, numbering, and section metadata. | +| format.paragraph.setAlignment | editor.doc.format.paragraph.setAlignment(...) | Set paragraph alignment (justification) on a paragraph-like block. | +| format.paragraph.clearAlignment | editor.doc.format.paragraph.clearAlignment(...) | Remove direct paragraph alignment, reverting to style-defined or default alignment. | +| format.paragraph.setIndentation | editor.doc.format.paragraph.setIndentation(...) | Set paragraph indentation properties (left, right, firstLine, hanging) in twips. | +| format.paragraph.clearIndentation | editor.doc.format.paragraph.clearIndentation(...) | Remove all direct paragraph indentation. | +| format.paragraph.setSpacing | editor.doc.format.paragraph.setSpacing(...) | Set paragraph spacing properties (before, after, line, lineRule) in twips. | +| format.paragraph.clearSpacing | editor.doc.format.paragraph.clearSpacing(...) | Remove all direct paragraph spacing. | +| format.paragraph.setKeepOptions | editor.doc.format.paragraph.setKeepOptions(...) | Set keep-with-next, keep-lines-together, and widow/orphan control flags. | +| format.paragraph.setOutlineLevel | editor.doc.format.paragraph.setOutlineLevel(...) | Set the paragraph outline level (0–9) or null to clear. | +| format.paragraph.setFlowOptions | editor.doc.format.paragraph.setFlowOptions(...) | Set contextual spacing, page-break-before, and suppress-auto-hyphens flags. | +| format.paragraph.setTabStop | editor.doc.format.paragraph.setTabStop(...) | Add or replace a tab stop at a given position. | +| format.paragraph.clearTabStop | editor.doc.format.paragraph.clearTabStop(...) | Remove a tab stop at a given position. | +| format.paragraph.clearAllTabStops | editor.doc.format.paragraph.clearAllTabStops(...) | Remove all tab stops from a paragraph. | +| format.paragraph.setBorder | editor.doc.format.paragraph.setBorder(...) | Set border properties for a specific side of a paragraph. | +| format.paragraph.clearBorder | editor.doc.format.paragraph.clearBorder(...) | Remove border for a specific side or all sides of a paragraph. | +| format.paragraph.setShading | editor.doc.format.paragraph.setShading(...) | Set paragraph shading (background fill, pattern color, pattern type). | +| format.paragraph.clearShading | editor.doc.format.paragraph.clearShading(...) | Remove all paragraph shading. | + +#### Paragraph Styles + +| Operation | API member path | Description | +| --- | --- | --- | +| styles.paragraph.setStyle | editor.doc.styles.paragraph.setStyle(...) | Set the paragraph style reference (w:pStyle) on a paragraph-like block. | +| styles.paragraph.clearStyle | editor.doc.styles.paragraph.clearStyle(...) | Remove the paragraph style reference from a paragraph-like block. | + #### Tables | Operation | API member path | Description | diff --git a/apps/docs/document-api/reference/styles/paragraph/clear-style.mdx b/apps/docs/document-api/reference/styles/paragraph/clear-style.mdx new file mode 100644 index 0000000000..116bfd4047 --- /dev/null +++ b/apps/docs/document-api/reference/styles/paragraph/clear-style.mdx @@ -0,0 +1,333 @@ +--- +title: styles.paragraph.clearStyle +sidebarTitle: styles.paragraph.clearStyle +description: Remove the paragraph style reference from a paragraph-like block. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Remove the paragraph style reference from a paragraph-like block. + +- Operation ID: `styles.paragraph.clearStyle` +- API member path: `editor.doc.styles.paragraph.clearStyle(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a ParagraphMutationResult; reports NO_OP if no style is set. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | + +### Example request + +```json +{ + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "resolution": { + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } + }, + "success": true, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/styles/paragraph/index.mdx b/apps/docs/document-api/reference/styles/paragraph/index.mdx new file mode 100644 index 0000000000..deb91e1c2b --- /dev/null +++ b/apps/docs/document-api/reference/styles/paragraph/index.mdx @@ -0,0 +1,19 @@ +--- +title: Paragraph Styles operations +sidebarTitle: Paragraph Styles +description: Paragraph Styles 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) + +Paragraph style reference operations (set/clear w:pStyle). + +| Operation | Member path | Mutates | Idempotency | Tracked | Dry run | +| --- | --- | --- | --- | --- | --- | +| styles.paragraph.setStyle | `styles.paragraph.setStyle` | Yes | `conditional` | No | Yes | +| styles.paragraph.clearStyle | `styles.paragraph.clearStyle` | Yes | `conditional` | No | Yes | + diff --git a/apps/docs/document-api/reference/styles/paragraph/set-style.mdx b/apps/docs/document-api/reference/styles/paragraph/set-style.mdx new file mode 100644 index 0000000000..15a2aed97a --- /dev/null +++ b/apps/docs/document-api/reference/styles/paragraph/set-style.mdx @@ -0,0 +1,340 @@ +--- +title: styles.paragraph.setStyle +sidebarTitle: styles.paragraph.setStyle +description: "Set the paragraph style reference (w:pStyle) on a paragraph-like block." +--- + +{/* 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 paragraph style reference (w:pStyle) on a paragraph-like block. + +- Operation ID: `styles.paragraph.setStyle` +- API member path: `editor.doc.styles.paragraph.setStyle(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a ParagraphMutationResult; reports NO_OP if the style already matches. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `styleId` | string | yes | | +| `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | + +### Example request + +```json +{ + "styleId": "style-001", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "resolution": { + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } + }, + "success": true, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `AMBIGUOUS_TARGET` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "styleId": { + "minLength": 1, + "type": "string" + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target", + "styleId" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": true + }, + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "success", + "target", + "resolution" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/$defs/ParagraphAddress" + }, + { + "$ref": "#/$defs/HeadingAddress" + }, + { + "$ref": "#/$defs/ListItemAddress" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-engine/sdks.mdx b/apps/docs/document-engine/sdks.mdx index 2cbacb9d64..61bbfaba9b 100644 --- a/apps/docs/document-engine/sdks.mdx +++ b/apps/docs/document-engine/sdks.mdx @@ -213,7 +213,6 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p | `doc.format.fontSize` | `format font-size` | Set or unset the font size on the target text range. Pass null to remove. | | `doc.format.fontFamily` | `format font-family` | Set or unset the font family on the target text range. Pass null to remove. | | `doc.format.color` | `format color` | Set or unset the text color on the target text range. Pass null to remove. | -| `doc.format.align` | `format align` | Set or unset paragraph alignment on the block containing the target. Pass null to reset to default. | #### Create diff --git a/packages/document-api/src/contract/contract.test.ts b/packages/document-api/src/contract/contract.test.ts index 0eb2811ebe..d46c073f35 100644 --- a/packages/document-api/src/contract/contract.test.ts +++ b/packages/document-api/src/contract/contract.test.ts @@ -123,7 +123,9 @@ describe('document-api contract catalog', () => { 'create', 'sections', 'format', + 'format.paragraph', 'styles', + 'styles.paragraph', 'lists', 'comments', 'trackChanges', diff --git a/packages/document-api/src/contract/operation-definitions.ts b/packages/document-api/src/contract/operation-definitions.ts index 47c8e35c16..39ac330fda 100644 --- a/packages/document-api/src/contract/operation-definitions.ts +++ b/packages/document-api/src/contract/operation-definitions.ts @@ -38,7 +38,9 @@ export type ReferenceGroupKey = | 'create' | 'sections' | 'format' + | 'format.paragraph' | 'styles' + | 'styles.paragraph' | 'lists' | 'comments' | 'trackChanges' @@ -144,6 +146,15 @@ 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; + +// Paragraph-mutation throw-code arrays +const T_PARAGRAPH_MUTATION = [ + 'TARGET_NOT_FOUND', + 'AMBIGUOUS_TARGET', + 'CAPABILITY_UNAVAILABLE', + 'INVALID_TARGET', + 'INVALID_INPUT', +] as const; const T_SECTION_CREATE = [ 'TARGET_NOT_FOUND', 'INVALID_TARGET', @@ -361,23 +372,6 @@ export const OPERATION_DEFINITIONS = { referenceDocPath: 'format/color.mdx', referenceGroup: 'format', }, - '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', - supportsDryRun: true, - supportsTrackedMode: false, - possibleFailureCodes: ['INVALID_TARGET', 'NO_OP'], - throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET', 'INVALID_INPUT'], - }), - referenceDocPath: 'format/align.mdx', - referenceGroup: 'format', - }, - 'styles.apply': { memberPath: 'styles.apply', description: @@ -716,6 +710,298 @@ export const OPERATION_DEFINITIONS = { referenceGroup: 'sections', }, + // --- styles.paragraph.* --- + + 'styles.paragraph.setStyle': { + memberPath: 'styles.paragraph.setStyle', + description: 'Set the paragraph style reference (w:pStyle) on a paragraph-like block.', + expectedResult: 'Returns a ParagraphMutationResult; reports NO_OP if the style already matches.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_PARAGRAPH_MUTATION, + }), + referenceDocPath: 'styles/paragraph/set-style.mdx', + referenceGroup: 'styles.paragraph', + }, + 'styles.paragraph.clearStyle': { + memberPath: 'styles.paragraph.clearStyle', + description: 'Remove the paragraph style reference from a paragraph-like block.', + expectedResult: 'Returns a ParagraphMutationResult; reports NO_OP if no style is set.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_PARAGRAPH_MUTATION, + }), + referenceDocPath: 'styles/paragraph/clear-style.mdx', + referenceGroup: 'styles.paragraph', + }, + + // --- format.paragraph.* --- + + 'format.paragraph.resetDirectFormatting': { + memberPath: 'format.paragraph.resetDirectFormatting', + description: + 'Strip all direct paragraph formatting while preserving style reference, numbering, and section metadata.', + expectedResult: 'Returns a ParagraphMutationResult; reports NO_OP if no direct formatting is present.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_PARAGRAPH_MUTATION, + }), + referenceDocPath: 'format/paragraph/reset-direct-formatting.mdx', + referenceGroup: 'format.paragraph', + }, + 'format.paragraph.setAlignment': { + memberPath: 'format.paragraph.setAlignment', + description: 'Set paragraph alignment (justification) on a paragraph-like block.', + expectedResult: 'Returns a ParagraphMutationResult; reports NO_OP if the alignment already matches.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_PARAGRAPH_MUTATION, + }), + referenceDocPath: 'format/paragraph/set-alignment.mdx', + referenceGroup: 'format.paragraph', + }, + 'format.paragraph.clearAlignment': { + memberPath: 'format.paragraph.clearAlignment', + description: 'Remove direct paragraph alignment, reverting to style-defined or default alignment.', + expectedResult: 'Returns a ParagraphMutationResult; reports NO_OP if no direct alignment is set.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_PARAGRAPH_MUTATION, + }), + referenceDocPath: 'format/paragraph/clear-alignment.mdx', + referenceGroup: 'format.paragraph', + }, + 'format.paragraph.setIndentation': { + memberPath: 'format.paragraph.setIndentation', + description: 'Set paragraph indentation properties (left, right, firstLine, hanging) in twips.', + expectedResult: 'Returns a ParagraphMutationResult; reports NO_OP if indentation already matches.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_PARAGRAPH_MUTATION, + }), + referenceDocPath: 'format/paragraph/set-indentation.mdx', + referenceGroup: 'format.paragraph', + }, + 'format.paragraph.clearIndentation': { + memberPath: 'format.paragraph.clearIndentation', + description: 'Remove all direct paragraph indentation.', + expectedResult: 'Returns a ParagraphMutationResult; reports NO_OP if no direct indentation is set.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_PARAGRAPH_MUTATION, + }), + referenceDocPath: 'format/paragraph/clear-indentation.mdx', + referenceGroup: 'format.paragraph', + }, + 'format.paragraph.setSpacing': { + memberPath: 'format.paragraph.setSpacing', + description: 'Set paragraph spacing properties (before, after, line, lineRule) in twips.', + expectedResult: 'Returns a ParagraphMutationResult; reports NO_OP if spacing already matches.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_PARAGRAPH_MUTATION, + }), + referenceDocPath: 'format/paragraph/set-spacing.mdx', + referenceGroup: 'format.paragraph', + }, + 'format.paragraph.clearSpacing': { + memberPath: 'format.paragraph.clearSpacing', + description: 'Remove all direct paragraph spacing.', + expectedResult: 'Returns a ParagraphMutationResult; reports NO_OP if no direct spacing is set.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_PARAGRAPH_MUTATION, + }), + referenceDocPath: 'format/paragraph/clear-spacing.mdx', + referenceGroup: 'format.paragraph', + }, + 'format.paragraph.setKeepOptions': { + memberPath: 'format.paragraph.setKeepOptions', + description: 'Set keep-with-next, keep-lines-together, and widow/orphan control flags.', + expectedResult: 'Returns a ParagraphMutationResult; reports NO_OP if all flags already match.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_PARAGRAPH_MUTATION, + }), + referenceDocPath: 'format/paragraph/set-keep-options.mdx', + referenceGroup: 'format.paragraph', + }, + 'format.paragraph.setOutlineLevel': { + memberPath: 'format.paragraph.setOutlineLevel', + description: 'Set the paragraph outline level (0–9) or null to clear.', + expectedResult: 'Returns a ParagraphMutationResult; reports NO_OP if outline level already matches.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_PARAGRAPH_MUTATION, + }), + referenceDocPath: 'format/paragraph/set-outline-level.mdx', + referenceGroup: 'format.paragraph', + }, + 'format.paragraph.setFlowOptions': { + memberPath: 'format.paragraph.setFlowOptions', + description: 'Set contextual spacing, page-break-before, and suppress-auto-hyphens flags.', + expectedResult: 'Returns a ParagraphMutationResult; reports NO_OP if all flags already match.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_PARAGRAPH_MUTATION, + }), + referenceDocPath: 'format/paragraph/set-flow-options.mdx', + referenceGroup: 'format.paragraph', + }, + 'format.paragraph.setTabStop': { + memberPath: 'format.paragraph.setTabStop', + description: 'Add or replace a tab stop at a given position.', + expectedResult: 'Returns a ParagraphMutationResult; reports NO_OP if an identical tab stop already exists.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_PARAGRAPH_MUTATION, + }), + referenceDocPath: 'format/paragraph/set-tab-stop.mdx', + referenceGroup: 'format.paragraph', + }, + 'format.paragraph.clearTabStop': { + memberPath: 'format.paragraph.clearTabStop', + description: 'Remove a tab stop at a given position.', + expectedResult: 'Returns a ParagraphMutationResult; reports NO_OP if no tab stop exists at that position.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_PARAGRAPH_MUTATION, + }), + referenceDocPath: 'format/paragraph/clear-tab-stop.mdx', + referenceGroup: 'format.paragraph', + }, + 'format.paragraph.clearAllTabStops': { + memberPath: 'format.paragraph.clearAllTabStops', + description: 'Remove all tab stops from a paragraph.', + expectedResult: 'Returns a ParagraphMutationResult; reports NO_OP if no tab stops exist.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_PARAGRAPH_MUTATION, + }), + referenceDocPath: 'format/paragraph/clear-all-tab-stops.mdx', + referenceGroup: 'format.paragraph', + }, + 'format.paragraph.setBorder': { + memberPath: 'format.paragraph.setBorder', + description: 'Set border properties for a specific side of a paragraph.', + expectedResult: 'Returns a ParagraphMutationResult; reports NO_OP if the border already matches.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_PARAGRAPH_MUTATION, + }), + referenceDocPath: 'format/paragraph/set-border.mdx', + referenceGroup: 'format.paragraph', + }, + 'format.paragraph.clearBorder': { + memberPath: 'format.paragraph.clearBorder', + description: 'Remove border for a specific side or all sides of a paragraph.', + expectedResult: 'Returns a ParagraphMutationResult; reports NO_OP if the border is already absent.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_PARAGRAPH_MUTATION, + }), + referenceDocPath: 'format/paragraph/clear-border.mdx', + referenceGroup: 'format.paragraph', + }, + 'format.paragraph.setShading': { + memberPath: 'format.paragraph.setShading', + description: 'Set paragraph shading (background fill, pattern color, pattern type).', + expectedResult: 'Returns a ParagraphMutationResult; reports NO_OP if the shading already matches.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_PARAGRAPH_MUTATION, + }), + referenceDocPath: 'format/paragraph/set-shading.mdx', + referenceGroup: 'format.paragraph', + }, + 'format.paragraph.clearShading': { + memberPath: 'format.paragraph.clearShading', + description: 'Remove all paragraph shading.', + expectedResult: 'Returns a ParagraphMutationResult; reports NO_OP if no shading is set.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: T_PARAGRAPH_MUTATION, + }), + referenceDocPath: 'format/paragraph/clear-shading.mdx', + referenceGroup: 'format.paragraph', + }, + 'lists.list': { memberPath: 'lists.list', description: 'List all list nodes in the document, optionally filtered by scope.', diff --git a/packages/document-api/src/contract/operation-registry.ts b/packages/document-api/src/contract/operation-registry.ts index 5a84c2f174..950e6eb43f 100644 --- a/packages/document-api/src/contract/operation-registry.ts +++ b/packages/document-api/src/contract/operation-registry.ts @@ -32,7 +32,6 @@ import type { FormatFontSizeInput, FormatFontFamilyInput, FormatColorInput, - FormatAlignInput, } from '../format/format.js'; import type { StylesApplyInput, StylesApplyOptions, StylesApplyReceipt } from '../styles/styles.js'; import type { @@ -57,6 +56,29 @@ import type { ListTargetInput, ListsExitResult, } from '../lists/lists.types.js'; +import type { + ParagraphsAdapter, + ParagraphMutationResult, + ParagraphsSetStyleInput, + ParagraphsClearStyleInput, + ParagraphsResetDirectFormattingInput, + ParagraphsSetAlignmentInput, + ParagraphsClearAlignmentInput, + ParagraphsSetIndentationInput, + ParagraphsClearIndentationInput, + ParagraphsSetSpacingInput, + ParagraphsClearSpacingInput, + ParagraphsSetKeepOptionsInput, + ParagraphsSetOutlineLevelInput, + ParagraphsSetFlowOptionsInput, + ParagraphsSetTabStopInput, + ParagraphsClearTabStopInput, + ParagraphsClearAllTabStopsInput, + ParagraphsSetBorderInput, + ParagraphsClearBorderInput, + ParagraphsSetShadingInput, + ParagraphsClearShadingInput, +} from '../paragraphs/paragraphs.js'; import type { CreateSectionBreakInput, CreateSectionBreakResult, @@ -158,7 +180,104 @@ export interface OperationRegistry { 'format.fontSize': { input: FormatFontSizeInput; options: MutationOptions; output: TextMutationReceipt }; 'format.fontFamily': { input: FormatFontFamilyInput; options: MutationOptions; output: TextMutationReceipt }; 'format.color': { input: FormatColorInput; options: MutationOptions; output: TextMutationReceipt }; - 'format.align': { input: FormatAlignInput; options: MutationOptions; output: TextMutationReceipt }; + // --- styles.paragraph.* --- + 'styles.paragraph.setStyle': { + input: ParagraphsSetStyleInput; + options: MutationOptions; + output: ParagraphMutationResult; + }; + 'styles.paragraph.clearStyle': { + input: ParagraphsClearStyleInput; + options: MutationOptions; + output: ParagraphMutationResult; + }; + + // --- format.paragraph.* --- + 'format.paragraph.resetDirectFormatting': { + input: ParagraphsResetDirectFormattingInput; + options: MutationOptions; + output: ParagraphMutationResult; + }; + 'format.paragraph.setAlignment': { + input: ParagraphsSetAlignmentInput; + options: MutationOptions; + output: ParagraphMutationResult; + }; + 'format.paragraph.clearAlignment': { + input: ParagraphsClearAlignmentInput; + options: MutationOptions; + output: ParagraphMutationResult; + }; + 'format.paragraph.setIndentation': { + input: ParagraphsSetIndentationInput; + options: MutationOptions; + output: ParagraphMutationResult; + }; + 'format.paragraph.clearIndentation': { + input: ParagraphsClearIndentationInput; + options: MutationOptions; + output: ParagraphMutationResult; + }; + 'format.paragraph.setSpacing': { + input: ParagraphsSetSpacingInput; + options: MutationOptions; + output: ParagraphMutationResult; + }; + 'format.paragraph.clearSpacing': { + input: ParagraphsClearSpacingInput; + options: MutationOptions; + output: ParagraphMutationResult; + }; + 'format.paragraph.setKeepOptions': { + input: ParagraphsSetKeepOptionsInput; + options: MutationOptions; + output: ParagraphMutationResult; + }; + 'format.paragraph.setOutlineLevel': { + input: ParagraphsSetOutlineLevelInput; + options: MutationOptions; + output: ParagraphMutationResult; + }; + 'format.paragraph.setFlowOptions': { + input: ParagraphsSetFlowOptionsInput; + options: MutationOptions; + output: ParagraphMutationResult; + }; + 'format.paragraph.setTabStop': { + input: ParagraphsSetTabStopInput; + options: MutationOptions; + output: ParagraphMutationResult; + }; + 'format.paragraph.clearTabStop': { + input: ParagraphsClearTabStopInput; + options: MutationOptions; + output: ParagraphMutationResult; + }; + 'format.paragraph.clearAllTabStops': { + input: ParagraphsClearAllTabStopsInput; + options: MutationOptions; + output: ParagraphMutationResult; + }; + 'format.paragraph.setBorder': { + input: ParagraphsSetBorderInput; + options: MutationOptions; + output: ParagraphMutationResult; + }; + 'format.paragraph.clearBorder': { + input: ParagraphsClearBorderInput; + options: MutationOptions; + output: ParagraphMutationResult; + }; + 'format.paragraph.setShading': { + input: ParagraphsSetShadingInput; + options: MutationOptions; + output: ParagraphMutationResult; + }; + 'format.paragraph.clearShading': { + input: ParagraphsClearShadingInput; + options: MutationOptions; + output: ParagraphMutationResult; + }; // --- styles.* --- 'styles.apply': { input: StylesApplyInput; options: StylesApplyOptions; output: StylesApplyReceipt }; diff --git a/packages/document-api/src/contract/reference-doc-map.ts b/packages/document-api/src/contract/reference-doc-map.ts index 283e8f1836..687ab7d75a 100644 --- a/packages/document-api/src/contract/reference-doc-map.ts +++ b/packages/document-api/src/contract/reference-doc-map.ts @@ -81,6 +81,16 @@ const GROUP_METADATA: Record; @@ -335,6 +342,9 @@ const deletableBlockNodeAddressSchema = ref('DeletableBlockNodeAddress'); const paragraphAddressSchema = ref('ParagraphAddress'); const headingAddressSchema = ref('HeadingAddress'); const listItemAddressSchema = ref('ListItemAddress'); +const paragraphTargetSchema: JsonSchema = { + oneOf: [paragraphAddressSchema, headingAddressSchema, listItemAddressSchema], +}; const sectionAddressSchema = ref('SectionAddress'); const inlineNodeAddressSchema = ref('InlineNodeAddress'); const nodeAddressSchema = ref('NodeAddress'); @@ -971,6 +981,38 @@ function documentMutationResultSchemaFor(operationId: OperationId): JsonSchema { }; } +// --------------------------------------------------------------------------- +// Paragraph mutation result schemas +// --------------------------------------------------------------------------- + +const paragraphMutationTargetSchema = objectSchema({ target: paragraphTargetSchema }, ['target']); + +const paragraphMutationSuccessSchema = objectSchema( + { + success: { const: true }, + target: paragraphTargetSchema, + resolution: paragraphMutationTargetSchema, + }, + ['success', 'target', 'resolution'], +); + +function paragraphMutationFailureSchemaFor(operationId: OperationId): JsonSchema { + return objectSchema( + { + success: { const: false }, + failure: receiptFailureSchemaFor(operationId), + resolution: paragraphMutationTargetSchema, + }, + ['success', 'failure'], + ); +} + +function paragraphMutationResultSchemaFor(operationId: OperationId): JsonSchema { + return { + oneOf: [paragraphMutationSuccessSchema, paragraphMutationFailureSchemaFor(operationId)], + }; +} + const createSectionBreakSuccessSchema = objectSchema( { success: { const: true }, @@ -1398,18 +1440,6 @@ const operationSchemas: Record = { success: textMutationSuccessSchema, failure: textMutationFailureSchemaFor('format.color'), }, - 'format.align': { - input: objectSchema( - { - target: textAddressSchema, - alignment: { oneOf: [{ enum: [...ALIGNMENTS] }, { type: 'null' }] }, - }, - ['target', 'alignment'], - ), - output: textMutationResultSchemaFor('format.align'), - success: textMutationSuccessSchema, - failure: textMutationFailureSchemaFor('format.align'), - }, 'blocks.delete': { input: objectSchema( { @@ -1433,6 +1463,226 @@ const operationSchemas: Record = { ), failure: preApplyFailureResultSchemaFor('blocks.delete'), }, + + // --- styles.paragraph.* --- + 'styles.paragraph.setStyle': { + input: objectSchema({ target: paragraphTargetSchema, styleId: { type: 'string', minLength: 1 } }, [ + 'target', + 'styleId', + ]), + output: paragraphMutationResultSchemaFor('styles.paragraph.setStyle'), + success: paragraphMutationSuccessSchema, + failure: paragraphMutationFailureSchemaFor('styles.paragraph.setStyle'), + }, + 'styles.paragraph.clearStyle': { + input: objectSchema({ target: paragraphTargetSchema }, ['target']), + output: paragraphMutationResultSchemaFor('styles.paragraph.clearStyle'), + success: paragraphMutationSuccessSchema, + failure: paragraphMutationFailureSchemaFor('styles.paragraph.clearStyle'), + }, + + // --- format.paragraph.* --- + 'format.paragraph.resetDirectFormatting': { + input: objectSchema({ target: paragraphTargetSchema }, ['target']), + output: paragraphMutationResultSchemaFor('format.paragraph.resetDirectFormatting'), + success: paragraphMutationSuccessSchema, + failure: paragraphMutationFailureSchemaFor('format.paragraph.resetDirectFormatting'), + }, + 'format.paragraph.setAlignment': { + input: objectSchema({ target: paragraphTargetSchema, alignment: { enum: [...PARAGRAPH_ALIGNMENTS] } }, [ + 'target', + 'alignment', + ]), + output: paragraphMutationResultSchemaFor('format.paragraph.setAlignment'), + success: paragraphMutationSuccessSchema, + failure: paragraphMutationFailureSchemaFor('format.paragraph.setAlignment'), + }, + 'format.paragraph.clearAlignment': { + input: objectSchema({ target: paragraphTargetSchema }, ['target']), + output: paragraphMutationResultSchemaFor('format.paragraph.clearAlignment'), + success: paragraphMutationSuccessSchema, + failure: paragraphMutationFailureSchemaFor('format.paragraph.clearAlignment'), + }, + 'format.paragraph.setIndentation': { + input: { + ...objectSchema( + { + target: paragraphTargetSchema, + left: { type: 'integer', minimum: 0 }, + right: { type: 'integer', minimum: 0 }, + firstLine: { type: 'integer', minimum: 0 }, + hanging: { type: 'integer', minimum: 0 }, + }, + ['target'], + ), + anyOf: [{ required: ['left'] }, { required: ['right'] }, { required: ['firstLine'] }, { required: ['hanging'] }], + not: { required: ['firstLine', 'hanging'] }, + }, + output: paragraphMutationResultSchemaFor('format.paragraph.setIndentation'), + success: paragraphMutationSuccessSchema, + failure: paragraphMutationFailureSchemaFor('format.paragraph.setIndentation'), + }, + 'format.paragraph.clearIndentation': { + input: objectSchema({ target: paragraphTargetSchema }, ['target']), + output: paragraphMutationResultSchemaFor('format.paragraph.clearIndentation'), + success: paragraphMutationSuccessSchema, + failure: paragraphMutationFailureSchemaFor('format.paragraph.clearIndentation'), + }, + 'format.paragraph.setSpacing': { + input: { + ...objectSchema( + { + target: paragraphTargetSchema, + before: { type: 'integer', minimum: 0 }, + after: { type: 'integer', minimum: 0 }, + line: { type: 'integer', minimum: 1 }, + lineRule: { enum: [...LINE_RULES] }, + }, + ['target'], + ), + anyOf: [{ required: ['before'] }, { required: ['after'] }, { required: ['line'] }, { required: ['lineRule'] }], + if: { required: ['line'] }, + then: { required: ['lineRule'] }, + }, + output: paragraphMutationResultSchemaFor('format.paragraph.setSpacing'), + success: paragraphMutationSuccessSchema, + failure: paragraphMutationFailureSchemaFor('format.paragraph.setSpacing'), + }, + 'format.paragraph.clearSpacing': { + input: objectSchema({ target: paragraphTargetSchema }, ['target']), + output: paragraphMutationResultSchemaFor('format.paragraph.clearSpacing'), + success: paragraphMutationSuccessSchema, + failure: paragraphMutationFailureSchemaFor('format.paragraph.clearSpacing'), + }, + 'format.paragraph.setKeepOptions': { + input: { + ...objectSchema( + { + target: paragraphTargetSchema, + keepNext: { type: 'boolean' }, + keepLines: { type: 'boolean' }, + widowControl: { type: 'boolean' }, + }, + ['target'], + ), + oneOf: [ + { required: ['target', 'keepNext'] }, + { required: ['target', 'keepLines'] }, + { required: ['target', 'widowControl'] }, + ], + }, + output: paragraphMutationResultSchemaFor('format.paragraph.setKeepOptions'), + success: paragraphMutationSuccessSchema, + failure: paragraphMutationFailureSchemaFor('format.paragraph.setKeepOptions'), + }, + 'format.paragraph.setOutlineLevel': { + input: objectSchema( + { + target: paragraphTargetSchema, + outlineLevel: { oneOf: [{ type: 'integer', minimum: 0, maximum: 9 }, { type: 'null' }] }, + }, + ['target', 'outlineLevel'], + ), + output: paragraphMutationResultSchemaFor('format.paragraph.setOutlineLevel'), + success: paragraphMutationSuccessSchema, + failure: paragraphMutationFailureSchemaFor('format.paragraph.setOutlineLevel'), + }, + 'format.paragraph.setFlowOptions': { + input: { + ...objectSchema( + { + target: paragraphTargetSchema, + contextualSpacing: { type: 'boolean' }, + pageBreakBefore: { type: 'boolean' }, + suppressAutoHyphens: { type: 'boolean' }, + }, + ['target'], + ), + oneOf: [ + { required: ['target', 'contextualSpacing'] }, + { required: ['target', 'pageBreakBefore'] }, + { required: ['target', 'suppressAutoHyphens'] }, + ], + }, + output: paragraphMutationResultSchemaFor('format.paragraph.setFlowOptions'), + success: paragraphMutationSuccessSchema, + failure: paragraphMutationFailureSchemaFor('format.paragraph.setFlowOptions'), + }, + 'format.paragraph.setTabStop': { + input: objectSchema( + { + target: paragraphTargetSchema, + position: { type: 'integer', minimum: 0 }, + alignment: { enum: [...TAB_STOP_ALIGNMENTS] }, + leader: { enum: [...TAB_STOP_LEADERS] }, + }, + ['target', 'position', 'alignment'], + ), + output: paragraphMutationResultSchemaFor('format.paragraph.setTabStop'), + success: paragraphMutationSuccessSchema, + failure: paragraphMutationFailureSchemaFor('format.paragraph.setTabStop'), + }, + 'format.paragraph.clearTabStop': { + input: objectSchema({ target: paragraphTargetSchema, position: { type: 'integer', minimum: 0 } }, [ + 'target', + 'position', + ]), + output: paragraphMutationResultSchemaFor('format.paragraph.clearTabStop'), + success: paragraphMutationSuccessSchema, + failure: paragraphMutationFailureSchemaFor('format.paragraph.clearTabStop'), + }, + 'format.paragraph.clearAllTabStops': { + input: objectSchema({ target: paragraphTargetSchema }, ['target']), + output: paragraphMutationResultSchemaFor('format.paragraph.clearAllTabStops'), + success: paragraphMutationSuccessSchema, + failure: paragraphMutationFailureSchemaFor('format.paragraph.clearAllTabStops'), + }, + 'format.paragraph.setBorder': { + input: objectSchema( + { + target: paragraphTargetSchema, + side: { enum: [...BORDER_SIDES] }, + style: { type: 'string', minLength: 1 }, + color: { type: 'string', minLength: 1 }, + size: { type: 'integer', minimum: 0 }, + space: { type: 'integer', minimum: 0 }, + }, + ['target', 'side', 'style'], + ), + output: paragraphMutationResultSchemaFor('format.paragraph.setBorder'), + success: paragraphMutationSuccessSchema, + failure: paragraphMutationFailureSchemaFor('format.paragraph.setBorder'), + }, + 'format.paragraph.clearBorder': { + input: objectSchema({ target: paragraphTargetSchema, side: { enum: [...CLEAR_BORDER_SIDES] } }, ['target', 'side']), + output: paragraphMutationResultSchemaFor('format.paragraph.clearBorder'), + success: paragraphMutationSuccessSchema, + failure: paragraphMutationFailureSchemaFor('format.paragraph.clearBorder'), + }, + 'format.paragraph.setShading': { + input: { + ...objectSchema( + { + target: paragraphTargetSchema, + fill: { type: 'string', minLength: 1 }, + color: { type: 'string', minLength: 1 }, + pattern: { type: 'string', minLength: 1 }, + }, + ['target'], + ), + oneOf: [{ required: ['target', 'fill'] }, { required: ['target', 'color'] }, { required: ['target', 'pattern'] }], + }, + output: paragraphMutationResultSchemaFor('format.paragraph.setShading'), + success: paragraphMutationSuccessSchema, + failure: paragraphMutationFailureSchemaFor('format.paragraph.setShading'), + }, + 'format.paragraph.clearShading': { + input: objectSchema({ target: paragraphTargetSchema }, ['target']), + output: paragraphMutationResultSchemaFor('format.paragraph.clearShading'), + success: paragraphMutationSuccessSchema, + failure: paragraphMutationFailureSchemaFor('format.paragraph.clearShading'), + }, + 'styles.apply': (() => { // --- Sub-schemas for object properties (all require minProperties: 1) --- const fontFamilySchema = { diff --git a/packages/document-api/src/contract/types.test.ts b/packages/document-api/src/contract/types.test.ts index 1a03cb80be..3d0d8f799e 100644 --- a/packages/document-api/src/contract/types.test.ts +++ b/packages/document-api/src/contract/types.test.ts @@ -14,6 +14,12 @@ describe('isValidOperationIdFormat', () => { expect(isValidOperationIdFormat('lists.setType')).toBe(true); }); + it('accepts three-segment identifiers (group.subgroup.camelCase)', () => { + expect(isValidOperationIdFormat('format.paragraph.setAlignment')).toBe(true); + expect(isValidOperationIdFormat('styles.paragraph.setStyle')).toBe(true); + expect(isValidOperationIdFormat('a.b.c')).toBe(true); + }); + it('rejects empty strings', () => { expect(isValidOperationIdFormat('')).toBe(false); }); @@ -23,8 +29,8 @@ describe('isValidOperationIdFormat', () => { expect(isValidOperationIdFormat('Comments.add')).toBe(false); }); - it('rejects identifiers with multiple dots', () => { - expect(isValidOperationIdFormat('a.b.c')).toBe(false); + it('rejects identifiers with four or more segments', () => { + expect(isValidOperationIdFormat('a.b.c.d')).toBe(false); }); it('rejects identifiers with special characters', () => { diff --git a/packages/document-api/src/contract/types.ts b/packages/document-api/src/contract/types.ts index 43e18ee970..19b50153a0 100644 --- a/packages/document-api/src/contract/types.ts +++ b/packages/document-api/src/contract/types.ts @@ -26,11 +26,11 @@ export type CommandCatalog = { readonly [K in OperationId]: CommandStaticMetadata; }; -const OPERATION_ID_FORMAT = /^(?:[a-z][a-zA-Z0-9]*|[a-z][a-zA-Z0-9]*\.[a-z][a-zA-Z0-9]*)$/; +const OPERATION_ID_FORMAT = /^[a-z][a-zA-Z0-9]*(?:\.[a-z][a-zA-Z0-9]*){0,2}$/; /** * Checks whether a string matches the syntactic format of an operation ID - * (`camelCase` or `namespace.camelCase`). + * (`camelCase`, `namespace.camelCase`, or `group.subgroup.camelCase`). * * @param operationId - The string to validate. * @returns `true` if the string matches the expected format. diff --git a/packages/document-api/src/format/format.test.ts b/packages/document-api/src/format/format.test.ts index 7b42448bc1..f42e25e873 100644 --- a/packages/document-api/src/format/format.test.ts +++ b/packages/document-api/src/format/format.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; import type { FormatAdapter, StyleApplyInput } from './format.js'; -import { executeStyleApply, executeFontSize, executeFontFamily, executeColor, executeAlign } from './format.js'; +import { executeStyleApply, executeFontSize, executeFontFamily, executeColor } from './format.js'; import { DocumentApiValidationError } from '../errors.js'; import type { TextMutationReceipt } from '../types/index.js'; @@ -25,7 +25,6 @@ function makeAdapter(): FormatAdapter & Record> fontSize: vi.fn(() => makeReceipt()), fontFamily: vi.fn(() => makeReceipt()), color: vi.fn(() => makeReceipt()), - align: vi.fn(() => makeReceipt()), }; } @@ -406,43 +405,3 @@ describe('executeColor validation', () => { expect(adapter.color).toHaveBeenCalled(); }); }); - -// --------------------------------------------------------------------------- -// executeAlign validation -// --------------------------------------------------------------------------- - -describe('executeAlign validation', () => { - targetValidationSuite('format.align', (a, i) => executeAlign(a, i as any)); - - it('rejects missing alignment', () => { - expect(() => executeAlign(makeAdapter(), { target: TARGET } as any)).toThrow('requires an alignment'); - }); - - it('rejects invalid alignment value', () => { - expect(() => executeAlign(makeAdapter(), { target: TARGET, alignment: 'middle' } as any)).toThrow( - 'left, center, right, justify', - ); - }); - - it('rejects empty string alignment', () => { - expect(() => executeAlign(makeAdapter(), { target: TARGET, alignment: '' } as any)).toThrow( - 'left, center, right, justify', - ); - }); - - it('rejects unknown fields', () => { - expect(() => executeAlign(makeAdapter(), { target: TARGET, alignment: 'left', extra: 1 } as any)).toThrow('extra'); - }); - - it('accepts null alignment (unset)', () => { - const adapter = makeAdapter(); - executeAlign(adapter, { target: TARGET, alignment: null }); - expect(adapter.align).toHaveBeenCalled(); - }); - - it.each(['left', 'center', 'right', 'justify'] as const)('accepts alignment "%s"', (alignment) => { - const adapter = makeAdapter(); - executeAlign(adapter, { target: TARGET, alignment }); - expect(adapter.align).toHaveBeenCalled(); - }); -}); diff --git a/packages/document-api/src/format/format.ts b/packages/document-api/src/format/format.ts index e3116168dc..d1123b5f7d 100644 --- a/packages/document-api/src/format/format.ts +++ b/packages/document-api/src/format/format.ts @@ -4,15 +4,6 @@ import { MARK_KEY_SET, INLINE_DIRECTIVE_SET } from '../types/style-policy.types. import { DocumentApiValidationError } from '../errors.js'; import { isRecord, isTextAddress, assertNoUnknownFields } from '../validation-primitives.js'; -// --------------------------------------------------------------------------- -// Alignment enum -// --------------------------------------------------------------------------- - -/** Valid paragraph alignment values. */ -export const ALIGNMENTS = ['left', 'center', 'right', 'justify'] as const; -export type Alignment = (typeof ALIGNMENTS)[number]; -const ALIGNMENT_SET: ReadonlySet = new Set(ALIGNMENTS); - // --------------------------------------------------------------------------- // Input types — boolean toggle marks (existing) // --------------------------------------------------------------------------- @@ -81,12 +72,6 @@ export interface FormatColorInput { value: string | null; } -/** Input payload for `format.align`. Pass `null` to unset (reset to default). */ -export interface FormatAlignInput { - target: TextAddress; - alignment: Alignment | null; -} - // --------------------------------------------------------------------------- // Adapter interface // --------------------------------------------------------------------------- @@ -103,7 +88,6 @@ export interface FormatAdapter { fontSize(input: FormatFontSizeInput, options?: MutationOptions): TextMutationReceipt; fontFamily(input: FormatFontFamilyInput, options?: MutationOptions): TextMutationReceipt; color(input: FormatColorInput, options?: MutationOptions): TextMutationReceipt; - align(input: FormatAlignInput, options?: MutationOptions): TextMutationReceipt; } // --------------------------------------------------------------------------- @@ -123,7 +107,6 @@ export interface FormatApi { fontSize(input: FormatFontSizeInput, options?: MutationOptions): TextMutationReceipt; fontFamily(input: FormatFontFamilyInput, options?: MutationOptions): TextMutationReceipt; color(input: FormatColorInput, options?: MutationOptions): TextMutationReceipt; - align(input: FormatAlignInput, options?: MutationOptions): TextMutationReceipt; } // --------------------------------------------------------------------------- @@ -351,35 +334,3 @@ export function executeColor( validateColorInput(input); return adapter.color(input, normalizeMutationOptions(options)); } - -// --------------------------------------------------------------------------- -// format.align — validation and execution -// --------------------------------------------------------------------------- - -const ALIGN_ALLOWED_KEYS = new Set(['target', 'alignment']); - -function validateAlignInput(input: unknown): asserts input is FormatAlignInput { - validateTarget(input, 'format.align'); - assertNoUnknownFields(input as Record, ALIGN_ALLOWED_KEYS, 'format.align'); - - const { alignment } = input as Record; - if (alignment === undefined) { - throw new DocumentApiValidationError('INVALID_INPUT', 'format.align requires an alignment field.'); - } - if (alignment !== null && (typeof alignment !== 'string' || !ALIGNMENT_SET.has(alignment))) { - throw new DocumentApiValidationError( - 'INVALID_INPUT', - `format.align alignment must be one of ${ALIGNMENTS.join(', ')}, or null.`, - { field: 'alignment', value: alignment }, - ); - } -} - -export function executeAlign( - adapter: FormatAdapter, - input: FormatAlignInput, - options?: MutationOptions, -): TextMutationReceipt { - validateAlignInput(input); - return adapter.align(input, normalizeMutationOptions(options)); -} diff --git a/packages/document-api/src/index.test.ts b/packages/document-api/src/index.test.ts index a32199e800..9fc0df2247 100644 --- a/packages/document-api/src/index.test.ts +++ b/packages/document-api/src/index.test.ts @@ -123,7 +123,6 @@ function makeFormatAdapter(): FormatAdapter { fontSize: vi.fn(() => makeFormatReceipt()), fontFamily: vi.fn(() => makeFormatReceipt()), color: vi.fn(() => makeFormatReceipt()), - align: vi.fn(() => makeFormatReceipt()), }; } @@ -727,29 +726,6 @@ describe('createDocumentApi', () => { ); }); - it('delegates format.align to adapter.align', () => { - const formatAdpt = makeFormatAdapter(); - const api = createDocumentApi({ - find: makeFindAdapter(QUERY_RESULT), - getNode: makeGetNodeAdapter(PARAGRAPH_INFO), - getText: makeGetTextAdapter(), - info: makeInfoAdapter(), - comments: makeCommentsAdapter(), - write: makeWriteAdapter(), - format: formatAdpt, - trackChanges: makeTrackChangesAdapter(), - create: makeCreateAdapter(), - lists: makeListsAdapter(), - }); - - const target = { kind: 'text', blockId: 'p1', range: { start: 0, end: 2 } } as const; - api.format.align({ target, alignment: 'center' }); - expect(formatAdpt.align).toHaveBeenCalledWith( - { target, alignment: 'center' }, - { changeMode: 'direct', dryRun: false }, - ); - }); - it('delegates trackChanges read operations', () => { const trackAdpt = makeTrackChangesAdapter(); const api = createDocumentApi({ diff --git a/packages/document-api/src/index.ts b/packages/document-api/src/index.ts index d44c57bcb5..47aca64d56 100644 --- a/packages/document-api/src/index.ts +++ b/packages/document-api/src/index.ts @@ -56,9 +56,8 @@ import type { FormatFontSizeInput, FormatFontFamilyInput, FormatColorInput, - FormatAlignInput, } from './format/format.js'; -import { executeStyleApply, executeFontSize, executeFontFamily, executeColor, executeAlign } from './format/format.js'; +import { executeStyleApply, executeFontSize, executeFontFamily, executeColor } from './format/format.js'; import type { StylesAdapter, StylesApi, @@ -177,6 +176,52 @@ 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 { + ParagraphsAdapter, + ParagraphFormatApi, + ParagraphStylesApi, + ParagraphsSetStyleInput, + ParagraphsClearStyleInput, + ParagraphsResetDirectFormattingInput, + ParagraphsSetAlignmentInput, + ParagraphsClearAlignmentInput, + ParagraphsSetIndentationInput, + ParagraphsClearIndentationInput, + ParagraphsSetSpacingInput, + ParagraphsClearSpacingInput, + ParagraphsSetKeepOptionsInput, + ParagraphsSetOutlineLevelInput, + ParagraphsSetFlowOptionsInput, + ParagraphsSetTabStopInput, + ParagraphsClearTabStopInput, + ParagraphsClearAllTabStopsInput, + ParagraphsSetBorderInput, + ParagraphsClearBorderInput, + ParagraphsSetShadingInput, + ParagraphsClearShadingInput, + ParagraphMutationResult, +} from './paragraphs/paragraphs.js'; +import { + executeParagraphsSetStyle, + executeParagraphsClearStyle, + executeParagraphsResetDirectFormatting, + executeParagraphsSetAlignment, + executeParagraphsClearAlignment, + executeParagraphsSetIndentation, + executeParagraphsClearIndentation, + executeParagraphsSetSpacing, + executeParagraphsClearSpacing, + executeParagraphsSetKeepOptions, + executeParagraphsSetOutlineLevel, + executeParagraphsSetFlowOptions, + executeParagraphsSetTabStop, + executeParagraphsClearTabStop, + executeParagraphsClearAllTabStops, + executeParagraphsSetBorder, + executeParagraphsClearBorder, + executeParagraphsSetShading, + executeParagraphsClearShading, +} from './paragraphs/paragraphs.js'; import type { SectionsAdapter, SectionsApi } from './sections/sections.js'; import type { CreateSectionBreakInput, @@ -241,9 +286,7 @@ export type { FormatFontSizeInput, FormatFontFamilyInput, FormatColorInput, - FormatAlignInput, } from './format/format.js'; -export { ALIGNMENTS, type Alignment } from './format/format.js'; export { PROPERTY_REGISTRY } from './styles/styles.js'; export type { PropertyDefinition, @@ -282,6 +325,48 @@ export type { export type { BlocksAdapter } from './blocks/blocks.js'; export type { ListsAdapter } from './lists/lists.js'; export type { SectionsAdapter } from './sections/sections.js'; +export type { ParagraphsAdapter, ParagraphFormatApi, ParagraphStylesApi } from './paragraphs/paragraphs.js'; +export type { + ParagraphTarget, + ParagraphBlockType, + ParagraphMutationResult, + ParagraphMutationSuccess, + ParagraphMutationFailure, + MutationResolution, + ParagraphAlignment, + TabStopAlignment, + TabStopLeader, + BorderSide, + ClearBorderSide, + LineRule, + ParagraphsSetStyleInput, + ParagraphsClearStyleInput, + ParagraphsResetDirectFormattingInput, + ParagraphsSetAlignmentInput, + ParagraphsClearAlignmentInput, + ParagraphsSetIndentationInput, + ParagraphsClearIndentationInput, + ParagraphsSetSpacingInput, + ParagraphsClearSpacingInput, + ParagraphsSetKeepOptionsInput, + ParagraphsSetOutlineLevelInput, + ParagraphsSetFlowOptionsInput, + ParagraphsSetTabStopInput, + ParagraphsClearTabStopInput, + ParagraphsClearAllTabStopsInput, + ParagraphsSetBorderInput, + ParagraphsClearBorderInput, + ParagraphsSetShadingInput, + ParagraphsClearShadingInput, +} from './paragraphs/paragraphs.js'; +export { + PARAGRAPH_ALIGNMENTS, + TAB_STOP_ALIGNMENTS, + TAB_STOP_LEADERS, + BORDER_SIDES, + CLEAR_BORDER_SIDES, + LINE_RULES, +} from './paragraphs/paragraphs.js'; export type { ListInsertInput, ListItemAddress, @@ -495,13 +580,13 @@ export interface DocumentApi { */ delete(input: DeleteInput, options?: MutationOptions): TextMutationReceipt; /** - * Formatting operations. + * Formatting operations (inline and paragraph direct formatting). */ - format: FormatApi; + format: FormatApi & { paragraph: ParagraphFormatApi }; /** - * Stylesheet operations (docDefaults, style definitions). + * Stylesheet operations (docDefaults, style definitions, paragraph style references). */ - styles: StylesApi; + styles: StylesApi & { paragraph: ParagraphStylesApi }; /** * Tracked-change operations (list, get, decide). */ @@ -570,6 +655,7 @@ export interface DocumentApiAdapters { blocks: BlocksAdapter; lists: ListsAdapter; sections: SectionsAdapter; + paragraphs: ParagraphsAdapter; tables: TablesAdapter; query: QueryAdapter; mutations: MutationsAdapter; @@ -662,14 +748,75 @@ export function createDocumentApi(adapters: DocumentApiAdapters): DocumentApi { color(input: FormatColorInput, options?: MutationOptions): TextMutationReceipt { return executeColor(adapters.format, input, options); }, - align(input: FormatAlignInput, options?: MutationOptions): TextMutationReceipt { - return executeAlign(adapters.format, input, options); + paragraph: { + resetDirectFormatting( + input: ParagraphsResetDirectFormattingInput, + options?: MutationOptions, + ): ParagraphMutationResult { + return executeParagraphsResetDirectFormatting(adapters.paragraphs, input, options); + }, + setAlignment(input: ParagraphsSetAlignmentInput, options?: MutationOptions): ParagraphMutationResult { + return executeParagraphsSetAlignment(adapters.paragraphs, input, options); + }, + clearAlignment(input: ParagraphsClearAlignmentInput, options?: MutationOptions): ParagraphMutationResult { + return executeParagraphsClearAlignment(adapters.paragraphs, input, options); + }, + setIndentation(input: ParagraphsSetIndentationInput, options?: MutationOptions): ParagraphMutationResult { + return executeParagraphsSetIndentation(adapters.paragraphs, input, options); + }, + clearIndentation(input: ParagraphsClearIndentationInput, options?: MutationOptions): ParagraphMutationResult { + return executeParagraphsClearIndentation(adapters.paragraphs, input, options); + }, + setSpacing(input: ParagraphsSetSpacingInput, options?: MutationOptions): ParagraphMutationResult { + return executeParagraphsSetSpacing(adapters.paragraphs, input, options); + }, + clearSpacing(input: ParagraphsClearSpacingInput, options?: MutationOptions): ParagraphMutationResult { + return executeParagraphsClearSpacing(adapters.paragraphs, input, options); + }, + setKeepOptions(input: ParagraphsSetKeepOptionsInput, options?: MutationOptions): ParagraphMutationResult { + return executeParagraphsSetKeepOptions(adapters.paragraphs, input, options); + }, + setOutlineLevel(input: ParagraphsSetOutlineLevelInput, options?: MutationOptions): ParagraphMutationResult { + return executeParagraphsSetOutlineLevel(adapters.paragraphs, input, options); + }, + setFlowOptions(input: ParagraphsSetFlowOptionsInput, options?: MutationOptions): ParagraphMutationResult { + return executeParagraphsSetFlowOptions(adapters.paragraphs, input, options); + }, + setTabStop(input: ParagraphsSetTabStopInput, options?: MutationOptions): ParagraphMutationResult { + return executeParagraphsSetTabStop(adapters.paragraphs, input, options); + }, + clearTabStop(input: ParagraphsClearTabStopInput, options?: MutationOptions): ParagraphMutationResult { + return executeParagraphsClearTabStop(adapters.paragraphs, input, options); + }, + clearAllTabStops(input: ParagraphsClearAllTabStopsInput, options?: MutationOptions): ParagraphMutationResult { + return executeParagraphsClearAllTabStops(adapters.paragraphs, input, options); + }, + setBorder(input: ParagraphsSetBorderInput, options?: MutationOptions): ParagraphMutationResult { + return executeParagraphsSetBorder(adapters.paragraphs, input, options); + }, + clearBorder(input: ParagraphsClearBorderInput, options?: MutationOptions): ParagraphMutationResult { + return executeParagraphsClearBorder(adapters.paragraphs, input, options); + }, + setShading(input: ParagraphsSetShadingInput, options?: MutationOptions): ParagraphMutationResult { + return executeParagraphsSetShading(adapters.paragraphs, input, options); + }, + clearShading(input: ParagraphsClearShadingInput, options?: MutationOptions): ParagraphMutationResult { + return executeParagraphsClearShading(adapters.paragraphs, input, options); + }, }, }, styles: { apply(input: StylesApplyInput, options?: StylesApplyOptions): StylesApplyReceipt { return executeStylesApply(adapters.styles, input, options); }, + paragraph: { + setStyle(input: ParagraphsSetStyleInput, options?: MutationOptions): ParagraphMutationResult { + return executeParagraphsSetStyle(adapters.paragraphs, input, options); + }, + clearStyle(input: ParagraphsClearStyleInput, options?: MutationOptions): ParagraphMutationResult { + return executeParagraphsClearStyle(adapters.paragraphs, input, options); + }, + }, }, trackChanges: { list(input?: TrackChangesListInput): TrackChangesListResult { diff --git a/packages/document-api/src/invoke/invoke.test.ts b/packages/document-api/src/invoke/invoke.test.ts index a269d35627..150365e927 100644 --- a/packages/document-api/src/invoke/invoke.test.ts +++ b/packages/document-api/src/invoke/invoke.test.ts @@ -97,7 +97,6 @@ function makeAdapters() { fontSize: vi.fn(formatReceipt), fontFamily: vi.fn(formatReceipt), color: vi.fn(formatReceipt), - align: vi.fn(formatReceipt), }; const stylesAdapter: StylesAdapter = { apply: vi.fn(() => ({ @@ -338,18 +337,6 @@ describe('invoke', () => { expect(invoked).toEqual(direct); }); - it('format.align: invoke returns same result as direct call', () => { - const { adapters } = makeAdapters(); - const api = createDocumentApi(adapters); - const input = { - target: { kind: 'text' as const, blockId: 'p1', range: { start: 0, end: 2 } }, - alignment: 'center' as const, - }; - const direct = api.format.align(input); - const invoked = api.invoke({ operationId: 'format.align', input }); - expect(invoked).toEqual(direct); - }); - it('styles.apply: invoke returns same result as direct call', () => { const { adapters } = makeAdapters(); const api = createDocumentApi(adapters); diff --git a/packages/document-api/src/invoke/invoke.ts b/packages/document-api/src/invoke/invoke.ts index 1e4997305a..4ca77d268d 100644 --- a/packages/document-api/src/invoke/invoke.ts +++ b/packages/document-api/src/invoke/invoke.ts @@ -52,7 +52,29 @@ export function buildDispatchTable(api: DocumentApi): TypedDispatchTable { 'format.fontSize': (input, options) => api.format.fontSize(input, options), 'format.fontFamily': (input, options) => api.format.fontFamily(input, options), 'format.color': (input, options) => api.format.color(input, options), - 'format.align': (input, options) => api.format.align(input, options), + // --- styles.paragraph.* --- + 'styles.paragraph.setStyle': (input, options) => api.styles.paragraph.setStyle(input, options), + 'styles.paragraph.clearStyle': (input, options) => api.styles.paragraph.clearStyle(input, options), + + // --- format.paragraph.* --- + 'format.paragraph.resetDirectFormatting': (input, options) => + api.format.paragraph.resetDirectFormatting(input, options), + 'format.paragraph.setAlignment': (input, options) => api.format.paragraph.setAlignment(input, options), + 'format.paragraph.clearAlignment': (input, options) => api.format.paragraph.clearAlignment(input, options), + 'format.paragraph.setIndentation': (input, options) => api.format.paragraph.setIndentation(input, options), + 'format.paragraph.clearIndentation': (input, options) => api.format.paragraph.clearIndentation(input, options), + 'format.paragraph.setSpacing': (input, options) => api.format.paragraph.setSpacing(input, options), + 'format.paragraph.clearSpacing': (input, options) => api.format.paragraph.clearSpacing(input, options), + 'format.paragraph.setKeepOptions': (input, options) => api.format.paragraph.setKeepOptions(input, options), + 'format.paragraph.setOutlineLevel': (input, options) => api.format.paragraph.setOutlineLevel(input, options), + 'format.paragraph.setFlowOptions': (input, options) => api.format.paragraph.setFlowOptions(input, options), + 'format.paragraph.setTabStop': (input, options) => api.format.paragraph.setTabStop(input, options), + 'format.paragraph.clearTabStop': (input, options) => api.format.paragraph.clearTabStop(input, options), + 'format.paragraph.clearAllTabStops': (input, options) => api.format.paragraph.clearAllTabStops(input, options), + 'format.paragraph.setBorder': (input, options) => api.format.paragraph.setBorder(input, options), + 'format.paragraph.clearBorder': (input, options) => api.format.paragraph.clearBorder(input, options), + 'format.paragraph.setShading': (input, options) => api.format.paragraph.setShading(input, options), + 'format.paragraph.clearShading': (input, options) => api.format.paragraph.clearShading(input, options), // --- styles.* --- 'styles.apply': (input, options) => api.styles.apply(input, options), diff --git a/packages/document-api/src/paragraphs/paragraphs.ts b/packages/document-api/src/paragraphs/paragraphs.ts new file mode 100644 index 0000000000..6fa93ea770 --- /dev/null +++ b/packages/document-api/src/paragraphs/paragraphs.ts @@ -0,0 +1,674 @@ +/** + * Engine-agnostic paragraphs domain module. + * + * Defines the adapter interface, validation, and execute* functions + * for all 19 `format.paragraph.*` / `styles.paragraph.*` operations. + */ + +import { normalizeMutationOptions, type MutationOptions } from '../write/write.js'; +import { DocumentApiValidationError } from '../errors.js'; +import { isRecord, assertNoUnknownFields } from '../validation-primitives.js'; +import type { + ParagraphTarget, + ParagraphMutationResult, + ParagraphsSetStyleInput, + ParagraphsClearStyleInput, + ParagraphsResetDirectFormattingInput, + ParagraphsSetAlignmentInput, + ParagraphsClearAlignmentInput, + ParagraphsSetIndentationInput, + ParagraphsClearIndentationInput, + ParagraphsSetSpacingInput, + ParagraphsClearSpacingInput, + ParagraphsSetKeepOptionsInput, + ParagraphsSetOutlineLevelInput, + ParagraphsSetFlowOptionsInput, + ParagraphsSetTabStopInput, + ParagraphsClearTabStopInput, + ParagraphsClearAllTabStopsInput, + ParagraphsSetBorderInput, + ParagraphsClearBorderInput, + ParagraphsSetShadingInput, + ParagraphsClearShadingInput, +} from './paragraphs.types.js'; +import { + PARAGRAPH_ALIGNMENTS, + TAB_STOP_ALIGNMENTS, + TAB_STOP_LEADERS, + BORDER_SIDES, + CLEAR_BORDER_SIDES, + LINE_RULES, +} from './paragraphs.types.js'; + +// Re-export types +export type { + ParagraphTarget, + ParagraphBlockType, + ParagraphMutationResult, + ParagraphMutationSuccess, + ParagraphMutationFailure, + MutationResolution, + ParagraphAlignment, + TabStopAlignment, + TabStopLeader, + BorderSide, + ClearBorderSide, + LineRule, + ParagraphsSetStyleInput, + ParagraphsClearStyleInput, + ParagraphsResetDirectFormattingInput, + ParagraphsSetAlignmentInput, + ParagraphsClearAlignmentInput, + ParagraphsSetIndentationInput, + ParagraphsClearIndentationInput, + ParagraphsSetSpacingInput, + ParagraphsClearSpacingInput, + ParagraphsSetKeepOptionsInput, + ParagraphsSetOutlineLevelInput, + ParagraphsSetFlowOptionsInput, + ParagraphsSetTabStopInput, + ParagraphsClearTabStopInput, + ParagraphsClearAllTabStopsInput, + ParagraphsSetBorderInput, + ParagraphsClearBorderInput, + ParagraphsSetShadingInput, + ParagraphsClearShadingInput, +} from './paragraphs.types.js'; +export { + PARAGRAPH_ALIGNMENTS, + TAB_STOP_ALIGNMENTS, + TAB_STOP_LEADERS, + BORDER_SIDES, + CLEAR_BORDER_SIDES, + LINE_RULES, +} from './paragraphs.types.js'; + +// --------------------------------------------------------------------------- +// Adapter interface +// --------------------------------------------------------------------------- + +export interface ParagraphsAdapter { + setStyle(input: ParagraphsSetStyleInput, options?: MutationOptions): ParagraphMutationResult; + clearStyle(input: ParagraphsClearStyleInput, options?: MutationOptions): ParagraphMutationResult; + resetDirectFormatting( + input: ParagraphsResetDirectFormattingInput, + options?: MutationOptions, + ): ParagraphMutationResult; + setAlignment(input: ParagraphsSetAlignmentInput, options?: MutationOptions): ParagraphMutationResult; + clearAlignment(input: ParagraphsClearAlignmentInput, options?: MutationOptions): ParagraphMutationResult; + setIndentation(input: ParagraphsSetIndentationInput, options?: MutationOptions): ParagraphMutationResult; + clearIndentation(input: ParagraphsClearIndentationInput, options?: MutationOptions): ParagraphMutationResult; + setSpacing(input: ParagraphsSetSpacingInput, options?: MutationOptions): ParagraphMutationResult; + clearSpacing(input: ParagraphsClearSpacingInput, options?: MutationOptions): ParagraphMutationResult; + setKeepOptions(input: ParagraphsSetKeepOptionsInput, options?: MutationOptions): ParagraphMutationResult; + setOutlineLevel(input: ParagraphsSetOutlineLevelInput, options?: MutationOptions): ParagraphMutationResult; + setFlowOptions(input: ParagraphsSetFlowOptionsInput, options?: MutationOptions): ParagraphMutationResult; + setTabStop(input: ParagraphsSetTabStopInput, options?: MutationOptions): ParagraphMutationResult; + clearTabStop(input: ParagraphsClearTabStopInput, options?: MutationOptions): ParagraphMutationResult; + clearAllTabStops(input: ParagraphsClearAllTabStopsInput, options?: MutationOptions): ParagraphMutationResult; + setBorder(input: ParagraphsSetBorderInput, options?: MutationOptions): ParagraphMutationResult; + clearBorder(input: ParagraphsClearBorderInput, options?: MutationOptions): ParagraphMutationResult; + setShading(input: ParagraphsSetShadingInput, options?: MutationOptions): ParagraphMutationResult; + clearShading(input: ParagraphsClearShadingInput, options?: MutationOptions): ParagraphMutationResult; +} + +/** Public API surface for `format.paragraph.*` — direct paragraph formatting. */ +export interface ParagraphFormatApi { + resetDirectFormatting( + input: ParagraphsResetDirectFormattingInput, + options?: MutationOptions, + ): ParagraphMutationResult; + setAlignment(input: ParagraphsSetAlignmentInput, options?: MutationOptions): ParagraphMutationResult; + clearAlignment(input: ParagraphsClearAlignmentInput, options?: MutationOptions): ParagraphMutationResult; + setIndentation(input: ParagraphsSetIndentationInput, options?: MutationOptions): ParagraphMutationResult; + clearIndentation(input: ParagraphsClearIndentationInput, options?: MutationOptions): ParagraphMutationResult; + setSpacing(input: ParagraphsSetSpacingInput, options?: MutationOptions): ParagraphMutationResult; + clearSpacing(input: ParagraphsClearSpacingInput, options?: MutationOptions): ParagraphMutationResult; + setKeepOptions(input: ParagraphsSetKeepOptionsInput, options?: MutationOptions): ParagraphMutationResult; + setOutlineLevel(input: ParagraphsSetOutlineLevelInput, options?: MutationOptions): ParagraphMutationResult; + setFlowOptions(input: ParagraphsSetFlowOptionsInput, options?: MutationOptions): ParagraphMutationResult; + setTabStop(input: ParagraphsSetTabStopInput, options?: MutationOptions): ParagraphMutationResult; + clearTabStop(input: ParagraphsClearTabStopInput, options?: MutationOptions): ParagraphMutationResult; + clearAllTabStops(input: ParagraphsClearAllTabStopsInput, options?: MutationOptions): ParagraphMutationResult; + setBorder(input: ParagraphsSetBorderInput, options?: MutationOptions): ParagraphMutationResult; + clearBorder(input: ParagraphsClearBorderInput, options?: MutationOptions): ParagraphMutationResult; + setShading(input: ParagraphsSetShadingInput, options?: MutationOptions): ParagraphMutationResult; + clearShading(input: ParagraphsClearShadingInput, options?: MutationOptions): ParagraphMutationResult; +} + +/** Public API surface for `styles.paragraph.*` — paragraph style reference operations. */ +export interface ParagraphStylesApi { + setStyle(input: ParagraphsSetStyleInput, options?: MutationOptions): ParagraphMutationResult; + clearStyle(input: ParagraphsClearStyleInput, options?: MutationOptions): ParagraphMutationResult; +} + +// --------------------------------------------------------------------------- +// Shared validation +// --------------------------------------------------------------------------- + +const PARAGRAPH_BLOCK_TYPES = new Set(['paragraph', 'heading', 'listItem']); + +function assertParagraphTarget(input: unknown, operation: string): asserts input is { target: ParagraphTarget } { + if (!isRecord(input)) { + throw new DocumentApiValidationError('INVALID_INPUT', `${operation} input must be a non-null object.`); + } + const { target } = input; + if (target === undefined || target === null) { + throw new DocumentApiValidationError('INVALID_TARGET', `${operation} requires a target.`); + } + if (!isRecord(target)) { + throw new DocumentApiValidationError('INVALID_TARGET', `${operation} target must be an object.`, { + field: 'target', + }); + } + if (target.kind !== 'block') { + throw new DocumentApiValidationError('INVALID_TARGET', `${operation} target.kind must be 'block'.`, { + field: 'target.kind', + value: target.kind, + }); + } + if (typeof target.nodeType !== 'string' || !PARAGRAPH_BLOCK_TYPES.has(target.nodeType)) { + throw new DocumentApiValidationError( + 'INVALID_TARGET', + `${operation} target.nodeType must be 'paragraph', 'heading', or 'listItem'.`, + { field: 'target.nodeType', value: target.nodeType }, + ); + } + if (typeof target.nodeId !== 'string' || target.nodeId.length === 0) { + throw new DocumentApiValidationError('INVALID_TARGET', `${operation} target.nodeId must be a non-empty string.`, { + field: 'target.nodeId', + value: target.nodeId, + }); + } +} + +function assertStrictBoolean(value: unknown, fieldName: string, operation: string): asserts value is boolean { + if (value !== true && value !== false) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operation} ${fieldName} must be true or false, got ${JSON.stringify(value)}.`, + { field: fieldName, value }, + ); + } +} + +function assertNonNegativeInteger(value: unknown, fieldName: string, operation: string): void { + if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operation} ${fieldName} must be a non-negative integer, got ${JSON.stringify(value)}.`, + { field: fieldName, value }, + ); + } +} + +function assertPositiveInteger(value: unknown, fieldName: string, operation: string): void { + if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operation} ${fieldName} must be a positive integer, got ${JSON.stringify(value)}.`, + { field: fieldName, value }, + ); + } +} + +function assertOneOf( + value: unknown, + fieldName: string, + allowed: readonly T[], + operation: string, +): asserts value is T { + if (typeof value !== 'string' || !(allowed as readonly string[]).includes(value)) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operation} ${fieldName} must be one of: ${allowed.join(', ')}. Got ${JSON.stringify(value)}.`, + { field: fieldName, value }, + ); + } +} + +function assertNonEmptyString(value: unknown, fieldName: string, operation: string): asserts value is string { + if (typeof value !== 'string' || value.length === 0) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operation} ${fieldName} must be a non-empty string, got ${JSON.stringify(value)}.`, + { field: fieldName, value }, + ); + } +} + +/** Rejects if a patch input has zero patchable fields beyond `target`. */ +function assertNotEmptyPatch(input: Record, patchKeys: readonly string[], operation: string): void { + const hasPatchField = patchKeys.some((key) => input[key] !== undefined); + if (!hasPatchField) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operation} requires at least one of: ${patchKeys.join(', ')}.`, + ); + } +} + +// --------------------------------------------------------------------------- +// Per-operation allowed keys +// --------------------------------------------------------------------------- + +const SET_STYLE_KEYS = new Set(['target', 'styleId']); +const CLEAR_STYLE_KEYS = new Set(['target']); +const RESET_DIRECT_FORMATTING_KEYS = new Set(['target']); +const SET_ALIGNMENT_KEYS = new Set(['target', 'alignment']); +const CLEAR_ALIGNMENT_KEYS = new Set(['target']); +const SET_INDENTATION_KEYS = new Set(['target', 'left', 'right', 'firstLine', 'hanging']); +const CLEAR_INDENTATION_KEYS = new Set(['target']); +const SET_SPACING_KEYS = new Set(['target', 'before', 'after', 'line', 'lineRule']); +const CLEAR_SPACING_KEYS = new Set(['target']); +const SET_KEEP_OPTIONS_KEYS = new Set(['target', 'keepNext', 'keepLines', 'widowControl']); +const SET_OUTLINE_LEVEL_KEYS = new Set(['target', 'outlineLevel']); +const SET_FLOW_OPTIONS_KEYS = new Set(['target', 'contextualSpacing', 'pageBreakBefore', 'suppressAutoHyphens']); +const SET_TAB_STOP_KEYS = new Set(['target', 'position', 'alignment', 'leader']); +const CLEAR_TAB_STOP_KEYS = new Set(['target', 'position']); +const CLEAR_ALL_TAB_STOPS_KEYS = new Set(['target']); +const SET_BORDER_KEYS = new Set(['target', 'side', 'style', 'color', 'size', 'space']); +const CLEAR_BORDER_KEYS = new Set(['target', 'side']); +const SET_SHADING_KEYS = new Set(['target', 'fill', 'color', 'pattern']); +const CLEAR_SHADING_KEYS = new Set(['target']); + +// --------------------------------------------------------------------------- +// Per-operation validators +// --------------------------------------------------------------------------- + +function validateSetStyle(input: unknown): asserts input is ParagraphsSetStyleInput { + assertParagraphTarget(input, 'styles.paragraph.setStyle'); + assertNoUnknownFields(input as Record, SET_STYLE_KEYS, 'styles.paragraph.setStyle'); + assertNonEmptyString((input as Record).styleId, 'styleId', 'styles.paragraph.setStyle'); +} + +function validateClearStyle(input: unknown): asserts input is ParagraphsClearStyleInput { + assertParagraphTarget(input, 'styles.paragraph.clearStyle'); + assertNoUnknownFields(input as Record, CLEAR_STYLE_KEYS, 'styles.paragraph.clearStyle'); +} + +function validateResetDirectFormatting(input: unknown): asserts input is ParagraphsResetDirectFormattingInput { + assertParagraphTarget(input, 'format.paragraph.resetDirectFormatting'); + assertNoUnknownFields( + input as Record, + RESET_DIRECT_FORMATTING_KEYS, + 'format.paragraph.resetDirectFormatting', + ); +} + +function validateSetAlignment(input: unknown): asserts input is ParagraphsSetAlignmentInput { + assertParagraphTarget(input, 'format.paragraph.setAlignment'); + assertNoUnknownFields(input as Record, SET_ALIGNMENT_KEYS, 'format.paragraph.setAlignment'); + const rec = input as Record; + if (rec.alignment === undefined) { + throw new DocumentApiValidationError('INVALID_INPUT', 'format.paragraph.setAlignment requires an alignment field.'); + } + assertOneOf(rec.alignment, 'alignment', PARAGRAPH_ALIGNMENTS, 'format.paragraph.setAlignment'); +} + +function validateClearAlignment(input: unknown): asserts input is ParagraphsClearAlignmentInput { + assertParagraphTarget(input, 'format.paragraph.clearAlignment'); + assertNoUnknownFields(input as Record, CLEAR_ALIGNMENT_KEYS, 'format.paragraph.clearAlignment'); +} + +function validateSetIndentation(input: unknown): asserts input is ParagraphsSetIndentationInput { + const op = 'format.paragraph.setIndentation'; + assertParagraphTarget(input, op); + assertNoUnknownFields(input as Record, SET_INDENTATION_KEYS, op); + const rec = input as Record; + assertNotEmptyPatch(rec, ['left', 'right', 'firstLine', 'hanging'], op); + + if (rec.left !== undefined) assertNonNegativeInteger(rec.left, 'left', op); + if (rec.right !== undefined) assertNonNegativeInteger(rec.right, 'right', op); + if (rec.firstLine !== undefined) assertNonNegativeInteger(rec.firstLine, 'firstLine', op); + if (rec.hanging !== undefined) assertNonNegativeInteger(rec.hanging, 'hanging', op); + + if (rec.firstLine !== undefined && rec.hanging !== undefined) { + throw new DocumentApiValidationError('INVALID_INPUT', `${op}: firstLine and hanging are mutually exclusive.`, { + field: 'firstLine,hanging', + }); + } +} + +function validateClearIndentation(input: unknown): asserts input is ParagraphsClearIndentationInput { + assertParagraphTarget(input, 'format.paragraph.clearIndentation'); + assertNoUnknownFields(input as Record, CLEAR_INDENTATION_KEYS, 'format.paragraph.clearIndentation'); +} + +function validateSetSpacing(input: unknown): asserts input is ParagraphsSetSpacingInput { + const op = 'format.paragraph.setSpacing'; + assertParagraphTarget(input, op); + assertNoUnknownFields(input as Record, SET_SPACING_KEYS, op); + const rec = input as Record; + assertNotEmptyPatch(rec, ['before', 'after', 'line', 'lineRule'], op); + + if (rec.before !== undefined) assertNonNegativeInteger(rec.before, 'before', op); + if (rec.after !== undefined) assertNonNegativeInteger(rec.after, 'after', op); + if (rec.line !== undefined) { + assertPositiveInteger(rec.line, 'line', op); + if (rec.lineRule === undefined) { + throw new DocumentApiValidationError('INVALID_INPUT', `${op}: lineRule is required when line is provided.`); + } + } + if (rec.lineRule !== undefined) { + assertOneOf(rec.lineRule, 'lineRule', LINE_RULES, op); + } +} + +function validateClearSpacing(input: unknown): asserts input is ParagraphsClearSpacingInput { + assertParagraphTarget(input, 'format.paragraph.clearSpacing'); + assertNoUnknownFields(input as Record, CLEAR_SPACING_KEYS, 'format.paragraph.clearSpacing'); +} + +function validateSetKeepOptions(input: unknown): asserts input is ParagraphsSetKeepOptionsInput { + const op = 'format.paragraph.setKeepOptions'; + assertParagraphTarget(input, op); + assertNoUnknownFields(input as Record, SET_KEEP_OPTIONS_KEYS, op); + const rec = input as Record; + assertNotEmptyPatch(rec, ['keepNext', 'keepLines', 'widowControl'], op); + + if (rec.keepNext !== undefined) assertStrictBoolean(rec.keepNext, 'keepNext', op); + if (rec.keepLines !== undefined) assertStrictBoolean(rec.keepLines, 'keepLines', op); + if (rec.widowControl !== undefined) assertStrictBoolean(rec.widowControl, 'widowControl', op); +} + +function validateSetOutlineLevel(input: unknown): asserts input is ParagraphsSetOutlineLevelInput { + const op = 'format.paragraph.setOutlineLevel'; + assertParagraphTarget(input, op); + assertNoUnknownFields(input as Record, SET_OUTLINE_LEVEL_KEYS, op); + const rec = input as Record; + if (rec.outlineLevel === undefined) { + throw new DocumentApiValidationError('INVALID_INPUT', `${op} requires an outlineLevel field.`); + } + if (rec.outlineLevel !== null) { + if ( + typeof rec.outlineLevel !== 'number' || + !Number.isInteger(rec.outlineLevel) || + rec.outlineLevel < 0 || + rec.outlineLevel > 9 + ) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${op} outlineLevel must be an integer 0–9, or null. Got ${JSON.stringify(rec.outlineLevel)}.`, + { field: 'outlineLevel', value: rec.outlineLevel }, + ); + } + } +} + +function validateSetFlowOptions(input: unknown): asserts input is ParagraphsSetFlowOptionsInput { + const op = 'format.paragraph.setFlowOptions'; + assertParagraphTarget(input, op); + assertNoUnknownFields(input as Record, SET_FLOW_OPTIONS_KEYS, op); + const rec = input as Record; + assertNotEmptyPatch(rec, ['contextualSpacing', 'pageBreakBefore', 'suppressAutoHyphens'], op); + + if (rec.contextualSpacing !== undefined) assertStrictBoolean(rec.contextualSpacing, 'contextualSpacing', op); + if (rec.pageBreakBefore !== undefined) assertStrictBoolean(rec.pageBreakBefore, 'pageBreakBefore', op); + if (rec.suppressAutoHyphens !== undefined) assertStrictBoolean(rec.suppressAutoHyphens, 'suppressAutoHyphens', op); +} + +function validateSetTabStop(input: unknown): asserts input is ParagraphsSetTabStopInput { + const op = 'format.paragraph.setTabStop'; + assertParagraphTarget(input, op); + assertNoUnknownFields(input as Record, SET_TAB_STOP_KEYS, op); + const rec = input as Record; + + if (rec.position === undefined) { + throw new DocumentApiValidationError('INVALID_INPUT', `${op} requires a position field.`); + } + assertNonNegativeInteger(rec.position, 'position', op); + + if (rec.alignment === undefined) { + throw new DocumentApiValidationError('INVALID_INPUT', `${op} requires an alignment field.`); + } + assertOneOf(rec.alignment, 'alignment', TAB_STOP_ALIGNMENTS, op); + + if (rec.leader !== undefined) { + assertOneOf(rec.leader, 'leader', TAB_STOP_LEADERS, op); + } +} + +function validateClearTabStop(input: unknown): asserts input is ParagraphsClearTabStopInput { + const op = 'format.paragraph.clearTabStop'; + assertParagraphTarget(input, op); + assertNoUnknownFields(input as Record, CLEAR_TAB_STOP_KEYS, op); + const rec = input as Record; + if (rec.position === undefined) { + throw new DocumentApiValidationError('INVALID_INPUT', `${op} requires a position field.`); + } + assertNonNegativeInteger(rec.position, 'position', op); +} + +function validateClearAllTabStops(input: unknown): asserts input is ParagraphsClearAllTabStopsInput { + assertParagraphTarget(input, 'format.paragraph.clearAllTabStops'); + assertNoUnknownFields( + input as Record, + CLEAR_ALL_TAB_STOPS_KEYS, + 'format.paragraph.clearAllTabStops', + ); +} + +function validateSetBorder(input: unknown): asserts input is ParagraphsSetBorderInput { + const op = 'format.paragraph.setBorder'; + assertParagraphTarget(input, op); + assertNoUnknownFields(input as Record, SET_BORDER_KEYS, op); + const rec = input as Record; + + if (rec.side === undefined) { + throw new DocumentApiValidationError('INVALID_INPUT', `${op} requires a side field.`); + } + assertOneOf(rec.side, 'side', BORDER_SIDES, op); + + if (rec.style === undefined) { + throw new DocumentApiValidationError('INVALID_INPUT', `${op} requires a style field.`); + } + assertNonEmptyString(rec.style, 'style', op); + + if (rec.color !== undefined) assertNonEmptyString(rec.color, 'color', op); + if (rec.size !== undefined) assertNonNegativeInteger(rec.size, 'size', op); + if (rec.space !== undefined) assertNonNegativeInteger(rec.space, 'space', op); +} + +function validateClearBorder(input: unknown): asserts input is ParagraphsClearBorderInput { + const op = 'format.paragraph.clearBorder'; + assertParagraphTarget(input, op); + assertNoUnknownFields(input as Record, CLEAR_BORDER_KEYS, op); + const rec = input as Record; + if (rec.side === undefined) { + throw new DocumentApiValidationError('INVALID_INPUT', `${op} requires a side field.`); + } + assertOneOf(rec.side, 'side', CLEAR_BORDER_SIDES, op); +} + +function validateSetShading(input: unknown): asserts input is ParagraphsSetShadingInput { + const op = 'format.paragraph.setShading'; + assertParagraphTarget(input, op); + assertNoUnknownFields(input as Record, SET_SHADING_KEYS, op); + const rec = input as Record; + assertNotEmptyPatch(rec, ['fill', 'color', 'pattern'], op); + + if (rec.fill !== undefined) assertNonEmptyString(rec.fill, 'fill', op); + if (rec.color !== undefined) assertNonEmptyString(rec.color, 'color', op); + if (rec.pattern !== undefined) assertNonEmptyString(rec.pattern, 'pattern', op); +} + +function validateClearShading(input: unknown): asserts input is ParagraphsClearShadingInput { + assertParagraphTarget(input, 'format.paragraph.clearShading'); + assertNoUnknownFields(input as Record, CLEAR_SHADING_KEYS, 'format.paragraph.clearShading'); +} + +// --------------------------------------------------------------------------- +// Execute functions — validate then delegate +// --------------------------------------------------------------------------- + +export function executeParagraphsSetStyle( + adapter: ParagraphsAdapter, + input: ParagraphsSetStyleInput, + options?: MutationOptions, +): ParagraphMutationResult { + validateSetStyle(input); + return adapter.setStyle(input, normalizeMutationOptions(options)); +} + +export function executeParagraphsClearStyle( + adapter: ParagraphsAdapter, + input: ParagraphsClearStyleInput, + options?: MutationOptions, +): ParagraphMutationResult { + validateClearStyle(input); + return adapter.clearStyle(input, normalizeMutationOptions(options)); +} + +export function executeParagraphsResetDirectFormatting( + adapter: ParagraphsAdapter, + input: ParagraphsResetDirectFormattingInput, + options?: MutationOptions, +): ParagraphMutationResult { + validateResetDirectFormatting(input); + return adapter.resetDirectFormatting(input, normalizeMutationOptions(options)); +} + +export function executeParagraphsSetAlignment( + adapter: ParagraphsAdapter, + input: ParagraphsSetAlignmentInput, + options?: MutationOptions, +): ParagraphMutationResult { + validateSetAlignment(input); + return adapter.setAlignment(input, normalizeMutationOptions(options)); +} + +export function executeParagraphsClearAlignment( + adapter: ParagraphsAdapter, + input: ParagraphsClearAlignmentInput, + options?: MutationOptions, +): ParagraphMutationResult { + validateClearAlignment(input); + return adapter.clearAlignment(input, normalizeMutationOptions(options)); +} + +export function executeParagraphsSetIndentation( + adapter: ParagraphsAdapter, + input: ParagraphsSetIndentationInput, + options?: MutationOptions, +): ParagraphMutationResult { + validateSetIndentation(input); + return adapter.setIndentation(input, normalizeMutationOptions(options)); +} + +export function executeParagraphsClearIndentation( + adapter: ParagraphsAdapter, + input: ParagraphsClearIndentationInput, + options?: MutationOptions, +): ParagraphMutationResult { + validateClearIndentation(input); + return adapter.clearIndentation(input, normalizeMutationOptions(options)); +} + +export function executeParagraphsSetSpacing( + adapter: ParagraphsAdapter, + input: ParagraphsSetSpacingInput, + options?: MutationOptions, +): ParagraphMutationResult { + validateSetSpacing(input); + return adapter.setSpacing(input, normalizeMutationOptions(options)); +} + +export function executeParagraphsClearSpacing( + adapter: ParagraphsAdapter, + input: ParagraphsClearSpacingInput, + options?: MutationOptions, +): ParagraphMutationResult { + validateClearSpacing(input); + return adapter.clearSpacing(input, normalizeMutationOptions(options)); +} + +export function executeParagraphsSetKeepOptions( + adapter: ParagraphsAdapter, + input: ParagraphsSetKeepOptionsInput, + options?: MutationOptions, +): ParagraphMutationResult { + validateSetKeepOptions(input); + return adapter.setKeepOptions(input, normalizeMutationOptions(options)); +} + +export function executeParagraphsSetOutlineLevel( + adapter: ParagraphsAdapter, + input: ParagraphsSetOutlineLevelInput, + options?: MutationOptions, +): ParagraphMutationResult { + validateSetOutlineLevel(input); + return adapter.setOutlineLevel(input, normalizeMutationOptions(options)); +} + +export function executeParagraphsSetFlowOptions( + adapter: ParagraphsAdapter, + input: ParagraphsSetFlowOptionsInput, + options?: MutationOptions, +): ParagraphMutationResult { + validateSetFlowOptions(input); + return adapter.setFlowOptions(input, normalizeMutationOptions(options)); +} + +export function executeParagraphsSetTabStop( + adapter: ParagraphsAdapter, + input: ParagraphsSetTabStopInput, + options?: MutationOptions, +): ParagraphMutationResult { + validateSetTabStop(input); + return adapter.setTabStop(input, normalizeMutationOptions(options)); +} + +export function executeParagraphsClearTabStop( + adapter: ParagraphsAdapter, + input: ParagraphsClearTabStopInput, + options?: MutationOptions, +): ParagraphMutationResult { + validateClearTabStop(input); + return adapter.clearTabStop(input, normalizeMutationOptions(options)); +} + +export function executeParagraphsClearAllTabStops( + adapter: ParagraphsAdapter, + input: ParagraphsClearAllTabStopsInput, + options?: MutationOptions, +): ParagraphMutationResult { + validateClearAllTabStops(input); + return adapter.clearAllTabStops(input, normalizeMutationOptions(options)); +} + +export function executeParagraphsSetBorder( + adapter: ParagraphsAdapter, + input: ParagraphsSetBorderInput, + options?: MutationOptions, +): ParagraphMutationResult { + validateSetBorder(input); + return adapter.setBorder(input, normalizeMutationOptions(options)); +} + +export function executeParagraphsClearBorder( + adapter: ParagraphsAdapter, + input: ParagraphsClearBorderInput, + options?: MutationOptions, +): ParagraphMutationResult { + validateClearBorder(input); + return adapter.clearBorder(input, normalizeMutationOptions(options)); +} + +export function executeParagraphsSetShading( + adapter: ParagraphsAdapter, + input: ParagraphsSetShadingInput, + options?: MutationOptions, +): ParagraphMutationResult { + validateSetShading(input); + return adapter.setShading(input, normalizeMutationOptions(options)); +} + +export function executeParagraphsClearShading( + adapter: ParagraphsAdapter, + input: ParagraphsClearShadingInput, + options?: MutationOptions, +): ParagraphMutationResult { + validateClearShading(input); + return adapter.clearShading(input, normalizeMutationOptions(options)); +} diff --git a/packages/document-api/src/paragraphs/paragraphs.types.ts b/packages/document-api/src/paragraphs/paragraphs.types.ts new file mode 100644 index 0000000000..906e043877 --- /dev/null +++ b/packages/document-api/src/paragraphs/paragraphs.types.ts @@ -0,0 +1,190 @@ +/** + * Types for the `format.paragraph.*` / `styles.paragraph.*` operation namespaces. + * + * External API uses user-centric naming (alignment, indentation, spacing). + * The adapter layer maps to OOXML-aligned internal keys (justification, indent, etc.). + */ + +import type { BlockNodeAddress } from '../types/base.js'; +import type { ReceiptFailure } from '../types/receipt.js'; + +// --------------------------------------------------------------------------- +// Target +// --------------------------------------------------------------------------- + +export type ParagraphBlockType = 'paragraph' | 'heading' | 'listItem'; + +export type ParagraphTarget = BlockNodeAddress & { nodeType: ParagraphBlockType }; + +// --------------------------------------------------------------------------- +// Result +// --------------------------------------------------------------------------- + +export interface MutationResolution { + target: ParagraphTarget; +} + +export interface ParagraphMutationSuccess { + success: true; + target: ParagraphTarget; + resolution: MutationResolution; +} + +export interface ParagraphMutationFailure { + success: false; + failure: ReceiptFailure; + resolution?: MutationResolution; +} + +export type ParagraphMutationResult = ParagraphMutationSuccess | ParagraphMutationFailure; + +// --------------------------------------------------------------------------- +// Enums +// --------------------------------------------------------------------------- + +export const PARAGRAPH_ALIGNMENTS = ['left', 'center', 'right', 'justify'] as const; +export type ParagraphAlignment = (typeof PARAGRAPH_ALIGNMENTS)[number]; + +export const TAB_STOP_ALIGNMENTS = ['left', 'center', 'right', 'decimal', 'bar'] as const; +export type TabStopAlignment = (typeof TAB_STOP_ALIGNMENTS)[number]; + +export const TAB_STOP_LEADERS = ['none', 'dot', 'hyphen', 'underscore', 'heavy', 'middleDot'] as const; +export type TabStopLeader = (typeof TAB_STOP_LEADERS)[number]; + +export const BORDER_SIDES = ['top', 'bottom', 'left', 'right', 'between', 'bar'] as const; +export type BorderSide = (typeof BORDER_SIDES)[number]; + +export const CLEAR_BORDER_SIDES = ['top', 'bottom', 'left', 'right', 'between', 'bar', 'all'] as const; +export type ClearBorderSide = (typeof CLEAR_BORDER_SIDES)[number]; + +export const LINE_RULES = ['auto', 'exact', 'atLeast'] as const; +export type LineRule = (typeof LINE_RULES)[number]; + +// --------------------------------------------------------------------------- +// Input types +// --------------------------------------------------------------------------- + +/** paragraphs.setStyle */ +export interface ParagraphsSetStyleInput { + target: ParagraphTarget; + styleId: string; +} + +/** paragraphs.clearStyle */ +export interface ParagraphsClearStyleInput { + target: ParagraphTarget; +} + +/** paragraphs.resetDirectFormatting */ +export interface ParagraphsResetDirectFormattingInput { + target: ParagraphTarget; +} + +/** paragraphs.setAlignment */ +export interface ParagraphsSetAlignmentInput { + target: ParagraphTarget; + alignment: ParagraphAlignment; +} + +/** paragraphs.clearAlignment */ +export interface ParagraphsClearAlignmentInput { + target: ParagraphTarget; +} + +/** paragraphs.setIndentation */ +export interface ParagraphsSetIndentationInput { + target: ParagraphTarget; + left?: number; + right?: number; + firstLine?: number; + hanging?: number; +} + +/** paragraphs.clearIndentation */ +export interface ParagraphsClearIndentationInput { + target: ParagraphTarget; +} + +/** paragraphs.setSpacing */ +export interface ParagraphsSetSpacingInput { + target: ParagraphTarget; + before?: number; + after?: number; + line?: number; + lineRule?: LineRule; +} + +/** paragraphs.clearSpacing */ +export interface ParagraphsClearSpacingInput { + target: ParagraphTarget; +} + +/** paragraphs.setKeepOptions */ +export interface ParagraphsSetKeepOptionsInput { + target: ParagraphTarget; + keepNext?: boolean; + keepLines?: boolean; + widowControl?: boolean; +} + +/** paragraphs.setOutlineLevel */ +export interface ParagraphsSetOutlineLevelInput { + target: ParagraphTarget; + outlineLevel: number | null; +} + +/** paragraphs.setFlowOptions */ +export interface ParagraphsSetFlowOptionsInput { + target: ParagraphTarget; + contextualSpacing?: boolean; + pageBreakBefore?: boolean; + suppressAutoHyphens?: boolean; +} + +/** paragraphs.setTabStop */ +export interface ParagraphsSetTabStopInput { + target: ParagraphTarget; + position: number; + alignment: TabStopAlignment; + leader?: TabStopLeader; +} + +/** paragraphs.clearTabStop */ +export interface ParagraphsClearTabStopInput { + target: ParagraphTarget; + position: number; +} + +/** paragraphs.clearAllTabStops */ +export interface ParagraphsClearAllTabStopsInput { + target: ParagraphTarget; +} + +/** paragraphs.setBorder */ +export interface ParagraphsSetBorderInput { + target: ParagraphTarget; + side: BorderSide; + style: string; + color?: string; + size?: number; + space?: number; +} + +/** paragraphs.clearBorder */ +export interface ParagraphsClearBorderInput { + target: ParagraphTarget; + side: ClearBorderSide; +} + +/** paragraphs.setShading */ +export interface ParagraphsSetShadingInput { + target: ParagraphTarget; + fill?: string; + color?: string; + pattern?: string; +} + +/** paragraphs.clearShading */ +export interface ParagraphsClearShadingInput { + target: ParagraphTarget; +} 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 29c8abcd9f..f42dc3a827 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 @@ -22,7 +22,6 @@ import { formatFontSizeWrapper, formatFontFamilyWrapper, formatColorWrapper, - formatAlignWrapper, } from '../plan-engine/format-value-wrappers.js'; import { stylesApplyAdapter } from '../styles-adapter.js'; import { createTableWrapper } from '../plan-engine/create-table-wrapper.js'; @@ -99,6 +98,27 @@ import { sectionsSetPageBordersAdapter, sectionsClearPageBordersAdapter, } from '../sections-adapter.js'; +import { + paragraphsSetStyleWrapper, + paragraphsClearStyleWrapper, + paragraphsResetDirectFormattingWrapper, + paragraphsSetAlignmentWrapper, + paragraphsClearAlignmentWrapper, + paragraphsSetIndentationWrapper, + paragraphsClearIndentationWrapper, + paragraphsSetSpacingWrapper, + paragraphsClearSpacingWrapper, + paragraphsSetKeepOptionsWrapper, + paragraphsSetOutlineLevelWrapper, + paragraphsSetFlowOptionsWrapper, + paragraphsSetTabStopWrapper, + paragraphsClearTabStopWrapper, + paragraphsClearAllTabStopsWrapper, + paragraphsSetBorderWrapper, + paragraphsClearBorderWrapper, + paragraphsSetShadingWrapper, + paragraphsClearShadingWrapper, +} from '../plan-engine/paragraphs-wrappers.js'; import { validateJsonSchema } from './schema-validator.js'; const mockedDeps = vi.hoisted(() => ({ @@ -429,6 +449,49 @@ function makeTextEditor( return { editor, dispatch, tr }; } +// --------------------------------------------------------------------------- +// Paragraph mutation test helpers +// --------------------------------------------------------------------------- + +const P_TARGET = { kind: 'block' as const, nodeType: 'paragraph' as const, nodeId: 'p1' }; +const P_TARGET_MISSING = { kind: 'block' as const, nodeType: 'paragraph' as const, nodeId: 'missing' }; + +/** Pre-populated properties so both "set" and "clear" applyCase tests produce a real change. */ +const MOCK_PARAGRAPH_PROPERTIES = { + styleId: 'Heading1', + justification: 'right', + indent: { left: 360 }, + spacing: { before: 60 }, + keepNext: false, + outlineLvl: 0, + pageBreakBefore: false, + tabStops: [{ tab: { pos: 720, tabType: 'center' } }], + borders: { bottom: { val: 'double' } }, + shading: { fill: '00FF00' }, +}; + +function makeParagraphEditor(overrides: { nodeAtReturnsNull?: boolean } = {}) { + const { editor, dispatch, tr } = makeTextEditor(); + // paragraphs-wrappers uses setNodeMarkup on the transaction + (tr as Record).setNodeMarkup = vi.fn().mockReturnValue(tr); + if (overrides.nodeAtReturnsNull) { + editor.state.doc.nodeAt = vi.fn(() => null) as unknown as typeof editor.state.doc.nodeAt; + } else { + // Wrap nodeAt so the handler sees pre-populated paragraphProperties while + // the original descendants() (used by buildBlockIndex) remains untouched. + const realNodeAt = editor.state.doc.nodeAt.bind(editor.state.doc); + const pProps = { ...MOCK_PARAGRAPH_PROPERTIES }; + (editor.state.doc as Record).nodeAt = (pos: number) => { + const node = realNodeAt(pos); + if (node && (node.type as { name: string }).name === 'paragraph') { + return { ...node, attrs: { ...node.attrs, paragraphProperties: pProps } }; + } + return node; + }; + } + return { editor, dispatch }; +} + function makeListParagraph(options: { id: string; text?: string; @@ -1416,32 +1479,6 @@ const mutationVectors: Partial> = { ); }, }, - 'format.align': { - throwCase: () => { - const { editor } = makeTextEditor(); - return formatAlignWrapper( - editor, - { target: { kind: 'text', blockId: 'missing', range: { start: 0, end: 1 } }, alignment: 'center' }, - { changeMode: 'direct' }, - ); - }, - failureCase: () => { - const { editor } = makeTextEditor('Hello', { commands: { setTextAlign: vi.fn(() => false) } }); - return formatAlignWrapper( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } }, alignment: 'center' }, - { changeMode: 'direct' }, - ); - }, - applyCase: () => { - const { editor } = makeTextEditor(); - return formatAlignWrapper( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } }, alignment: 'center' }, - { changeMode: 'direct' }, - ); - }, - }, 'create.paragraph': { throwCase: () => { const { editor } = makeTextEditor('Hello', { commands: { insertParagraphAt: undefined } }); @@ -2893,6 +2930,335 @@ const mutationVectors: Partial> = { ); }, }, + // --------------------------------------------------------------------------- + // Paragraph property mutations + // --------------------------------------------------------------------------- + 'styles.paragraph.setStyle': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetStyleWrapper( + editor, + { target: P_TARGET_MISSING, styleId: 'Normal' }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ nodeAtReturnsNull: true }); + return paragraphsSetStyleWrapper(editor, { target: P_TARGET, styleId: 'Normal' }, { changeMode: 'direct' }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetStyleWrapper(editor, { target: P_TARGET, styleId: 'Normal' }, { changeMode: 'direct' }); + }, + }, + 'styles.paragraph.clearStyle': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearStyleWrapper(editor, { target: P_TARGET_MISSING }, { changeMode: 'direct' }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ nodeAtReturnsNull: true }); + return paragraphsClearStyleWrapper(editor, { target: P_TARGET }, { changeMode: 'direct' }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearStyleWrapper(editor, { target: P_TARGET }, { changeMode: 'direct' }); + }, + }, + 'format.paragraph.resetDirectFormatting': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsResetDirectFormattingWrapper(editor, { target: P_TARGET_MISSING }, { changeMode: 'direct' }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ nodeAtReturnsNull: true }); + return paragraphsResetDirectFormattingWrapper(editor, { target: P_TARGET }, { changeMode: 'direct' }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsResetDirectFormattingWrapper(editor, { target: P_TARGET }, { changeMode: 'direct' }); + }, + }, + 'format.paragraph.setAlignment': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetAlignmentWrapper( + editor, + { target: P_TARGET_MISSING, alignment: 'center' }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ nodeAtReturnsNull: true }); + return paragraphsSetAlignmentWrapper(editor, { target: P_TARGET, alignment: 'center' }, { changeMode: 'direct' }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetAlignmentWrapper(editor, { target: P_TARGET, alignment: 'center' }, { changeMode: 'direct' }); + }, + }, + 'format.paragraph.clearAlignment': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearAlignmentWrapper(editor, { target: P_TARGET_MISSING }, { changeMode: 'direct' }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ nodeAtReturnsNull: true }); + return paragraphsClearAlignmentWrapper(editor, { target: P_TARGET }, { changeMode: 'direct' }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearAlignmentWrapper(editor, { target: P_TARGET }, { changeMode: 'direct' }); + }, + }, + 'format.paragraph.setIndentation': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetIndentationWrapper(editor, { target: P_TARGET_MISSING, left: 720 }, { changeMode: 'direct' }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ nodeAtReturnsNull: true }); + return paragraphsSetIndentationWrapper(editor, { target: P_TARGET, left: 720 }, { changeMode: 'direct' }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetIndentationWrapper(editor, { target: P_TARGET, left: 720 }, { changeMode: 'direct' }); + }, + }, + 'format.paragraph.clearIndentation': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearIndentationWrapper(editor, { target: P_TARGET_MISSING }, { changeMode: 'direct' }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ nodeAtReturnsNull: true }); + return paragraphsClearIndentationWrapper(editor, { target: P_TARGET }, { changeMode: 'direct' }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearIndentationWrapper(editor, { target: P_TARGET }, { changeMode: 'direct' }); + }, + }, + 'format.paragraph.setSpacing': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetSpacingWrapper(editor, { target: P_TARGET_MISSING, before: 120 }, { changeMode: 'direct' }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ nodeAtReturnsNull: true }); + return paragraphsSetSpacingWrapper(editor, { target: P_TARGET, before: 120 }, { changeMode: 'direct' }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetSpacingWrapper(editor, { target: P_TARGET, before: 120 }, { changeMode: 'direct' }); + }, + }, + 'format.paragraph.clearSpacing': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearSpacingWrapper(editor, { target: P_TARGET_MISSING }, { changeMode: 'direct' }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ nodeAtReturnsNull: true }); + return paragraphsClearSpacingWrapper(editor, { target: P_TARGET }, { changeMode: 'direct' }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearSpacingWrapper(editor, { target: P_TARGET }, { changeMode: 'direct' }); + }, + }, + 'format.paragraph.setKeepOptions': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetKeepOptionsWrapper( + editor, + { target: P_TARGET_MISSING, keepNext: true }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ nodeAtReturnsNull: true }); + return paragraphsSetKeepOptionsWrapper(editor, { target: P_TARGET, keepNext: true }, { changeMode: 'direct' }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetKeepOptionsWrapper(editor, { target: P_TARGET, keepNext: true }, { changeMode: 'direct' }); + }, + }, + 'format.paragraph.setOutlineLevel': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetOutlineLevelWrapper( + editor, + { target: P_TARGET_MISSING, outlineLevel: 1 }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ nodeAtReturnsNull: true }); + return paragraphsSetOutlineLevelWrapper(editor, { target: P_TARGET, outlineLevel: 1 }, { changeMode: 'direct' }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetOutlineLevelWrapper(editor, { target: P_TARGET, outlineLevel: 1 }, { changeMode: 'direct' }); + }, + }, + 'format.paragraph.setFlowOptions': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetFlowOptionsWrapper( + editor, + { target: P_TARGET_MISSING, pageBreakBefore: true }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ nodeAtReturnsNull: true }); + return paragraphsSetFlowOptionsWrapper( + editor, + { target: P_TARGET, pageBreakBefore: true }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetFlowOptionsWrapper( + editor, + { target: P_TARGET, pageBreakBefore: true }, + { changeMode: 'direct' }, + ); + }, + }, + 'format.paragraph.setTabStop': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetTabStopWrapper( + editor, + { target: P_TARGET_MISSING, position: 720, alignment: 'left' }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ nodeAtReturnsNull: true }); + return paragraphsSetTabStopWrapper( + editor, + { target: P_TARGET, position: 720, alignment: 'left' }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetTabStopWrapper( + editor, + { target: P_TARGET, position: 720, alignment: 'left' }, + { changeMode: 'direct' }, + ); + }, + }, + 'format.paragraph.clearTabStop': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearTabStopWrapper( + editor, + { target: P_TARGET_MISSING, position: 720 }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ nodeAtReturnsNull: true }); + return paragraphsClearTabStopWrapper(editor, { target: P_TARGET, position: 720 }, { changeMode: 'direct' }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearTabStopWrapper(editor, { target: P_TARGET, position: 720 }, { changeMode: 'direct' }); + }, + }, + 'format.paragraph.clearAllTabStops': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearAllTabStopsWrapper(editor, { target: P_TARGET_MISSING }, { changeMode: 'direct' }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ nodeAtReturnsNull: true }); + return paragraphsClearAllTabStopsWrapper(editor, { target: P_TARGET }, { changeMode: 'direct' }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearAllTabStopsWrapper(editor, { target: P_TARGET }, { changeMode: 'direct' }); + }, + }, + 'format.paragraph.setBorder': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetBorderWrapper( + editor, + { target: P_TARGET_MISSING, side: 'top', style: 'single' }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ nodeAtReturnsNull: true }); + return paragraphsSetBorderWrapper( + editor, + { target: P_TARGET, side: 'top', style: 'single' }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetBorderWrapper( + editor, + { target: P_TARGET, side: 'top', style: 'single' }, + { changeMode: 'direct' }, + ); + }, + }, + 'format.paragraph.clearBorder': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearBorderWrapper(editor, { target: P_TARGET_MISSING, side: 'all' }, { changeMode: 'direct' }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ nodeAtReturnsNull: true }); + return paragraphsClearBorderWrapper(editor, { target: P_TARGET, side: 'all' }, { changeMode: 'direct' }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearBorderWrapper(editor, { target: P_TARGET, side: 'all' }, { changeMode: 'direct' }); + }, + }, + 'format.paragraph.setShading': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetShadingWrapper( + editor, + { target: P_TARGET_MISSING, fill: 'FF0000' }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ nodeAtReturnsNull: true }); + return paragraphsSetShadingWrapper(editor, { target: P_TARGET, fill: 'FF0000' }, { changeMode: 'direct' }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetShadingWrapper(editor, { target: P_TARGET, fill: 'FF0000' }, { changeMode: 'direct' }); + }, + }, + 'format.paragraph.clearShading': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearShadingWrapper(editor, { target: P_TARGET_MISSING }, { changeMode: 'direct' }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ nodeAtReturnsNull: true }); + return paragraphsClearShadingWrapper(editor, { target: P_TARGET }, { changeMode: 'direct' }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearShadingWrapper(editor, { target: P_TARGET }, { changeMode: 'direct' }); + }, + }, }; const dryRunVectors: Partial unknown>> = { @@ -2981,16 +3347,6 @@ const dryRunVectors: Partial unknown>> = { expect(dispatch).not.toHaveBeenCalled(); return result; }, - 'format.align': () => { - const { editor, dispatch } = makeTextEditor(); - const result = formatAlignWrapper( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } }, alignment: 'center' }, - { changeMode: 'direct', dryRun: true }, - ); - expect(dispatch).not.toHaveBeenCalled(); - return result; - }, 'create.paragraph': () => { const insertParagraphAt = vi.fn(() => true); const { editor } = makeTextEditor('Hello', { commands: { insertParagraphAt } }); @@ -3679,6 +4035,185 @@ const dryRunVectors: Partial unknown>> = { expect(dispatch).not.toHaveBeenCalled(); return result; }, + // Paragraph property mutations + 'styles.paragraph.setStyle': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsSetStyleWrapper( + editor, + { target: P_TARGET, styleId: 'Normal' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'styles.paragraph.clearStyle': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsClearStyleWrapper(editor, { target: P_TARGET }, { changeMode: 'direct', dryRun: true }); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.resetDirectFormatting': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsResetDirectFormattingWrapper( + editor, + { target: P_TARGET }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.setAlignment': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsSetAlignmentWrapper( + editor, + { target: P_TARGET, alignment: 'center' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.clearAlignment': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsClearAlignmentWrapper( + editor, + { target: P_TARGET }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.setIndentation': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsSetIndentationWrapper( + editor, + { target: P_TARGET, left: 720 }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.clearIndentation': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsClearIndentationWrapper( + editor, + { target: P_TARGET }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.setSpacing': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsSetSpacingWrapper( + editor, + { target: P_TARGET, before: 120 }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.clearSpacing': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsClearSpacingWrapper(editor, { target: P_TARGET }, { changeMode: 'direct', dryRun: true }); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.setKeepOptions': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsSetKeepOptionsWrapper( + editor, + { target: P_TARGET, keepNext: true }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.setOutlineLevel': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsSetOutlineLevelWrapper( + editor, + { target: P_TARGET, outlineLevel: 1 }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.setFlowOptions': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsSetFlowOptionsWrapper( + editor, + { target: P_TARGET, pageBreakBefore: true }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.setTabStop': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsSetTabStopWrapper( + editor, + { target: P_TARGET, position: 720, alignment: 'left' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.clearTabStop': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsClearTabStopWrapper( + editor, + { target: P_TARGET, position: 720 }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.clearAllTabStops': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsClearAllTabStopsWrapper( + editor, + { target: P_TARGET }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.setBorder': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsSetBorderWrapper( + editor, + { target: P_TARGET, side: 'top', style: 'single' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.clearBorder': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsClearBorderWrapper( + editor, + { target: P_TARGET, side: 'all' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.setShading': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsSetShadingWrapper( + editor, + { target: P_TARGET, fill: 'FF0000' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.clearShading': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsClearShadingWrapper(editor, { target: P_TARGET }, { changeMode: 'direct', dryRun: true }); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, }; beforeEach(() => { 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 35fe131a75..72a4f25b61 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 @@ -27,7 +27,6 @@ describe('assembleDocumentApiAdapters', () => { expect(adapters).toHaveProperty('format.fontSize'); expect(adapters).toHaveProperty('format.fontFamily'); expect(adapters).toHaveProperty('format.color'); - expect(adapters).toHaveProperty('format.align'); expect(adapters).toHaveProperty('trackChanges.list'); expect(adapters).toHaveProperty('trackChanges.get'); expect(adapters).toHaveProperty('trackChanges.accept'); @@ -66,6 +65,25 @@ describe('assembleDocumentApiAdapters', () => { expect(adapters).toHaveProperty('tables.get'); expect(adapters).toHaveProperty('tables.getCells'); expect(adapters).toHaveProperty('tables.getProperties'); + expect(adapters).toHaveProperty('paragraphs.setStyle'); + expect(adapters).toHaveProperty('paragraphs.clearStyle'); + expect(adapters).toHaveProperty('paragraphs.resetDirectFormatting'); + expect(adapters).toHaveProperty('paragraphs.setAlignment'); + expect(adapters).toHaveProperty('paragraphs.clearAlignment'); + expect(adapters).toHaveProperty('paragraphs.setIndentation'); + expect(adapters).toHaveProperty('paragraphs.clearIndentation'); + expect(adapters).toHaveProperty('paragraphs.setSpacing'); + expect(adapters).toHaveProperty('paragraphs.clearSpacing'); + expect(adapters).toHaveProperty('paragraphs.setKeepOptions'); + expect(adapters).toHaveProperty('paragraphs.setOutlineLevel'); + expect(adapters).toHaveProperty('paragraphs.setFlowOptions'); + expect(adapters).toHaveProperty('paragraphs.setTabStop'); + expect(adapters).toHaveProperty('paragraphs.clearTabStop'); + expect(adapters).toHaveProperty('paragraphs.clearAllTabStops'); + expect(adapters).toHaveProperty('paragraphs.setBorder'); + expect(adapters).toHaveProperty('paragraphs.clearBorder'); + expect(adapters).toHaveProperty('paragraphs.setShading'); + expect(adapters).toHaveProperty('paragraphs.clearShading'); }); it('returns functions for all adapter methods', () => { @@ -77,7 +95,6 @@ describe('assembleDocumentApiAdapters', () => { expect(typeof adapters.format.fontSize).toBe('function'); expect(typeof adapters.format.fontFamily).toBe('function'); expect(typeof adapters.format.color).toBe('function'); - 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'); 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 1b7249b16c..41b63e7ec3 100644 --- a/packages/super-editor/src/document-api-adapters/assemble-adapters.ts +++ b/packages/super-editor/src/document-api-adapters/assemble-adapters.ts @@ -12,7 +12,6 @@ import { formatFontSizeWrapper, formatFontFamilyWrapper, formatColorWrapper, - formatAlignWrapper, } from './plan-engine/format-value-wrappers.js'; import { trackChangesListWrapper, @@ -100,6 +99,27 @@ import { tablesClearCellSpacingWrapper, } from './plan-engine/tables-wrappers.js'; import { tablesGetAdapter, tablesGetCellsAdapter, tablesGetPropertiesAdapter } from './tables-adapter.js'; +import { + paragraphsSetStyleWrapper, + paragraphsClearStyleWrapper, + paragraphsResetDirectFormattingWrapper, + paragraphsSetAlignmentWrapper, + paragraphsClearAlignmentWrapper, + paragraphsSetIndentationWrapper, + paragraphsClearIndentationWrapper, + paragraphsSetSpacingWrapper, + paragraphsClearSpacingWrapper, + paragraphsSetKeepOptionsWrapper, + paragraphsSetOutlineLevelWrapper, + paragraphsSetFlowOptionsWrapper, + paragraphsSetTabStopWrapper, + paragraphsClearTabStopWrapper, + paragraphsClearAllTabStopsWrapper, + paragraphsSetBorderWrapper, + paragraphsClearBorderWrapper, + paragraphsSetShadingWrapper, + paragraphsClearShadingWrapper, +} from './plan-engine/paragraphs-wrappers.js'; /** * Assembles all document-api adapters for the given editor instance. @@ -139,7 +159,6 @@ export function assembleDocumentApiAdapters(editor: Editor): DocumentApiAdapters fontSize: (input, options) => formatFontSizeWrapper(editor, input, options), fontFamily: (input, options) => formatFontFamilyWrapper(editor, input, options), color: (input, options) => formatColorWrapper(editor, input, options), - align: (input, options) => formatAlignWrapper(editor, input, options), }, styles: { apply: (input, options) => stylesApplyAdapter(editor, input, options), @@ -191,6 +210,29 @@ export function assembleDocumentApiAdapters(editor: Editor): DocumentApiAdapters setPageBorders: (input, options) => sectionsSetPageBordersAdapter(editor, input, options), clearPageBorders: (input, options) => sectionsClearPageBordersAdapter(editor, input, options), }, + paragraphs: { + setStyle: (input, options) => paragraphsSetStyleWrapper(editor, input, options), + clearStyle: (input, options) => paragraphsClearStyleWrapper(editor, input, options), + resetDirectFormatting: (input, options) => paragraphsResetDirectFormattingWrapper(editor, input, options), + setAlignment: (input, options) => paragraphsSetAlignmentWrapper(editor, input, options), + clearAlignment: (input, options) => paragraphsClearAlignmentWrapper(editor, input, options), + setIndentation: (input, options) => paragraphsSetIndentationWrapper(editor, input, options), + clearIndentation: (input, options) => paragraphsClearIndentationWrapper(editor, input, options), + setSpacing: (input, options) => paragraphsSetSpacingWrapper(editor, input, options), + clearSpacing: (input, options) => paragraphsClearSpacingWrapper(editor, input, options), + setKeepOptions: (input, options) => paragraphsSetKeepOptionsWrapper(editor, input, options), + setOutlineLevel: (input, options) => paragraphsSetOutlineLevelWrapper(editor, input, options), + setFlowOptions: (input, options) => paragraphsSetFlowOptionsWrapper(editor, input, options), + setTabStop: (input, options) => paragraphsSetTabStopWrapper(editor, input, options), + clearTabStop: (input, options) => paragraphsClearTabStopWrapper(editor, input, options), + clearAllTabStops: (input, options) => paragraphsClearAllTabStopsWrapper(editor, input, options), + setBorder: (input, options) => paragraphsSetBorderWrapper(editor, input, options), + clearBorder: (input, options) => paragraphsClearBorderWrapper(editor, input, options), + setShading: (input, options) => paragraphsSetShadingWrapper(editor, input, options), + clearShading: (input, options) => paragraphsClearShadingWrapper(editor, input, options), + }, + // Note: paragraphs adapter is flat — DocumentApiAdapters keeps it as `paragraphs: ParagraphsAdapter`. + // The document-api index.ts splits it into `format.paragraph.*` and `styles.paragraph.*` on the public surface. 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 10cfab9c36..121bbaf6fe 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 @@ -259,7 +259,7 @@ describe('getDocumentApiCapabilities', () => { }); // --------------------------------------------------------------------------- - // format.fontSize / fontFamily / color / align capability reporting + // format.fontSize / fontFamily / color capability reporting // --------------------------------------------------------------------------- describe('format value operations', () => { @@ -272,8 +272,6 @@ describe('getDocumentApiCapabilities', () => { unsetFontFamily: vi.fn(() => true), setColor: vi.fn(() => true), unsetColor: vi.fn(() => true), - setTextAlign: vi.fn(() => true), - unsetTextAlign: vi.fn(() => true), ...overrides.commands, } as unknown as Editor['commands'], schema: { @@ -293,20 +291,12 @@ describe('getDocumentApiCapabilities', () => { expect(capabilities.operations['format.color'].available).toBe(true); }); - it('reports format.align as available when set and unset commands are present', () => { - const capabilities = getDocumentApiCapabilities(makeFormatEditor()); - - expect(capabilities.operations['format.align'].available).toBe(true); - }); - it('reports inline format ops as unavailable when textStyle mark is missing', () => { const capabilities = getDocumentApiCapabilities(makeFormatEditor({ marks: { textStyle: undefined } })); expect(capabilities.operations['format.fontSize'].available).toBe(false); expect(capabilities.operations['format.fontFamily'].available).toBe(false); expect(capabilities.operations['format.color'].available).toBe(false); - // align is paragraph-level — it does not require the textStyle mark - expect(capabilities.operations['format.align'].available).toBe(true); }); it('reports format.fontSize as unavailable when unsetFontSize command is missing', () => { @@ -316,13 +306,6 @@ describe('getDocumentApiCapabilities', () => { expect(capabilities.operations['format.fontSize'].reasons).toContain('OPERATION_UNAVAILABLE'); }); - it('reports format.align as unavailable when unsetTextAlign command is missing', () => { - const capabilities = getDocumentApiCapabilities(makeFormatEditor({ commands: { unsetTextAlign: undefined } })); - - expect(capabilities.operations['format.align'].available).toBe(false); - expect(capabilities.operations['format.align'].reasons).toContain('COMMAND_UNAVAILABLE'); - }); - it('uses OPERATION_UNAVAILABLE without COMMAND_UNAVAILABLE for inline format ops missing textStyle mark', () => { const capabilities = getDocumentApiCapabilities(makeFormatEditor({ marks: { textStyle: undefined } })); @@ -337,7 +320,6 @@ describe('getDocumentApiCapabilities', () => { expect(capabilities.operations['format.fontSize'].dryRun).toBe(true); expect(capabilities.operations['format.fontFamily'].dryRun).toBe(true); expect(capabilities.operations['format.color'].dryRun).toBe(true); - expect(capabilities.operations['format.align'].dryRun).toBe(true); }); it('reports all format value ops as direct-only (tracked = false)', () => { @@ -346,7 +328,6 @@ describe('getDocumentApiCapabilities', () => { expect(capabilities.operations['format.fontSize'].tracked).toBe(false); expect(capabilities.operations['format.fontFamily'].tracked).toBe(false); expect(capabilities.operations['format.color'].tracked).toBe(false); - expect(capabilities.operations['format.align'].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 a279933b26..9758c283e4 100644 --- a/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts +++ b/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts @@ -32,7 +32,6 @@ const REQUIRED_COMMANDS: Partial['justification'], ): TableNodeInfo['properties']['alignment'] { switch (justification) { case 'start': @@ -59,12 +59,12 @@ function mapTableAlignment( function mapParagraphProperties(attrs: ParagraphAttrs | null | undefined): ParagraphProperties { const props = attrs?.paragraphProperties ?? undefined; - const indentation = props?.indentation + const indentation = props?.indent ? { - left: props.indentation.left, - right: props.indentation.right, - firstLine: props.indentation.firstLine, - hanging: props.indentation.hanging, + left: props.indent.left, + right: props.indent.right, + firstLine: props.indent.firstLine, + hanging: props.indent.hanging, } : undefined; @@ -92,7 +92,7 @@ function mapParagraphProperties(attrs: ParagraphAttrs | null | undefined): Parag indentation, spacing, keepWithNext: props?.keepNext ?? undefined, - outlineLevel: props?.outlineLevel ?? undefined, + outlineLevel: props?.outlineLvl ?? undefined, paragraphNumbering, }; } diff --git a/packages/super-editor/src/document-api-adapters/plan-engine/format-value-wrappers.ts b/packages/super-editor/src/document-api-adapters/plan-engine/format-value-wrappers.ts index 7ddfe93961..2f78080191 100644 --- a/packages/super-editor/src/document-api-adapters/plan-engine/format-value-wrappers.ts +++ b/packages/super-editor/src/document-api-adapters/plan-engine/format-value-wrappers.ts @@ -12,7 +12,6 @@ import type { FormatFontSizeInput, FormatFontFamilyInput, FormatColorInput, - FormatAlignInput, MutationOptions, TextAddress, TextMutationReceipt, @@ -166,59 +165,3 @@ export function formatColorWrapper( unsetCommand: 'unsetColor', }); } - -// --------------------------------------------------------------------------- -// format.align (paragraph-level — different execution path) -// --------------------------------------------------------------------------- - -export function formatAlignWrapper( - editor: Editor, - input: FormatAlignInput, - options?: MutationOptions, -): TextMutationReceipt { - const operation = 'format.align'; - rejectTrackedMode(operation, options); - - const resolved = resolveFormatTarget(editor, input.target, operation); - // Align allows collapsed targets — a cursor identifies the containing paragraph. - - const setTextSelection = requireEditorCommand( - editor.commands?.setTextSelection as ((range: { from: number; to: number }) => boolean) | undefined, - `${operation} (setTextSelection)`, - ); - - if (input.alignment !== null) { - requireEditorCommand( - editor.commands?.setTextAlign as ((alignment: string) => boolean) | undefined, - `${operation} (setTextAlign)`, - ); - } else { - requireEditorCommand( - editor.commands?.unsetTextAlign as (() => boolean) | undefined, - `${operation} (unsetTextAlign)`, - ); - } - - if (options?.dryRun) { - return { success: true, resolution: resolved.resolution }; - } - - const receipt = executeDomainCommand( - editor, - () => { - setTextSelection({ from: resolved.from, to: resolved.to }); - - if (input.alignment !== null) { - return (editor.commands as Record boolean>).setTextAlign(input.alignment); - } - return (editor.commands as Record boolean>).unsetTextAlign(); - }, - { expectedRevision: options?.expectedRevision }, - ); - - if (receipt.steps[0]?.effect !== 'changed') { - return noOpFailure(resolved.resolution, operation); - } - - return { success: true, resolution: resolved.resolution }; -} diff --git a/packages/super-editor/src/document-api-adapters/plan-engine/paragraphs-wrappers.ts b/packages/super-editor/src/document-api-adapters/plan-engine/paragraphs-wrappers.ts new file mode 100644 index 0000000000..3ebcd80e0c --- /dev/null +++ b/packages/super-editor/src/document-api-adapters/plan-engine/paragraphs-wrappers.ts @@ -0,0 +1,624 @@ +/** + * Paragraph property adapter wrappers — bridge `format.paragraph.*` and `styles.paragraph.*` operations + * to ProseMirror paragraph node attribute mutations via the plan engine. + * + * Each wrapper: + * 1. Rejects tracked mode (unsupported for paragraph properties) + * 2. Resolves the target block from the block index + * 3. Validates that the target is a paragraph-family node + * 4. Short-circuits for dryRun + * 5. Executes the mutation through `executeDomainCommand` + * 6. Returns a typed ParagraphMutationResult + */ + +import type { Editor } from '../../core/Editor.js'; +import type { + MutationOptions, + ParagraphMutationResult, + ParagraphTarget, + ParagraphsSetStyleInput, + ParagraphsClearStyleInput, + ParagraphsResetDirectFormattingInput, + ParagraphsSetAlignmentInput, + ParagraphsClearAlignmentInput, + ParagraphsSetIndentationInput, + ParagraphsClearIndentationInput, + ParagraphsSetSpacingInput, + ParagraphsClearSpacingInput, + ParagraphsSetKeepOptionsInput, + ParagraphsSetOutlineLevelInput, + ParagraphsSetFlowOptionsInput, + ParagraphsSetTabStopInput, + ParagraphsClearTabStopInput, + ParagraphsClearAllTabStopsInput, + ParagraphsSetBorderInput, + ParagraphsClearBorderInput, + ParagraphsSetShadingInput, + ParagraphsClearShadingInput, + ParagraphAlignment, +} from '@superdoc/document-api'; +import { clearIndexCache, getBlockIndex } from '../helpers/index-cache.js'; +import { findBlockByIdStrict, type BlockCandidate } from '../helpers/node-address-resolver.js'; +import { DocumentApiAdapterError } from '../errors.js'; +import { rejectTrackedMode } from '../helpers/mutation-helpers.js'; +import { executeDomainCommand } from './plan-wrappers.js'; + +// --------------------------------------------------------------------------- +// Paragraph block types accepted by this adapter +// --------------------------------------------------------------------------- + +const PARAGRAPH_NODE_TYPES = new Set(['paragraph', 'heading', 'listItem']); + +// --------------------------------------------------------------------------- +// Target resolution +// --------------------------------------------------------------------------- + +function resolveParagraphBlock(editor: Editor, target: ParagraphTarget): BlockCandidate { + const index = getBlockIndex(editor); + const candidate = findBlockByIdStrict(index, target); + + if (!PARAGRAPH_NODE_TYPES.has(candidate.nodeType)) { + throw new DocumentApiAdapterError( + 'INVALID_TARGET', + `format.paragraph.* / styles.paragraph.* operations require a paragraph, heading, or listItem target. Got "${candidate.nodeType}".`, + { nodeType: candidate.nodeType }, + ); + } + + return candidate; +} + +// --------------------------------------------------------------------------- +// Result helpers +// --------------------------------------------------------------------------- + +function successResult(target: ParagraphTarget): ParagraphMutationResult { + return { success: true, target, resolution: { target } }; +} + +function noOpResult(operation: string): ParagraphMutationResult { + return { + success: false, + failure: { code: 'NO_OP', message: `${operation} produced no changes.` }, + }; +} + +// --------------------------------------------------------------------------- +// Core mutation helper — transforms paragraphProperties on a resolved block +// --------------------------------------------------------------------------- + +/** Loose runtime shape of paragraphProperties stored on ProseMirror nodes. */ +type PPr = Record; + +/** + * Resolves the target, applies a transform to the paragraph's `paragraphProperties`, + * and dispatches the resulting transaction through the plan engine. + */ +function mutateParagraphProperties( + editor: Editor, + candidate: BlockCandidate, + operation: string, + target: ParagraphTarget, + transform: (pPr: PPr) => PPr, + options?: MutationOptions, +): ParagraphMutationResult { + if (options?.dryRun) return successResult(target); + + const receipt = executeDomainCommand( + editor, + () => { + const node = editor.state.doc.nodeAt(candidate.pos); + if (!node) return false; + + const existing = (node.attrs as { paragraphProperties?: PPr }).paragraphProperties ?? {}; + const updated = transform({ ...existing }); + + if (JSON.stringify(existing) === JSON.stringify(updated)) return false; + + const tr = editor.state.tr; + tr.setNodeMarkup(candidate.pos, undefined, { ...node.attrs, paragraphProperties: updated }); + editor.dispatch(tr); + clearIndexCache(editor); + return true; + }, + { expectedRevision: options?.expectedRevision }, + ); + + if (receipt.steps[0]?.effect !== 'changed') { + return noOpResult(operation); + } + + return successResult(target); +} + +// --------------------------------------------------------------------------- +// Alignment mapping — external API → OOXML justification value +// --------------------------------------------------------------------------- + +const ALIGNMENT_TO_JUSTIFICATION: Record = { + left: 'left', + center: 'center', + right: 'right', + justify: 'both', +}; + +// --------------------------------------------------------------------------- +// Property helpers +// --------------------------------------------------------------------------- + +/** Merge only defined fields from a patch into an existing object. */ +function mergeDefinedFields(existing: PPr | undefined, patch: Record): PPr { + const result = { ...(existing ?? {}) }; + for (const [key, value] of Object.entries(patch)) { + if (value !== undefined) result[key] = value; + } + return result; +} + +/** Remove a key from an object, returning undefined if the object is now empty. */ +function deleteKey(obj: PPr, key: string): PPr | undefined { + const result = { ...obj }; + delete result[key]; + return Object.keys(result).length > 0 ? result : undefined; +} + +// --------------------------------------------------------------------------- +// Tab stop helpers +// --------------------------------------------------------------------------- + +interface TabStopEntry { + tab: { pos: number; tabType: string; leader?: string }; +} + +function addOrReplaceTabStop(existing: TabStopEntry[] | undefined, entry: TabStopEntry): TabStopEntry[] { + const filtered = (existing ?? []).filter((t) => t.tab.pos !== entry.tab.pos); + return [...filtered, entry].sort((a, b) => a.tab.pos - b.tab.pos); +} + +function removeTabStop(existing: TabStopEntry[] | undefined, position: number): TabStopEntry[] | undefined { + const filtered = (existing ?? []).filter((t) => t.tab.pos !== position); + return filtered.length > 0 ? filtered : undefined; +} + +// --------------------------------------------------------------------------- +// Wrapper functions +// --------------------------------------------------------------------------- + +export function paragraphsSetStyleWrapper( + editor: Editor, + input: ParagraphsSetStyleInput, + options?: MutationOptions, +): ParagraphMutationResult { + rejectTrackedMode('styles.paragraph.setStyle', options); + const candidate = resolveParagraphBlock(editor, input.target); + return mutateParagraphProperties( + editor, + candidate, + 'styles.paragraph.setStyle', + input.target, + (pPr) => ({ + ...pPr, + styleId: input.styleId, + }), + options, + ); +} + +export function paragraphsClearStyleWrapper( + editor: Editor, + input: ParagraphsClearStyleInput, + options?: MutationOptions, +): ParagraphMutationResult { + rejectTrackedMode('styles.paragraph.clearStyle', options); + const candidate = resolveParagraphBlock(editor, input.target); + return mutateParagraphProperties( + editor, + candidate, + 'styles.paragraph.clearStyle', + input.target, + (pPr) => { + const result = { ...pPr }; + delete result.styleId; + return result; + }, + options, + ); +} + +export function paragraphsResetDirectFormattingWrapper( + editor: Editor, + input: ParagraphsResetDirectFormattingInput, + options?: MutationOptions, +): ParagraphMutationResult { + rejectTrackedMode('format.paragraph.resetDirectFormatting', options); + const candidate = resolveParagraphBlock(editor, input.target); + return mutateParagraphProperties( + editor, + candidate, + 'format.paragraph.resetDirectFormatting', + input.target, + (pPr) => { + // Keep only structural references and section metadata; clear all direct formatting. + const result: PPr = {}; + if (pPr.styleId !== undefined) result.styleId = pPr.styleId; + if (pPr.numberingProperties !== undefined) result.numberingProperties = pPr.numberingProperties; + if (pPr.sectPr !== undefined) result.sectPr = pPr.sectPr; + return result; + }, + options, + ); +} + +export function paragraphsSetAlignmentWrapper( + editor: Editor, + input: ParagraphsSetAlignmentInput, + options?: MutationOptions, +): ParagraphMutationResult { + rejectTrackedMode('format.paragraph.setAlignment', options); + const candidate = resolveParagraphBlock(editor, input.target); + return mutateParagraphProperties( + editor, + candidate, + 'format.paragraph.setAlignment', + input.target, + (pPr) => ({ + ...pPr, + justification: ALIGNMENT_TO_JUSTIFICATION[input.alignment], + }), + options, + ); +} + +export function paragraphsClearAlignmentWrapper( + editor: Editor, + input: ParagraphsClearAlignmentInput, + options?: MutationOptions, +): ParagraphMutationResult { + rejectTrackedMode('format.paragraph.clearAlignment', options); + const candidate = resolveParagraphBlock(editor, input.target); + return mutateParagraphProperties( + editor, + candidate, + 'format.paragraph.clearAlignment', + input.target, + (pPr) => { + const result = { ...pPr }; + delete result.justification; + return result; + }, + options, + ); +} + +export function paragraphsSetIndentationWrapper( + editor: Editor, + input: ParagraphsSetIndentationInput, + options?: MutationOptions, +): ParagraphMutationResult { + rejectTrackedMode('format.paragraph.setIndentation', options); + const candidate = resolveParagraphBlock(editor, input.target); + return mutateParagraphProperties( + editor, + candidate, + 'format.paragraph.setIndentation', + input.target, + (pPr) => ({ + ...pPr, + indent: mergeDefinedFields(pPr.indent as PPr | undefined, { + left: input.left, + right: input.right, + firstLine: input.firstLine, + hanging: input.hanging, + }), + }), + options, + ); +} + +export function paragraphsClearIndentationWrapper( + editor: Editor, + input: ParagraphsClearIndentationInput, + options?: MutationOptions, +): ParagraphMutationResult { + rejectTrackedMode('format.paragraph.clearIndentation', options); + const candidate = resolveParagraphBlock(editor, input.target); + return mutateParagraphProperties( + editor, + candidate, + 'format.paragraph.clearIndentation', + input.target, + (pPr) => { + const result = { ...pPr }; + delete result.indent; + return result; + }, + options, + ); +} + +export function paragraphsSetSpacingWrapper( + editor: Editor, + input: ParagraphsSetSpacingInput, + options?: MutationOptions, +): ParagraphMutationResult { + rejectTrackedMode('format.paragraph.setSpacing', options); + const candidate = resolveParagraphBlock(editor, input.target); + return mutateParagraphProperties( + editor, + candidate, + 'format.paragraph.setSpacing', + input.target, + (pPr) => ({ + ...pPr, + spacing: mergeDefinedFields(pPr.spacing as PPr | undefined, { + before: input.before, + after: input.after, + line: input.line, + lineRule: input.lineRule, + }), + }), + options, + ); +} + +export function paragraphsClearSpacingWrapper( + editor: Editor, + input: ParagraphsClearSpacingInput, + options?: MutationOptions, +): ParagraphMutationResult { + rejectTrackedMode('format.paragraph.clearSpacing', options); + const candidate = resolveParagraphBlock(editor, input.target); + return mutateParagraphProperties( + editor, + candidate, + 'format.paragraph.clearSpacing', + input.target, + (pPr) => { + const result = { ...pPr }; + delete result.spacing; + return result; + }, + options, + ); +} + +export function paragraphsSetKeepOptionsWrapper( + editor: Editor, + input: ParagraphsSetKeepOptionsInput, + options?: MutationOptions, +): ParagraphMutationResult { + rejectTrackedMode('format.paragraph.setKeepOptions', options); + const candidate = resolveParagraphBlock(editor, input.target); + return mutateParagraphProperties( + editor, + candidate, + 'format.paragraph.setKeepOptions', + input.target, + (pPr) => { + const result = { ...pPr }; + if (input.keepNext !== undefined) result.keepNext = input.keepNext; + if (input.keepLines !== undefined) result.keepLines = input.keepLines; + if (input.widowControl !== undefined) result.widowControl = input.widowControl; + return result; + }, + options, + ); +} + +export function paragraphsSetOutlineLevelWrapper( + editor: Editor, + input: ParagraphsSetOutlineLevelInput, + options?: MutationOptions, +): ParagraphMutationResult { + rejectTrackedMode('format.paragraph.setOutlineLevel', options); + const candidate = resolveParagraphBlock(editor, input.target); + return mutateParagraphProperties( + editor, + candidate, + 'format.paragraph.setOutlineLevel', + input.target, + (pPr) => { + if (input.outlineLevel === null) { + const result = { ...pPr }; + delete result.outlineLvl; + return result; + } + return { ...pPr, outlineLvl: input.outlineLevel }; + }, + options, + ); +} + +export function paragraphsSetFlowOptionsWrapper( + editor: Editor, + input: ParagraphsSetFlowOptionsInput, + options?: MutationOptions, +): ParagraphMutationResult { + rejectTrackedMode('format.paragraph.setFlowOptions', options); + const candidate = resolveParagraphBlock(editor, input.target); + return mutateParagraphProperties( + editor, + candidate, + 'format.paragraph.setFlowOptions', + input.target, + (pPr) => { + const result = { ...pPr }; + if (input.contextualSpacing !== undefined) result.contextualSpacing = input.contextualSpacing; + if (input.pageBreakBefore !== undefined) result.pageBreakBefore = input.pageBreakBefore; + if (input.suppressAutoHyphens !== undefined) result.suppressAutoHyphens = input.suppressAutoHyphens; + return result; + }, + options, + ); +} + +export function paragraphsSetTabStopWrapper( + editor: Editor, + input: ParagraphsSetTabStopInput, + options?: MutationOptions, +): ParagraphMutationResult { + rejectTrackedMode('format.paragraph.setTabStop', options); + const candidate = resolveParagraphBlock(editor, input.target); + return mutateParagraphProperties( + editor, + candidate, + 'format.paragraph.setTabStop', + input.target, + (pPr) => { + const entry: TabStopEntry = { + tab: { + pos: input.position, + tabType: input.alignment, + ...(input.leader !== undefined && { leader: input.leader }), + }, + }; + return { ...pPr, tabStops: addOrReplaceTabStop(pPr.tabStops as TabStopEntry[] | undefined, entry) }; + }, + options, + ); +} + +export function paragraphsClearTabStopWrapper( + editor: Editor, + input: ParagraphsClearTabStopInput, + options?: MutationOptions, +): ParagraphMutationResult { + rejectTrackedMode('format.paragraph.clearTabStop', options); + const candidate = resolveParagraphBlock(editor, input.target); + return mutateParagraphProperties( + editor, + candidate, + 'format.paragraph.clearTabStop', + input.target, + (pPr) => { + const updated = removeTabStop(pPr.tabStops as TabStopEntry[] | undefined, input.position); + if (updated === undefined) { + const result = { ...pPr }; + delete result.tabStops; + return result; + } + return { ...pPr, tabStops: updated }; + }, + options, + ); +} + +export function paragraphsClearAllTabStopsWrapper( + editor: Editor, + input: ParagraphsClearAllTabStopsInput, + options?: MutationOptions, +): ParagraphMutationResult { + rejectTrackedMode('format.paragraph.clearAllTabStops', options); + const candidate = resolveParagraphBlock(editor, input.target); + return mutateParagraphProperties( + editor, + candidate, + 'format.paragraph.clearAllTabStops', + input.target, + (pPr) => { + const result = { ...pPr }; + delete result.tabStops; + return result; + }, + options, + ); +} + +export function paragraphsSetBorderWrapper( + editor: Editor, + input: ParagraphsSetBorderInput, + options?: MutationOptions, +): ParagraphMutationResult { + rejectTrackedMode('format.paragraph.setBorder', options); + const candidate = resolveParagraphBlock(editor, input.target); + return mutateParagraphProperties( + editor, + candidate, + 'format.paragraph.setBorder', + input.target, + (pPr) => { + const existing = (pPr.borders as PPr) ?? {}; + const border: PPr = { val: input.style }; + if (input.color !== undefined) border.color = input.color; + if (input.size !== undefined) border.size = input.size; + if (input.space !== undefined) border.space = input.space; + return { ...pPr, borders: { ...existing, [input.side]: border } }; + }, + options, + ); +} + +export function paragraphsClearBorderWrapper( + editor: Editor, + input: ParagraphsClearBorderInput, + options?: MutationOptions, +): ParagraphMutationResult { + rejectTrackedMode('format.paragraph.clearBorder', options); + const candidate = resolveParagraphBlock(editor, input.target); + return mutateParagraphProperties( + editor, + candidate, + 'format.paragraph.clearBorder', + input.target, + (pPr) => { + if (input.side === 'all') { + const result = { ...pPr }; + delete result.borders; + return result; + } + const updated = deleteKey((pPr.borders as PPr) ?? {}, input.side); + if (updated === undefined) { + const result = { ...pPr }; + delete result.borders; + return result; + } + return { ...pPr, borders: updated }; + }, + options, + ); +} + +export function paragraphsSetShadingWrapper( + editor: Editor, + input: ParagraphsSetShadingInput, + options?: MutationOptions, +): ParagraphMutationResult { + rejectTrackedMode('format.paragraph.setShading', options); + const candidate = resolveParagraphBlock(editor, input.target); + return mutateParagraphProperties( + editor, + candidate, + 'format.paragraph.setShading', + input.target, + (pPr) => ({ + ...pPr, + shading: mergeDefinedFields(pPr.shading as PPr | undefined, { + fill: input.fill, + color: input.color, + val: input.pattern, + }), + }), + options, + ); +} + +export function paragraphsClearShadingWrapper( + editor: Editor, + input: ParagraphsClearShadingInput, + options?: MutationOptions, +): ParagraphMutationResult { + rejectTrackedMode('format.paragraph.clearShading', options); + const candidate = resolveParagraphBlock(editor, input.target); + return mutateParagraphProperties( + editor, + candidate, + 'format.paragraph.clearShading', + input.target, + (pPr) => { + const result = { ...pPr }; + delete result.shading; + return result; + }, + options, + ); +} diff --git a/packages/super-editor/src/extensions/types/node-attributes.ts b/packages/super-editor/src/extensions/types/node-attributes.ts index f985a7de45..429731dd6d 100644 --- a/packages/super-editor/src/extensions/types/node-attributes.ts +++ b/packages/super-editor/src/extensions/types/node-attributes.ts @@ -148,15 +148,15 @@ export interface ParagraphProperties { styleId?: string; numberingProperties?: NumberingProperties; justification?: 'left' | 'center' | 'right' | 'both' | 'start' | 'end'; - indentation?: IndentationProperties; + indent?: IndentationProperties; spacing?: SpacingProperties; - outlineLevel?: number; + outlineLvl?: number; keepNext?: boolean; keepLines?: boolean; pageBreakBefore?: boolean; widowControl?: boolean; textDirection?: 'lrTb' | 'tbRl' | 'btLr'; - tabs?: Array<{ val: string; pos: number }>; + tabStops?: Array<{ tab: { tabType: string; pos: number; leader?: string } }>; suppressAutoHyphens?: boolean; contextualSpacing?: boolean; } diff --git a/tests/doc-api-stories/tests/formatting/inline-formatting.ts b/tests/doc-api-stories/tests/formatting/inline-formatting.ts index bd6d95103e..ee0943419b 100644 --- a/tests/doc-api-stories/tests/formatting/inline-formatting.ts +++ b/tests/doc-api-stories/tests/formatting/inline-formatting.ts @@ -271,47 +271,7 @@ describe('document-api story: inline formatting', () => { await saveResult(sessionId, 'color.docx'); }); - it('align center: centers paragraph', async () => { - const sessionId = sid('align-center'); - const { target } = await seedBlankFormattableRange( - sessionId, - 'align-center-source.docx', - 'This paragraph should be centered.', - ); - - const result = unwrap(await client.doc.format.align({ sessionId, target, alignment: 'center' })); - assertMutationSuccess(result); - - await saveResult(sessionId, 'align-center.docx'); - }); - - it('align right: right-aligns paragraph', async () => { - const sessionId = sid('align-right'); - const { target } = await seedBlankFormattableRange( - sessionId, - 'align-right-source.docx', - 'This paragraph should be right aligned.', - ); - - const result = unwrap(await client.doc.format.align({ sessionId, target, alignment: 'right' })); - assertMutationSuccess(result); - - await saveResult(sessionId, 'align-right.docx'); - }); - - it('align justify: justifies paragraph', async () => { - const sessionId = sid('align-justify'); - const { target } = await seedBlankFormattableRange( - sessionId, - 'align-justify-source.docx', - 'This paragraph should be fully justified across multiple wrapped lines so the alignment difference is visually obvious in exported output.', - ); - - const result = unwrap(await client.doc.format.align({ sessionId, target, alignment: 'justify' })); - assertMutationSuccess(result); - - await saveResult(sessionId, 'align-justify.docx'); - }); + // Alignment tests moved to format.paragraph.setAlignment — see paragraph-formatting stories. it('combined value formats: fontSize + fontFamily + color on same range', async () => { const sessionId = sid('combined-values'); From f51a5824ef31d1c09d6bc2f6a7a5e9570f82462e Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Fri, 27 Feb 2026 16:20:28 -0800 Subject: [PATCH 2/3] chore: fix cli test failure --- apps/cli/src/lib/document.ts | 37 ++++++++++++++++++++++++++++-- apps/cli/src/lib/session-collab.ts | 2 +- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/apps/cli/src/lib/document.ts b/apps/cli/src/lib/document.ts index 14c1b02635..a46705f1bd 100644 --- a/apps/cli/src/lib/document.ts +++ b/apps/cli/src/lib/document.ts @@ -1,6 +1,6 @@ import { readFile, writeFile } from 'node:fs/promises'; import { createHash } from 'node:crypto'; -import { Editor } from '@superdoc/super-editor'; +import type { Editor } from 'superdoc/super-editor'; import { BLANK_DOCX_BASE64 } from '@superdoc/super-editor/blank-docx'; import { getDocumentApiAdapters } from '@superdoc/super-editor/document-api-adapters'; import { markdownToPmDoc } from '@superdoc/super-editor/markdown'; @@ -37,6 +37,38 @@ export interface FileOutputMeta { byteLength: number; } +type EditorModule = { + Editor: { + open(source: Buffer, options: Record): Promise; + }; +}; + +const EDITOR_IMPORT_CANDIDATES = ['@superdoc/super-editor', 'superdoc/super-editor'] as const; +let cachedEditorModule: EditorModule | null = null; + +async function loadEditorModule(): Promise { + if (cachedEditorModule) return cachedEditorModule; + + const errors: string[] = []; + for (const specifier of EDITOR_IMPORT_CANDIDATES) { + try { + const module = (await import(specifier)) as Partial; + if (module.Editor && typeof module.Editor.open === 'function') { + cachedEditorModule = module as EditorModule; + return cachedEditorModule; + } + errors.push(`${specifier}: module loaded but Editor.open is unavailable`); + } catch (error) { + errors.push(`${specifier}: ${error instanceof Error ? error.message : String(error)}`); + } + } + + throw new CliError('DOCUMENT_OPEN_FAILED', 'Failed to load editor runtime module.', { + candidates: [...EDITOR_IMPORT_CANDIDATES], + errors, + }); +} + function toUint8Array(data: unknown): Uint8Array { if (data instanceof Uint8Array) return data; if (data instanceof ArrayBuffer) return new Uint8Array(data); @@ -122,9 +154,10 @@ export async function openDocument( } let editor: Editor; + const { Editor: EditorRuntime } = await loadEditorModule(); try { const isTest = process.env.NODE_ENV === 'test'; - editor = await Editor.open(Buffer.from(source), { + editor = await EditorRuntime.open(Buffer.from(source), { documentId: options.documentId ?? meta.path ?? 'blank.docx', user: { id: 'cli', name: 'CLI' }, ...(isTest ? { telemetry: { enabled: false } } : {}), diff --git a/apps/cli/src/lib/session-collab.ts b/apps/cli/src/lib/session-collab.ts index 4e953dbd7e..71505189be 100644 --- a/apps/cli/src/lib/session-collab.ts +++ b/apps/cli/src/lib/session-collab.ts @@ -1,4 +1,4 @@ -import type { Editor } from '@superdoc/super-editor'; +import type { Editor } from 'superdoc/super-editor'; import { markContextUpdated, type ContextMetadata, type ContextPaths, writeContextMetadata } from './context'; import { exportToPath, getFileChecksum } from './document'; import { CliError } from './errors'; From e7f4aedcfa656471be1faa4cb496b1bd0dcbd6d4 Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Fri, 27 Feb 2026 16:27:35 -0800 Subject: [PATCH 3/3] fix(document-api): firstLine vs hanging, paragraph wrappers tests --- .../src/__tests__/lib/error-mapping.test.ts | 11 ++ apps/cli/src/cli/operation-hints.ts | 2 +- .../plan-engine/paragraphs-wrappers.test.ts | 103 ++++++++++++++++++ .../plan-engine/paragraphs-wrappers.ts | 21 +++- 4 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 packages/super-editor/src/document-api-adapters/plan-engine/paragraphs-wrappers.test.ts diff --git a/apps/cli/src/__tests__/lib/error-mapping.test.ts b/apps/cli/src/__tests__/lib/error-mapping.test.ts index dff457f77b..685259ba0a 100644 --- a/apps/cli/src/__tests__/lib/error-mapping.test.ts +++ b/apps/cli/src/__tests__/lib/error-mapping.test.ts @@ -262,4 +262,15 @@ describe('mapFailedReceipt: plan-engine code passthrough', () => { expect(result).toBeInstanceOf(CliError); expect(result!.code).not.toBe('NO_OP'); }); + + test('paragraph mutation receipt maps INVALID_TARGET to INVALID_ARGUMENT', () => { + const receipt = { + success: false, + failure: { code: 'INVALID_TARGET', message: 'Paragraph target is invalid.' }, + }; + + const result = mapFailedReceipt('format.paragraph.setAlignment' as any, receipt); + expect(result).toBeInstanceOf(CliError); + expect(result!.code).toBe('INVALID_ARGUMENT'); + }); }); diff --git a/apps/cli/src/cli/operation-hints.ts b/apps/cli/src/cli/operation-hints.ts index bd4da6fa0e..08450053b6 100644 --- a/apps/cli/src/cli/operation-hints.ts +++ b/apps/cli/src/cli/operation-hints.ts @@ -417,7 +417,7 @@ export const OPERATION_FAMILY: Record = 'blocks.delete': 'blocks', 'format.apply': 'textMutation', ...buildFormatInlineAliasRecord('textMutation'), - ...buildParagraphRecord('general'), + ...buildParagraphRecord('textMutation'), 'styles.apply': 'general', 'create.paragraph': 'create', 'create.heading': 'create', diff --git a/packages/super-editor/src/document-api-adapters/plan-engine/paragraphs-wrappers.test.ts b/packages/super-editor/src/document-api-adapters/plan-engine/paragraphs-wrappers.test.ts new file mode 100644 index 0000000000..6215da3ea8 --- /dev/null +++ b/packages/super-editor/src/document-api-adapters/plan-engine/paragraphs-wrappers.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, it, vi } from 'vitest'; +import type { Editor } from '../../core/Editor.js'; + +vi.mock('./plan-wrappers.js', () => ({ + executeDomainCommand: vi.fn((_editor: Editor, handler: () => boolean) => { + const changed = handler(); + return { + success: true, + revision: { before: '0', after: '1' }, + steps: [ + { + stepId: 'step-1', + op: 'domain.command', + effect: changed ? 'changed' : 'noop', + matchCount: changed ? 1 : 0, + data: { domain: 'command', commandDispatched: changed }, + }, + ], + timing: { totalMs: 0 }, + }; + }), +})); + +import { paragraphsSetIndentationWrapper } from './paragraphs-wrappers.js'; + +type MockNode = { + type: { name: 'paragraph' }; + isBlock: true; + nodeSize: number; + attrs: Record; +}; + +function createParagraphNode(attrs: Record): MockNode { + return { + type: { name: 'paragraph' }, + isBlock: true, + nodeSize: 2, + attrs, + }; +} + +function makeEditor(paragraphProperties: Record): { + editor: Editor; + setNodeMarkup: ReturnType; +} { + const paragraphNode = createParagraphNode({ + paraId: 'p1', + sdBlockId: 'p1', + paragraphProperties, + }); + + const setNodeMarkup = vi.fn().mockReturnThis(); + const tr = { + setNodeMarkup, + }; + + const doc = { + descendants(callback: (node: MockNode, pos: number) => void) { + callback(paragraphNode, 0); + }, + nodeAt(pos: number) { + return pos === 0 ? paragraphNode : null; + }, + }; + + const editor = { + state: { doc, tr }, + dispatch: vi.fn(), + commands: {}, + } as unknown as Editor; + + return { editor, setNodeMarkup }; +} + +describe('paragraphsSetIndentationWrapper', () => { + it('drops existing hanging when setting firstLine', () => { + const { editor, setNodeMarkup } = makeEditor({ + indent: { left: 240, hanging: 360 }, + }); + + paragraphsSetIndentationWrapper(editor, { + target: { kind: 'block', nodeType: 'paragraph', nodeId: 'p1' }, + firstLine: 720, + }); + + const nextAttrs = setNodeMarkup.mock.calls[0]?.[2] as { paragraphProperties: { indent: Record } }; + expect(nextAttrs.paragraphProperties.indent).toEqual({ left: 240, firstLine: 720 }); + }); + + it('drops existing firstLine when setting hanging', () => { + const { editor, setNodeMarkup } = makeEditor({ + indent: { right: 120, firstLine: 480 }, + }); + + paragraphsSetIndentationWrapper(editor, { + target: { kind: 'block', nodeType: 'paragraph', nodeId: 'p1' }, + hanging: 360, + }); + + const nextAttrs = setNodeMarkup.mock.calls[0]?.[2] as { paragraphProperties: { indent: Record } }; + expect(nextAttrs.paragraphProperties.indent).toEqual({ right: 120, hanging: 360 }); + }); +}); diff --git a/packages/super-editor/src/document-api-adapters/plan-engine/paragraphs-wrappers.ts b/packages/super-editor/src/document-api-adapters/plan-engine/paragraphs-wrappers.ts index 3ebcd80e0c..4c3e0b5a7b 100644 --- a/packages/super-editor/src/document-api-adapters/plan-engine/paragraphs-wrappers.ts +++ b/packages/super-editor/src/document-api-adapters/plan-engine/paragraphs-wrappers.ts @@ -155,6 +155,25 @@ function mergeDefinedFields(existing: PPr | undefined, patch: Record): PPr { + const result = mergeDefinedFields(existing, patch); + const firstLineWasUpdated = patch.firstLine !== undefined; + const hangingWasUpdated = patch.hanging !== undefined; + + if (firstLineWasUpdated && !hangingWasUpdated) { + delete result.hanging; + } + if (hangingWasUpdated && !firstLineWasUpdated) { + delete result.firstLine; + } + + return result; +} + /** Remove a key from an object, returning undefined if the object is now empty. */ function deleteKey(obj: PPr, key: string): PPr | undefined { const result = { ...obj }; @@ -304,7 +323,7 @@ export function paragraphsSetIndentationWrapper( input.target, (pPr) => ({ ...pPr, - indent: mergeDefinedFields(pPr.indent as PPr | undefined, { + indent: mergeIndentationFields(pPr.indent as PPr | undefined, { left: input.left, right: input.right, firstLine: input.firstLine,