diff --git a/apps/cli/scripts/export-sdk-contract.ts b/apps/cli/scripts/export-sdk-contract.ts index ae49e5bb77..4cf4debe13 100644 --- a/apps/cli/scripts/export-sdk-contract.ts +++ b/apps/cli/scripts/export-sdk-contract.ts @@ -66,7 +66,25 @@ const INTENT_NAMES = { `format_${entry.key.replace(/[A-Z]/g, (char) => `_${char.toLowerCase()}`)}`, ]), ), - 'doc.format.align': 'format_align', + 'doc.styles.paragraph.setStyle': 'set_paragraph_style', + 'doc.styles.paragraph.clearStyle': 'clear_paragraph_style', + 'doc.format.paragraph.resetDirectFormatting': 'reset_paragraph_direct_formatting', + 'doc.format.paragraph.setAlignment': 'set_paragraph_alignment', + 'doc.format.paragraph.clearAlignment': 'clear_paragraph_alignment', + 'doc.format.paragraph.setIndentation': 'set_paragraph_indentation', + 'doc.format.paragraph.clearIndentation': 'clear_paragraph_indentation', + 'doc.format.paragraph.setSpacing': 'set_paragraph_spacing', + 'doc.format.paragraph.clearSpacing': 'clear_paragraph_spacing', + 'doc.format.paragraph.setKeepOptions': 'set_paragraph_keep_options', + 'doc.format.paragraph.setOutlineLevel': 'set_paragraph_outline_level', + 'doc.format.paragraph.setFlowOptions': 'set_paragraph_flow_options', + 'doc.format.paragraph.setTabStop': 'set_paragraph_tab_stop', + 'doc.format.paragraph.clearTabStop': 'clear_paragraph_tab_stop', + 'doc.format.paragraph.clearAllTabStops': 'clear_all_paragraph_tab_stops', + 'doc.format.paragraph.setBorder': 'set_paragraph_border', + 'doc.format.paragraph.clearBorder': 'clear_paragraph_border', + 'doc.format.paragraph.setShading': 'set_paragraph_shading', + 'doc.format.paragraph.clearShading': 'clear_paragraph_shading', '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 9e9d155006..6661371d00 100644 --- a/apps/cli/src/__tests__/conformance/scenarios.ts +++ b/apps/cli/src/__tests__/conformance/scenarios.ts @@ -232,6 +232,56 @@ const FORMAT_INLINE_ALIAS_SUCCESS_SCENARIOS: Record< return [operationId, formatInlineAliasSuccessScenario(operationId)]; }), ) as Record Promise>; + +function paragraphMutationScenario( + operationId: CliOperationId, + label: string, + extraArgs: string[], + prepare: Array<{ operationId: CliOperationId; extraArgs: string[] }> = [], +): (harness: ConformanceHarness) => Promise { + return async (harness) => { + const stateDir = await harness.createStateDir(`${label}-success`); + let docPath = await harness.copyFixtureDoc(`${label}-source`); + let block = await harness.firstBlockMatch(docPath, stateDir); + + for (let index = 0; index < prepare.length; index += 1) { + const step = prepare[index]; + const preparedOut = harness.createOutputPath(`${label}-prepare-${index + 1}`); + const prepared = await harness.runCli( + [ + ...commandTokens(step.operationId), + docPath, + '--target-json', + JSON.stringify({ kind: 'block', nodeType: 'paragraph', nodeId: block.nodeId }), + ...step.extraArgs, + '--out', + preparedOut, + ], + stateDir, + ); + + if (prepared.result.code !== 0 || prepared.envelope.ok !== true) { + throw new Error(`Failed to prepare paragraph scenario ${label} with ${step.operationId}.`); + } + + docPath = preparedOut; + block = await harness.firstBlockMatch(docPath, stateDir); + } + + return { + stateDir, + args: [ + ...commandTokens(operationId), + docPath, + '--target-json', + JSON.stringify({ kind: 'block', nodeType: 'paragraph', nodeId: block.nodeId }), + ...extraArgs, + '--out', + harness.createOutputPath(`${label}-output`), + ], + }; + }; +} // --------------------------------------------------------------------------- // Table scenario helpers (DRY builders for the 40 table operations) // --------------------------------------------------------------------------- @@ -1143,25 +1193,113 @@ export const SUCCESS_SCENARIOS = { }; }, ...FORMAT_INLINE_ALIAS_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.paragraph.setStyle': paragraphMutationScenario('doc.styles.paragraph.setStyle', 'styles-paragraph-set', [ + '--style-id', + 'Normal', + ]), + 'doc.styles.paragraph.clearStyle': paragraphMutationScenario( + 'doc.styles.paragraph.clearStyle', + 'styles-paragraph-clear', + [], + [{ operationId: 'doc.styles.paragraph.setStyle', extraArgs: ['--style-id', '__ConformanceTmpStyle__'] }], + ), + 'doc.format.paragraph.resetDirectFormatting': paragraphMutationScenario( + 'doc.format.paragraph.resetDirectFormatting', + 'format-paragraph-reset', + [], + ), + 'doc.format.paragraph.setAlignment': paragraphMutationScenario( + 'doc.format.paragraph.setAlignment', + 'format-paragraph-set-alignment', + ['--alignment', 'center'], + [{ operationId: 'doc.format.paragraph.setAlignment', extraArgs: ['--alignment', 'left'] }], + ), + 'doc.format.paragraph.clearAlignment': paragraphMutationScenario( + 'doc.format.paragraph.clearAlignment', + 'format-paragraph-clear-alignment', + [], + ), + 'doc.format.paragraph.setIndentation': paragraphMutationScenario( + 'doc.format.paragraph.setIndentation', + 'format-paragraph-set-indentation', + ['--left', '720'], + ), + 'doc.format.paragraph.clearIndentation': paragraphMutationScenario( + 'doc.format.paragraph.clearIndentation', + 'format-paragraph-clear-indentation', + [], + [{ operationId: 'doc.format.paragraph.setIndentation', extraArgs: ['--left', '720'] }], + ), + 'doc.format.paragraph.setSpacing': paragraphMutationScenario( + 'doc.format.paragraph.setSpacing', + 'format-paragraph-set-spacing', + ['--before', '120', '--after', '120'], + ), + 'doc.format.paragraph.clearSpacing': paragraphMutationScenario( + 'doc.format.paragraph.clearSpacing', + 'format-paragraph-clear-spacing', + [], + [{ operationId: 'doc.format.paragraph.setSpacing', extraArgs: ['--before', '120', '--after', '120'] }], + ), + 'doc.format.paragraph.setKeepOptions': paragraphMutationScenario( + 'doc.format.paragraph.setKeepOptions', + 'format-paragraph-set-keep-options', + ['--keep-next', 'true'], + ), + 'doc.format.paragraph.setOutlineLevel': paragraphMutationScenario( + 'doc.format.paragraph.setOutlineLevel', + 'format-paragraph-set-outline', + ['--outline-level-json', '1'], + ), + 'doc.format.paragraph.setFlowOptions': paragraphMutationScenario( + 'doc.format.paragraph.setFlowOptions', + 'format-paragraph-set-flow', + ['--contextual-spacing', 'true'], + ), + 'doc.format.paragraph.setTabStop': paragraphMutationScenario( + 'doc.format.paragraph.setTabStop', + 'format-paragraph-set-tab-stop', + ['--position', '720', '--alignment', 'left'], + ), + 'doc.format.paragraph.clearTabStop': paragraphMutationScenario( + 'doc.format.paragraph.clearTabStop', + 'format-paragraph-clear-tab-stop', + ['--position', '720'], + [{ operationId: 'doc.format.paragraph.setTabStop', extraArgs: ['--position', '720', '--alignment', 'left'] }], + ), + 'doc.format.paragraph.clearAllTabStops': paragraphMutationScenario( + 'doc.format.paragraph.clearAllTabStops', + 'format-paragraph-clear-all-tab-stops', + [], + [{ operationId: 'doc.format.paragraph.setTabStop', extraArgs: ['--position', '720', '--alignment', 'left'] }], + ), + 'doc.format.paragraph.setBorder': paragraphMutationScenario( + 'doc.format.paragraph.setBorder', + 'format-paragraph-set-border', + ['--side', 'top', '--style', 'single', '--color', '000000'], + ), + 'doc.format.paragraph.clearBorder': paragraphMutationScenario( + 'doc.format.paragraph.clearBorder', + 'format-paragraph-clear-border', + ['--side', 'top'], + [ + { + operationId: 'doc.format.paragraph.setBorder', + extraArgs: ['--side', 'top', '--style', 'single', '--color', '000000'], + }, + ], + ), + 'doc.format.paragraph.setShading': paragraphMutationScenario( + 'doc.format.paragraph.setShading', + 'format-paragraph-set-shading', + ['--fill', 'FFFF00'], + ), + 'doc.format.paragraph.clearShading': paragraphMutationScenario( + 'doc.format.paragraph.clearShading', + 'format-paragraph-clear-shading', + [], + [{ operationId: 'doc.format.paragraph.setShading', extraArgs: ['--fill', 'FFFF00'] }], + ), 'doc.styles.apply': async (harness: ConformanceHarness): Promise => { const stateDir = await harness.createStateDir('doc-styles-apply-success'); const docPath = await harness.copyFixtureDoc('doc-styles-apply'); 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 b0d46b6f0e..08450053b6 100644 --- a/apps/cli/src/cli/operation-hints.ts +++ b/apps/cli/src/cli/operation-hints.ts @@ -9,15 +9,13 @@ * OPERATION_DEFINITIONS, the CLI requires only a one-line entry in each table. */ -import { COMMAND_CATALOG } from '@superdoc/document-api'; +import { COMMAND_CATALOG, INLINE_PROPERTY_REGISTRY, type InlineRunPatchKey } from '@superdoc/document-api'; import type { CliExposedOperationId } from './operation-set.js'; -type FormatOperationId = Extract; -type FormatInlineAliasOperationId = Exclude; +type FormatInlineAliasOperationId = `format.${InlineRunPatchKey}`; -const FORMAT_INLINE_ALIAS_OPERATION_IDS = (Object.keys(COMMAND_CATALOG) as CliExposedOperationId[]).filter( - (operationId): operationId is FormatInlineAliasOperationId => - operationId.startsWith('format.') && operationId !== 'format.apply' && operationId !== 'format.align', +const FORMAT_INLINE_ALIAS_OPERATION_IDS = INLINE_PROPERTY_REGISTRY.map( + (entry) => `format.${entry.key}` as FormatInlineAliasOperationId, ); function buildFormatInlineAliasRecord(value: T): Record { @@ -27,6 +25,37 @@ function buildFormatInlineAliasRecord(value: T): Record; } +const PARAGRAPH_OPERATION_IDS = [ + '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', +] as const satisfies readonly CliExposedOperationId[]; + +type ParagraphOperationId = (typeof PARAGRAPH_OPERATION_IDS)[number]; + +function buildParagraphRecord(value: T): Record { + return Object.fromEntries(PARAGRAPH_OPERATION_IDS.map((operationId) => [operationId, value])) as Record< + ParagraphOperationId, + T + >; +} + // --------------------------------------------------------------------------- // Orchestration kind (derived from COMMAND_CATALOG) // --------------------------------------------------------------------------- @@ -52,8 +81,8 @@ export const SUCCESS_VERB: Record = { delete: 'deleted text', 'blocks.delete': 'deleted block', 'format.apply': 'applied style', - 'format.align': 'set alignment', ...buildFormatInlineAliasRecord('applied style'), + ...buildParagraphRecord('updated paragraph formatting'), 'styles.apply': 'applied stylesheet defaults', 'create.paragraph': 'created paragraph', 'create.heading': 'created heading', @@ -165,8 +194,8 @@ export const OUTPUT_FORMAT: Record = { delete: 'mutationReceipt', 'blocks.delete': 'plain', 'format.apply': 'mutationReceipt', - 'format.align': 'mutationReceipt', ...buildFormatInlineAliasRecord('mutationReceipt'), + ...buildParagraphRecord('plain'), 'styles.apply': 'receipt', 'create.paragraph': 'createResult', 'create.heading': 'createResult', @@ -262,8 +291,8 @@ export const RESPONSE_ENVELOPE_KEY: Record delete: null, 'blocks.delete': 'result', 'format.apply': null, - 'format.align': null, ...buildFormatInlineAliasRecord(null), + ...buildParagraphRecord('result'), 'styles.apply': 'receipt', 'create.paragraph': 'result', 'create.heading': 'result', @@ -353,7 +382,6 @@ export const RESPONSE_VALIDATION_KEY: Partial = delete: 'textMutation', 'blocks.delete': 'blocks', 'format.apply': 'textMutation', - 'format.align': 'textMutation', ...buildFormatInlineAliasRecord('textMutation'), + ...buildParagraphRecord('textMutation'), 'styles.apply': 'general', 'create.paragraph': 'create', 'create.heading': 'create', diff --git a/apps/cli/src/lib/document.ts b/apps/cli/src/lib/document.ts index 707587a1a1..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/special-handlers.ts b/apps/cli/src/lib/special-handlers.ts index b3f4727307..24029e20f6 100644 --- a/apps/cli/src/lib/special-handlers.ts +++ b/apps/cli/src/lib/special-handlers.ts @@ -9,7 +9,8 @@ */ import { createHash } from 'node:crypto'; -import { CLI_DOC_OPERATIONS, type CliExposedOperationId } from '../cli/operation-set.js'; +import { INLINE_PROPERTY_REGISTRY } from '@superdoc/document-api'; +import type { CliExposedOperationId } from '../cli/operation-set.js'; import type { EditorWithDoc } from './document.js'; // --------------------------------------------------------------------------- @@ -25,9 +26,10 @@ type PreInvokeHook = (input: unknown, context: HookContext) => unknown; type PostInvokeHook = (result: unknown, context: HookContext) => unknown; -const FORMAT_OPERATION_IDS = CLI_DOC_OPERATIONS.filter((operationId): operationId is CliExposedOperationId => - operationId.startsWith('format.'), -); +const FORMAT_RECEIPT_OPERATION_IDS: readonly CliExposedOperationId[] = [ + 'format.apply', + ...INLINE_PROPERTY_REGISTRY.map((entry) => `format.${entry.key}` as CliExposedOperationId), +]; // --------------------------------------------------------------------------- // Track-changes stable-ID helpers @@ -231,7 +233,7 @@ const flattenTextMutationReceipt: PostInvokeHook = (result) => { }; const FORMAT_POST_INVOKE_HOOKS: Partial> = Object.fromEntries( - FORMAT_OPERATION_IDS.map((operationId) => [operationId, flattenTextMutationReceipt]), + FORMAT_RECEIPT_OPERATION_IDS.map((operationId) => [operationId, flattenTextMutationReceipt]), ) as Partial>; /** Pre-invoke: custom input resolution before calling editor.doc.invoke(). */ diff --git a/apps/docs/document-api/available-operations.mdx b/apps/docs/document-api/available-operations.mdx index eb0dae76cc..24491cd292 100644 --- a/apps/docs/document-api/available-operations.mdx +++ b/apps/docs/document-api/available-operations.mdx @@ -22,6 +22,8 @@ Use the tables below to see what operations are available and where each one is | Format | 44 | 1 | 45 | [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) | @@ -59,6 +61,7 @@ Use the tables below to see what operations are available and where each one is | editor.doc.format.highlight(...) | [`format.highlight`](/document-api/reference/format/highlight) | | editor.doc.format.color(...) | [`format.color`](/document-api/reference/format/color) | | 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.letterSpacing(...) | [`format.letterSpacing`](/document-api/reference/format/letter-spacing) | | editor.doc.format.vertAlign(...) | [`format.vertAlign`](/document-api/reference/format/vert-align) | | editor.doc.format.position(...) | [`format.position`](/document-api/reference/format/position) | @@ -94,7 +97,6 @@ Use the tables below to see what operations are available and where each one is | editor.doc.format.numSpacing(...) | [`format.numSpacing`](/document-api/reference/format/num-spacing) | | editor.doc.format.stylisticSets(...) | [`format.stylisticSets`](/document-api/reference/format/stylistic-sets) | | editor.doc.format.contextualAlternates(...) | [`format.contextualAlternates`](/document-api/reference/format/contextual-alternates) | -| editor.doc.format.align(...) | [`format.align`](/document-api/reference/format/align) | | editor.doc.format.strikethrough(...) | [`format.strike`](/document-api/reference/format/strike) | | editor.doc.lists.list(...) | [`lists.list`](/document-api/reference/lists/list) | | editor.doc.lists.get(...) | [`lists.get`](/document-api/reference/lists/get) | @@ -106,6 +108,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 4f6ededfe9..bd76195562 100644 --- a/apps/docs/document-api/reference/_generated-manifest.json +++ b/apps/docs/document-api/reference/_generated-manifest.json @@ -20,7 +20,6 @@ "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/b-cs.mdx", "apps/docs/document-api/reference/format/bold.mdx", @@ -35,6 +34,7 @@ "apps/docs/document-api/reference/format/em.mdx", "apps/docs/document-api/reference/format/emboss.mdx", "apps/docs/document-api/reference/format/fit-text.mdx", + "apps/docs/document-api/reference/format/font-family.mdx", "apps/docs/document-api/reference/format/font-size-cs.mdx", "apps/docs/document-api/reference/format/font-size.mdx", "apps/docs/document-api/reference/format/highlight.mdx", @@ -50,6 +50,24 @@ "apps/docs/document-api/reference/format/num-spacing.mdx", "apps/docs/document-api/reference/format/o-math.mdx", "apps/docs/document-api/reference/format/outline.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/format/position.mdx", "apps/docs/document-api/reference/format/r-fonts.mdx", "apps/docs/document-api/reference/format/r-style.mdx", @@ -107,6 +125,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", @@ -232,6 +253,7 @@ "format.highlight", "format.color", "format.fontSize", + "format.fontFamily", "format.letterSpacing", "format.vertAlign", "format.position", @@ -266,8 +288,7 @@ "format.numForm", "format.numSpacing", "format.stylisticSets", - "format.contextualAlternates", - "format.align" + "format.contextualAlternates" ], "pagePath": "apps/docs/document-api/reference/format/index.mdx", "title": "Format" @@ -323,6 +344,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", @@ -379,5 +432,5 @@ } ], "marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}", - "sourceHash": "b347a10d2f82fe516ca8eacd8770c036dabb5b9faf176f7373dd8328aaa4ef98" + "sourceHash": "98ac639d0837d66b0d968f4a0811a0bac22f407a989caeda784326e40f337e68" } diff --git a/apps/docs/document-api/reference/capabilities/get.mdx b/apps/docs/document-api/reference/capabilities/get.mdx index e9d5795fa5..23cad987a5 100644 --- a/apps/docs/document-api/reference/capabilities/get.mdx +++ b/apps/docs/document-api/reference/capabilities/get.mdx @@ -127,6 +127,12 @@ _No fields._ "tracked": true, "type": "boolean" }, + "fontFamily": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, "fontSize": { "available": true, "storage": "mark", @@ -442,14 +448,6 @@ _No fields._ ], "tracked": true }, - "format.align": { - "available": true, - "dryRun": true, - "reasons": [ - "COMMAND_UNAVAILABLE" - ], - "tracked": true - }, "format.apply": { "available": true, "dryRun": true, @@ -562,6 +560,14 @@ _No fields._ ], "tracked": true }, + "format.fontFamily": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, "format.fontSize": { "available": true, "dryRun": true, @@ -674,6 +680,142 @@ _No fields._ ], "tracked": true }, + "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": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, "format.position": { "available": true, "dryRun": true, @@ -1082,6 +1224,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, @@ -1936,6 +2094,39 @@ _No fields._ ], "type": "object" }, + "fontFamily": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "storage": { + "enum": [ + "mark", + "runAttribute" + ] + }, + "tracked": { + "type": "boolean" + }, + "type": { + "enum": [ + "boolean", + "string", + "number", + "object", + "array" + ] + } + }, + "required": [ + "available", + "tracked", + "type", + "storage" + ], + "type": "object" + }, "fontSize": { "additionalProperties": false, "properties": { @@ -2902,6 +3093,7 @@ _No fields._ "highlight", "color", "fontSize", + "fontFamily", "letterSpacing", "vertAlign", "position", @@ -3559,7 +3751,7 @@ _No fields._ ], "type": "object" }, - "format.align": { + "format.apply": { "additionalProperties": false, "properties": { "available": { @@ -3594,7 +3786,7 @@ _No fields._ ], "type": "object" }, - "format.apply": { + "format.bCs": { "additionalProperties": false, "properties": { "available": { @@ -3629,7 +3821,7 @@ _No fields._ ], "type": "object" }, - "format.bCs": { + "format.bold": { "additionalProperties": false, "properties": { "available": { @@ -3664,7 +3856,7 @@ _No fields._ ], "type": "object" }, - "format.bold": { + "format.border": { "additionalProperties": false, "properties": { "available": { @@ -3699,7 +3891,7 @@ _No fields._ ], "type": "object" }, - "format.border": { + "format.caps": { "additionalProperties": false, "properties": { "available": { @@ -3734,7 +3926,7 @@ _No fields._ ], "type": "object" }, - "format.caps": { + "format.charScale": { "additionalProperties": false, "properties": { "available": { @@ -3769,7 +3961,7 @@ _No fields._ ], "type": "object" }, - "format.charScale": { + "format.color": { "additionalProperties": false, "properties": { "available": { @@ -3804,7 +3996,7 @@ _No fields._ ], "type": "object" }, - "format.color": { + "format.contextualAlternates": { "additionalProperties": false, "properties": { "available": { @@ -3839,7 +4031,7 @@ _No fields._ ], "type": "object" }, - "format.contextualAlternates": { + "format.cs": { "additionalProperties": false, "properties": { "available": { @@ -3874,7 +4066,7 @@ _No fields._ ], "type": "object" }, - "format.cs": { + "format.dstrike": { "additionalProperties": false, "properties": { "available": { @@ -3909,7 +4101,7 @@ _No fields._ ], "type": "object" }, - "format.dstrike": { + "format.eastAsianLayout": { "additionalProperties": false, "properties": { "available": { @@ -3944,7 +4136,7 @@ _No fields._ ], "type": "object" }, - "format.eastAsianLayout": { + "format.em": { "additionalProperties": false, "properties": { "available": { @@ -3979,7 +4171,7 @@ _No fields._ ], "type": "object" }, - "format.em": { + "format.emboss": { "additionalProperties": false, "properties": { "available": { @@ -4014,7 +4206,7 @@ _No fields._ ], "type": "object" }, - "format.emboss": { + "format.fitText": { "additionalProperties": false, "properties": { "available": { @@ -4049,7 +4241,7 @@ _No fields._ ], "type": "object" }, - "format.fitText": { + "format.fontFamily": { "additionalProperties": false, "properties": { "available": { @@ -4574,7 +4766,7 @@ _No fields._ ], "type": "object" }, - "format.position": { + "format.paragraph.clearAlignment": { "additionalProperties": false, "properties": { "available": { @@ -4609,7 +4801,7 @@ _No fields._ ], "type": "object" }, - "format.rFonts": { + "format.paragraph.clearAllTabStops": { "additionalProperties": false, "properties": { "available": { @@ -4644,7 +4836,7 @@ _No fields._ ], "type": "object" }, - "format.rStyle": { + "format.paragraph.clearBorder": { "additionalProperties": false, "properties": { "available": { @@ -4679,7 +4871,7 @@ _No fields._ ], "type": "object" }, - "format.rtl": { + "format.paragraph.clearIndentation": { "additionalProperties": false, "properties": { "available": { @@ -4714,7 +4906,7 @@ _No fields._ ], "type": "object" }, - "format.shading": { + "format.paragraph.clearShading": { "additionalProperties": false, "properties": { "available": { @@ -4749,7 +4941,7 @@ _No fields._ ], "type": "object" }, - "format.shadow": { + "format.paragraph.clearSpacing": { "additionalProperties": false, "properties": { "available": { @@ -4784,7 +4976,7 @@ _No fields._ ], "type": "object" }, - "format.smallCaps": { + "format.paragraph.clearTabStop": { "additionalProperties": false, "properties": { "available": { @@ -4819,7 +5011,7 @@ _No fields._ ], "type": "object" }, - "format.snapToGrid": { + "format.paragraph.resetDirectFormatting": { "additionalProperties": false, "properties": { "available": { @@ -4854,7 +5046,7 @@ _No fields._ ], "type": "object" }, - "format.specVanish": { + "format.paragraph.setAlignment": { "additionalProperties": false, "properties": { "available": { @@ -4889,7 +5081,7 @@ _No fields._ ], "type": "object" }, - "format.strike": { + "format.paragraph.setBorder": { "additionalProperties": false, "properties": { "available": { @@ -4924,7 +5116,7 @@ _No fields._ ], "type": "object" }, - "format.stylisticSets": { + "format.paragraph.setFlowOptions": { "additionalProperties": false, "properties": { "available": { @@ -4959,7 +5151,7 @@ _No fields._ ], "type": "object" }, - "format.underline": { + "format.paragraph.setIndentation": { "additionalProperties": false, "properties": { "available": { @@ -4994,7 +5186,7 @@ _No fields._ ], "type": "object" }, - "format.vanish": { + "format.paragraph.setKeepOptions": { "additionalProperties": false, "properties": { "available": { @@ -5029,7 +5221,7 @@ _No fields._ ], "type": "object" }, - "format.vertAlign": { + "format.paragraph.setOutlineLevel": { "additionalProperties": false, "properties": { "available": { @@ -5064,7 +5256,7 @@ _No fields._ ], "type": "object" }, - "format.webHidden": { + "format.paragraph.setShading": { "additionalProperties": false, "properties": { "available": { @@ -5099,7 +5291,7 @@ _No fields._ ], "type": "object" }, - "getNode": { + "format.paragraph.setSpacing": { "additionalProperties": false, "properties": { "available": { @@ -5134,7 +5326,7 @@ _No fields._ ], "type": "object" }, - "getNodeById": { + "format.paragraph.setTabStop": { "additionalProperties": false, "properties": { "available": { @@ -5169,7 +5361,7 @@ _No fields._ ], "type": "object" }, - "getText": { + "format.position": { "additionalProperties": false, "properties": { "available": { @@ -5204,7 +5396,7 @@ _No fields._ ], "type": "object" }, - "info": { + "format.rFonts": { "additionalProperties": false, "properties": { "available": { @@ -5239,7 +5431,7 @@ _No fields._ ], "type": "object" }, - "insert": { + "format.rStyle": { "additionalProperties": false, "properties": { "available": { @@ -5274,7 +5466,7 @@ _No fields._ ], "type": "object" }, - "lists.exit": { + "format.rtl": { "additionalProperties": false, "properties": { "available": { @@ -5309,7 +5501,7 @@ _No fields._ ], "type": "object" }, - "lists.get": { + "format.shading": { "additionalProperties": false, "properties": { "available": { @@ -5344,7 +5536,7 @@ _No fields._ ], "type": "object" }, - "lists.indent": { + "format.shadow": { "additionalProperties": false, "properties": { "available": { @@ -5379,7 +5571,7 @@ _No fields._ ], "type": "object" }, - "lists.insert": { + "format.smallCaps": { "additionalProperties": false, "properties": { "available": { @@ -5414,7 +5606,7 @@ _No fields._ ], "type": "object" }, - "lists.list": { + "format.snapToGrid": { "additionalProperties": false, "properties": { "available": { @@ -5449,7 +5641,7 @@ _No fields._ ], "type": "object" }, - "lists.outdent": { + "format.specVanish": { "additionalProperties": false, "properties": { "available": { @@ -5484,7 +5676,7 @@ _No fields._ ], "type": "object" }, - "lists.restart": { + "format.strike": { "additionalProperties": false, "properties": { "available": { @@ -5519,7 +5711,7 @@ _No fields._ ], "type": "object" }, - "lists.setType": { + "format.stylisticSets": { "additionalProperties": false, "properties": { "available": { @@ -5554,7 +5746,7 @@ _No fields._ ], "type": "object" }, - "mutations.apply": { + "format.underline": { "additionalProperties": false, "properties": { "available": { @@ -5589,7 +5781,7 @@ _No fields._ ], "type": "object" }, - "mutations.preview": { + "format.vanish": { "additionalProperties": false, "properties": { "available": { @@ -5624,7 +5816,602 @@ _No fields._ ], "type": "object" }, - "query.match": { + "format.vertAlign": { + "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" + }, + "format.webHidden": { + "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" + }, + "getNode": { + "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" + }, + "getNodeById": { + "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" + }, + "getText": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "info": { + "additionalProperties": false, + "properties": { + "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" + }, + "insert": { + "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" + }, + "lists.exit": { + "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" + }, + "lists.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" + }, + "lists.indent": { + "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" + }, + "lists.insert": { + "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" + }, + "lists.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" + }, + "lists.outdent": { + "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" + }, + "lists.restart": { + "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" + }, + "lists.setType": { + "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" + }, + "mutations.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" + }, + "mutations.preview": { + "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" + }, + "query.match": { "additionalProperties": false, "properties": { "available": { @@ -6359,6 +7146,76 @@ _No fields._ ], "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": { + "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" + }, "tables.applyBorderPreset": { "additionalProperties": false, "properties": { @@ -8023,6 +8880,7 @@ _No fields._ "format.highlight", "format.color", "format.fontSize", + "format.fontFamily", "format.letterSpacing", "format.vertAlign", "format.position", @@ -8058,7 +8916,6 @@ _No fields._ "format.numSpacing", "format.stylisticSets", "format.contextualAlternates", - "format.align", "styles.apply", "create.paragraph", "create.heading", @@ -8081,6 +8938,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/apply.mdx b/apps/docs/document-api/reference/format/apply.mdx index 1d2931aadf..c33e58acb4 100644 --- a/apps/docs/document-api/reference/format/apply.mdx +++ b/apps/docs/document-api/reference/format/apply.mdx @@ -380,6 +380,17 @@ _No fields._ } ] }, + "fontFamily": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, "fontSize": { "oneOf": [ { diff --git a/apps/docs/document-api/reference/format/align.mdx b/apps/docs/document-api/reference/format/font-family.mdx similarity index 80% rename from apps/docs/document-api/reference/format/align.mdx rename to apps/docs/document-api/reference/format/font-family.mdx index b347b66417..cf3098d647 100644 --- a/apps/docs/document-api/reference/format/align.mdx +++ b/apps/docs/document-api/reference/format/font-family.mdx @@ -1,7 +1,7 @@ --- -title: format.align -sidebarTitle: format.align -description: Set or unset paragraph alignment on the block containing the target. Pass null to reset to default. +title: format.fontFamily +sidebarTitle: format.fontFamily +description: "Set or clear the `fontFamily` inline run property on the target text range." --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,32 +10,31 @@ description: Set or unset paragraph alignment on the block containing the target ## Summary -Set or unset paragraph alignment on the block containing the target. Pass null to reset to default. +Set or clear the `fontFamily` inline run property on the target text range. -- Operation ID: `format.align` -- API member path: `editor.doc.format.align(...)` +- Operation ID: `format.fontFamily` +- API member path: `editor.doc.format.fontFamily(...)` - Mutates document: `yes` - Idempotency: `conditional` -- Supports tracked mode: `no` +- Supports tracked mode: `yes` - 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. +Returns a TextMutationReceipt confirming the inline run property patch was applied to the target range. ## Input fields | Field | Type | Required | Description | | --- | --- | --- | --- | -| `alignment` | enum \\| null | yes | One of: enum, null | | `target` | TextAddress | yes | TextAddress | +| `value` | string \\| null | yes | One of: string, null | ### Example request ```json { - "alignment": "left", "target": { "blockId": "block-abc123", "kind": "text", @@ -43,7 +42,8 @@ Returns a TextMutationReceipt; receipt reports NO_OP if the block already has th "end": 10, "start": 0 } - } + }, + "value": "example" } ``` @@ -106,7 +106,6 @@ _No fields._ ## Non-applied failure codes - `INVALID_TARGET` -- `NO_OP` ## Raw schemas @@ -115,28 +114,24 @@ _No fields._ { "additionalProperties": false, "properties": { - "alignment": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { "oneOf": [ { - "enum": [ - "left", - "center", - "right", - "justify" - ] + "minLength": 1, + "type": "string" }, { "type": "null" } ] - }, - "target": { - "$ref": "#/$defs/TextAddress" } }, "required": [ "target", - "alignment" + "value" ], "type": "object" } @@ -158,8 +153,7 @@ _No fields._ "properties": { "code": { "enum": [ - "INVALID_TARGET", - "NO_OP" + "INVALID_TARGET" ] }, "details": {}, @@ -210,8 +204,7 @@ _No fields._ "properties": { "code": { "enum": [ - "INVALID_TARGET", - "NO_OP" + "INVALID_TARGET" ] }, "details": {}, diff --git a/apps/docs/document-api/reference/format/index.mdx b/apps/docs/document-api/reference/format/index.mdx index 99f020687f..539a866d59 100644 --- a/apps/docs/document-api/reference/format/index.mdx +++ b/apps/docs/document-api/reference/format/index.mdx @@ -22,6 +22,7 @@ Canonical formatting mutation with directive semantics ('on', 'off', 'clear'). | format.highlight | `format.highlight` | Yes | `conditional` | Yes | Yes | | format.color | `format.color` | Yes | `conditional` | Yes | Yes | | format.fontSize | `format.fontSize` | Yes | `conditional` | Yes | Yes | +| format.fontFamily | `format.fontFamily` | Yes | `conditional` | Yes | Yes | | format.letterSpacing | `format.letterSpacing` | Yes | `conditional` | Yes | Yes | | format.vertAlign | `format.vertAlign` | Yes | `conditional` | Yes | Yes | | format.position | `format.position` | Yes | `conditional` | Yes | Yes | @@ -57,7 +58,6 @@ Canonical formatting mutation with directive semantics ('on', 'off', 'clear'). | format.numSpacing | `format.numSpacing` | Yes | `conditional` | No | Yes | | format.stylisticSets | `format.stylisticSets` | Yes | `conditional` | No | Yes | | format.contextualAlternates | `format.contextualAlternates` | 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..02b0d2dc8d --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/clear-alignment.mdx @@ -0,0 +1,331 @@ +--- +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` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## 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..c00e783c1f --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/clear-all-tab-stops.mdx @@ -0,0 +1,331 @@ +--- +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` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## 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..e2b3d39e09 --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/clear-border.mdx @@ -0,0 +1,345 @@ +--- +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` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## 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..f5ea2eb7c5 --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/clear-indentation.mdx @@ -0,0 +1,331 @@ +--- +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` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## 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..d47ab36a9a --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/clear-shading.mdx @@ -0,0 +1,331 @@ +--- +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` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## 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..c97ab8cb14 --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/clear-spacing.mdx @@ -0,0 +1,331 @@ +--- +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` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## 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..544a6b0af2 --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/clear-tab-stop.mdx @@ -0,0 +1,338 @@ +--- +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` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## 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..b86793bb0b --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/reset-direct-formatting.mdx @@ -0,0 +1,331 @@ +--- +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` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## 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..f3b5e6bcb4 --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/set-alignment.mdx @@ -0,0 +1,342 @@ +--- +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` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## 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..5b04f0c340 --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/set-border.mdx @@ -0,0 +1,368 @@ +--- +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` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## 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..d77a065842 --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/set-flow-options.mdx @@ -0,0 +1,366 @@ +--- +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` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## 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..2b64778547 --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/set-indentation.mdx @@ -0,0 +1,382 @@ +--- +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` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## 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..b84b87e827 --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/set-keep-options.mdx @@ -0,0 +1,366 @@ +--- +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` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## 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..56b0934971 --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/set-outline-level.mdx @@ -0,0 +1,346 @@ +--- +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` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## 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..0883eddb21 --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/set-shading.mdx @@ -0,0 +1,369 @@ +--- +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` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## 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..8ba0ef77a3 --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/set-spacing.mdx @@ -0,0 +1,389 @@ +--- +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` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## 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..2376a6f619 --- /dev/null +++ b/apps/docs/document-api/reference/format/paragraph/set-tab-stop.mdx @@ -0,0 +1,362 @@ +--- +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` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## 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 1ffd81f8ce..73e31ad2c6 100644 --- a/apps/docs/document-api/reference/index.mdx +++ b/apps/docs/document-api/reference/index.mdx @@ -32,6 +32,8 @@ Document API is currently alpha and subject to breaking changes. | 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) | | Table of Contents | 5 | 0 | 5 | [Open](/document-api/reference/toc/index) | @@ -109,6 +111,7 @@ The tables below are grouped by namespace. | format.highlight | editor.doc.format.highlight(...) | Set or clear the `highlight` inline run property on the target text range. | | format.color | editor.doc.format.color(...) | Set or clear the `color` inline run property on the target text range. | | format.fontSize | editor.doc.format.fontSize(...) | Set or clear the `fontSize` inline run property on the target text range. | +| format.fontFamily | editor.doc.format.fontFamily(...) | Set or clear the `fontFamily` inline run property on the target text range. | | format.letterSpacing | editor.doc.format.letterSpacing(...) | Set or clear the `letterSpacing` inline run property on the target text range. | | format.vertAlign | editor.doc.format.vertAlign(...) | Set or clear the `vertAlign` inline run property on the target text range. | | format.position | editor.doc.format.position(...) | Set or clear the `position` inline run property on the target text range. | @@ -144,7 +147,6 @@ The tables below are grouped by namespace. | format.numSpacing | editor.doc.format.numSpacing(...) | Set or clear the `numSpacing` inline run property on the target text range. | | format.stylisticSets | editor.doc.format.stylisticSets(...) | Set or clear the `stylisticSets` inline run property on the target text range. | | format.contextualAlternates | editor.doc.format.contextualAlternates(...) | Set or clear the `contextualAlternates` inline run property on the target text range. | -| format.align | editor.doc.format.align(...) | Set or unset paragraph alignment on the block containing the target. Pass null to reset to default. | | format.strikethrough | editor.doc.format.strikethrough(...) | Convenience alias for `format.strike` with `value: true`. | #### Styles @@ -197,6 +199,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..3afa42a9d6 --- /dev/null +++ b/apps/docs/document-api/reference/styles/paragraph/clear-style.mdx @@ -0,0 +1,331 @@ +--- +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` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## 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..b2f4519dcf --- /dev/null +++ b/apps/docs/document-api/reference/styles/paragraph/set-style.mdx @@ -0,0 +1,338 @@ +--- +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` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## 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 2c146087ec..c290694e19 100644 --- a/apps/docs/document-engine/sdks.mdx +++ b/apps/docs/document-engine/sdks.mdx @@ -217,6 +217,7 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p | `doc.format.highlight` | `format highlight` | Set or clear the `highlight` inline run property on the target text range. | | `doc.format.color` | `format color` | Set or clear the `color` inline run property on the target text range. | | `doc.format.fontSize` | `format font-size` | Set or clear the `fontSize` inline run property on the target text range. | +| `doc.format.fontFamily` | `format font-family` | Set or clear the `fontFamily` inline run property on the target text range. | | `doc.format.letterSpacing` | `format letter-spacing` | Set or clear the `letterSpacing` inline run property on the target text range. | | `doc.format.vertAlign` | `format vert-align` | Set or clear the `vertAlign` inline run property on the target text range. | | `doc.format.position` | `format position` | Set or clear the `position` inline run property on the target text range. | @@ -252,7 +253,6 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p | `doc.format.numSpacing` | `format num-spacing` | Set or clear the `numSpacing` inline run property on the target text range. | | `doc.format.stylisticSets` | `format stylistic-sets` | Set or clear the `stylisticSets` inline run property on the target text range. | | `doc.format.contextualAlternates` | `format contextual-alternates` | Set or clear the `contextualAlternates` inline run property on the target text range. | -| `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 97331755c1..7f303a97ed 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 01b1335579..9857a446e0 100644 --- a/packages/document-api/src/contract/operation-definitions.ts +++ b/packages/document-api/src/contract/operation-definitions.ts @@ -39,7 +39,9 @@ export type ReferenceGroupKey = | 'create' | 'sections' | 'format' + | 'format.paragraph' | 'styles' + | 'styles.paragraph' | 'lists' | 'comments' | 'trackChanges' @@ -155,6 +157,7 @@ const T_SECTION_CREATE = [ 'INTERNAL_ERROR', ] as const; const T_SECTION_READ = ['TARGET_NOT_FOUND', 'INVALID_TARGET', 'INVALID_INPUT', 'CAPABILITY_UNAVAILABLE'] as const; +const T_PARAGRAPH_MUTATION = ['TARGET_NOT_FOUND', 'INVALID_TARGET', 'CAPABILITY_UNAVAILABLE'] as const; const T_SECTION_MUTATION = [ 'TARGET_NOT_FOUND', 'INVALID_TARGET', @@ -349,22 +352,6 @@ export const OPERATION_DEFINITIONS = { referenceGroup: 'format', }, ...FORMAT_INLINE_ALIAS_OPERATION_DEFINITIONS, - 'format.align': { - memberPath: 'format.align', - description: 'Set or unset paragraph alignment on the block containing the target. Pass null to reset to default.', - expectedResult: - 'Returns a TextMutationReceipt; receipt reports NO_OP if the block already has the requested alignment.', - requiresDocumentContext: true, - metadata: mutationOperation({ - idempotency: 'conditional', - 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', @@ -704,6 +691,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 7f22f927ef..10c40429ef 100644 --- a/packages/document-api/src/contract/operation-registry.ts +++ b/packages/document-api/src/contract/operation-registry.ts @@ -27,7 +27,7 @@ import type { InsertInput } from '../insert/insert.js'; import type { ReplaceInput } from '../replace/replace.js'; import type { DeleteInput } from '../delete/delete.js'; import type { MutationOptions, RevisionGuardOptions } from '../write/write.js'; -import type { FormatInlineAliasInput, StyleApplyInput, FormatAlignInput } from '../format/format.js'; +import type { FormatInlineAliasInput, StyleApplyInput } from '../format/format.js'; import type { InlineRunPatchKey } from '../format/inline-run-patch.js'; import type { StylesApplyInput, StylesApplyOptions, StylesApplyReceipt } from '../styles/styles.js'; import type { @@ -52,6 +52,28 @@ import type { ListTargetInput, ListsExitResult, } from '../lists/lists.types.js'; +import type { + 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, @@ -170,7 +192,104 @@ export interface OperationRegistry extends FormatInlineAliasOperationRegistry { // --- format.* --- 'format.apply': { input: StyleApplyInput; 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 7ae51799b2..21d3bc1a94 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; @@ -337,6 +344,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'); @@ -973,6 +983,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 }, @@ -1438,18 +1480,6 @@ const operationSchemas: Record = { failure: textMutationFailureSchemaFor('format.apply'), }, ...formatInlineAliasOperationSchemas, - '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( { @@ -1473,6 +1503,225 @@ 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 1567b49c04..6023774ec6 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, assertType } from 'vitest'; import type { FormatAdapter, FormatInlineAliasInput, StyleApplyInput } from './format.js'; -import { executeStyleApply, executeAlign, executeInlineAlias } from './format.js'; +import { executeStyleApply, executeInlineAlias } from './format.js'; import { DocumentApiValidationError } from '../errors.js'; import type { TextMutationReceipt } from '../types/index.js'; @@ -22,7 +22,6 @@ function makeReceipt(): TextMutationReceipt { function makeAdapter(): FormatAdapter & Record> { return { apply: vi.fn(() => makeReceipt()), - align: vi.fn(() => makeReceipt()), }; } @@ -124,56 +123,6 @@ describe('executeStyleApply validation', () => { }); }); -function targetValidationSuite( - name: string, - exec: (adapter: ReturnType, input: unknown, options?: unknown) => unknown, -) { - describe(`${name} target validation`, () => { - it('rejects non-object input', () => { - expect(() => exec(makeAdapter(), null)).toThrow(DocumentApiValidationError); - }); - - it('rejects missing target', () => { - expect(() => exec(makeAdapter(), { alignment: 'left' })).toThrow('requires a target'); - }); - - it('rejects invalid target', () => { - expect(() => exec(makeAdapter(), { target: 'bad', alignment: 'left' })).toThrow('text address'); - }); - }); -} - -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 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(); - }); -}); - -// --------------------------------------------------------------------------- // executeInlineAlias — runtime + type contract // --------------------------------------------------------------------------- diff --git a/packages/document-api/src/format/format.ts b/packages/document-api/src/format/format.ts index a4a37155df..a716e991fb 100644 --- a/packages/document-api/src/format/format.ts +++ b/packages/document-api/src/format/format.ts @@ -5,16 +5,6 @@ import { isRecord, isTextAddress, assertNoUnknownFields } from '../validation-pr import type { InlineRunPatch, InlineRunPatchKey } from './inline-run-patch.js'; import { INLINE_PROPERTY_BY_KEY, validateInlineRunPatch } from './inline-run-patch.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 // --------------------------------------------------------------------------- @@ -69,12 +59,6 @@ export interface StyleApplyInput { /** Options for `format.apply` — same shape as all other mutations. */ export type StyleApplyOptions = MutationOptions; -/** Input payload for `format.align`. Pass `null` to unset (reset to default). */ -export interface FormatAlignInput { - target: TextAddress; - alignment: Alignment | null; -} - // --------------------------------------------------------------------------- // Adapter interface // --------------------------------------------------------------------------- @@ -82,7 +66,6 @@ export interface FormatAlignInput { /** Engine-specific adapter for format operations. */ export interface FormatAdapter { apply(input: StyleApplyInput, options?: MutationOptions): TextMutationReceipt; - align(input: FormatAlignInput, options?: MutationOptions): TextMutationReceipt; } // --------------------------------------------------------------------------- @@ -98,7 +81,6 @@ export type FormatInlineAliasApi = { export interface FormatApi extends FormatInlineAliasApi { strikethrough(input: FormatStrikethroughInput, options?: MutationOptions): TextMutationReceipt; apply(input: StyleApplyInput, options?: MutationOptions): TextMutationReceipt; - align(input: FormatAlignInput, options?: MutationOptions): TextMutationReceipt; } // --------------------------------------------------------------------------- @@ -217,35 +199,3 @@ function validateTarget(input: unknown, operation: string): asserts input is { t }); } } - -// --------------------------------------------------------------------------- -// 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/format/inline-run-patch.ts b/packages/document-api/src/format/inline-run-patch.ts index c95ed64f18..34e3dfd40a 100644 --- a/packages/document-api/src/format/inline-run-patch.ts +++ b/packages/document-api/src/format/inline-run-patch.ts @@ -87,6 +87,7 @@ export interface InlineRunPatch { snapToGrid?: boolean | null; oMath?: boolean | null; fontSize?: number | null; + fontFamily?: string | null; fontSizeCs?: number | null; letterSpacing?: number | null; charScale?: number | null; @@ -270,6 +271,7 @@ export const INLINE_PROPERTY_REGISTRY = [ }, markTextStyleValue('color', 'string', 'w:color', schemaStringOrNull()), markTextStyleValue('fontSize', 'number', 'w:sz', schemaNumberOrNull()), + markTextStyleValue('fontFamily', 'string', 'w:rFonts', schemaStringOrNull()), markTextStyleValue('letterSpacing', 'number', 'w:spacing', schemaNumberOrNull()), markTextStyleValue('vertAlign', 'string', 'w:vertAlign', { oneOf: [{ enum: ['superscript', 'subscript', 'baseline'] }, { type: 'null' }], @@ -592,6 +594,7 @@ function validateInlineProperty(key: string, value: unknown): void { return; case 'highlight': case 'color': + case 'fontFamily': case 'rStyle': case 'em': case 'ligatures': diff --git a/packages/document-api/src/index.test.ts b/packages/document-api/src/index.test.ts index 31007bdb98..f092b204d2 100644 --- a/packages/document-api/src/index.test.ts +++ b/packages/document-api/src/index.test.ts @@ -120,7 +120,6 @@ function makeFormatReceipt() { function makeFormatAdapter(): FormatAdapter { return { apply: vi.fn(() => makeFormatReceipt()), - align: vi.fn(() => makeFormatReceipt()), }; } @@ -655,7 +654,7 @@ describe('createDocumentApi', () => { ); }); - it('delegates format.align to adapter.align', () => { + it('delegates format.fontFamily to adapter.apply with inline.fontFamily', () => { const formatAdpt = makeFormatAdapter(); const api = createDocumentApi({ find: makeFindAdapter(QUERY_RESULT), @@ -671,9 +670,9 @@ describe('createDocumentApi', () => { }); 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' }, + api.format.fontFamily({ target, value: 'Arial' }); + expect(formatAdpt.apply).toHaveBeenCalledWith( + { target, inline: { fontFamily: 'Arial' } }, { changeMode: 'direct', dryRun: false }, ); }); diff --git a/packages/document-api/src/index.ts b/packages/document-api/src/index.ts index 99a6d0a6fb..cbc54b69de 100644 --- a/packages/document-api/src/index.ts +++ b/packages/document-api/src/index.ts @@ -52,9 +52,8 @@ import type { FormatInlineAliasInput, FormatStrikethroughInput, StyleApplyInput, - FormatAlignInput, } from './format/format.js'; -import { executeStyleApply, executeInlineAlias, executeAlign } from './format/format.js'; +import { executeStyleApply, executeInlineAlias } from './format/format.js'; import { INLINE_PROPERTY_REGISTRY, type InlineRunPatchKey } from './format/inline-run-patch.js'; import type { StylesAdapter, @@ -175,6 +174,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, @@ -252,9 +297,7 @@ export type { FormatStrikethroughInput, StyleApplyInput, StyleApplyOptions, - FormatAlignInput, } from './format/format.js'; -export { ALIGNMENTS, type Alignment } from './format/format.js'; export type { InlineRunPatch, InlineRunPatchKey, @@ -342,6 +385,48 @@ export type { } from './toc/toc.types.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, @@ -555,13 +640,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). */ @@ -634,6 +719,7 @@ export interface DocumentApiAdapters { blocks: BlocksAdapter; lists: ListsAdapter; sections: SectionsAdapter; + paragraphs: ParagraphsAdapter; tables: TablesAdapter; toc: TocAdapter; query: QueryAdapter; @@ -722,14 +808,75 @@ export function createDocumentApi(adapters: DocumentApiAdapters): DocumentApi { apply(input: StyleApplyInput, options?: MutationOptions): TextMutationReceipt { return executeStyleApply(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 1493955074..f970464755 100644 --- a/packages/document-api/src/invoke/invoke.test.ts +++ b/packages/document-api/src/invoke/invoke.test.ts @@ -94,7 +94,6 @@ function makeAdapters() { }); const formatAdapter: FormatAdapter = { apply: vi.fn(formatReceipt), - align: vi.fn(formatReceipt), }; const stylesAdapter: StylesAdapter = { apply: vi.fn(() => ({ @@ -308,15 +307,15 @@ describe('invoke', () => { expect(invoked).toEqual(direct); }); - it('format.align: invoke returns same result as direct call', () => { + it('format.fontFamily: 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, + value: 'Arial', }; - const direct = api.format.align(input); - const invoked = api.invoke({ operationId: 'format.align', input }); + const direct = api.format.fontFamily(input); + const invoked = api.invoke({ operationId: 'format.fontFamily', input }); expect(invoked).toEqual(direct); }); diff --git a/packages/document-api/src/invoke/invoke.ts b/packages/document-api/src/invoke/invoke.ts index 5c0b0864ce..6b7c771922 100644 --- a/packages/document-api/src/invoke/invoke.ts +++ b/packages/document-api/src/invoke/invoke.ts @@ -76,7 +76,29 @@ export function buildDispatchTable(api: DocumentApi): TypedDispatchTable { // --- format.* --- 'format.apply': (input, options) => api.format.apply(input, options), ...formatInlineAliasDispatch, - '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/overview-examples.test.ts b/packages/document-api/src/overview-examples.test.ts index cdf6a80518..cfe2dca230 100644 --- a/packages/document-api/src/overview-examples.test.ts +++ b/packages/document-api/src/overview-examples.test.ts @@ -78,7 +78,36 @@ function makeWriteAdapter() { function makeFormatAdapter() { return { apply: vi.fn(() => makeTextMutationReceipt()), - align: vi.fn(() => makeTextMutationReceipt()), + }; +} + +function makeParagraphsAdapter() { + const ok = () => ({ + success: true as const, + target: { kind: 'block' as const, nodeType: 'paragraph' as const, nodeId: 'p1' }, + resolution: { target: { kind: 'block' as const, nodeType: 'paragraph' as const, nodeId: 'p1' } }, + }); + + return { + setStyle: vi.fn(ok), + clearStyle: vi.fn(ok), + resetDirectFormatting: vi.fn(ok), + setAlignment: vi.fn(ok), + clearAlignment: vi.fn(ok), + setIndentation: vi.fn(ok), + clearIndentation: vi.fn(ok), + setSpacing: vi.fn(ok), + clearSpacing: vi.fn(ok), + setKeepOptions: vi.fn(ok), + setOutlineLevel: vi.fn(ok), + setFlowOptions: vi.fn(ok), + setTabStop: vi.fn(ok), + clearTabStop: vi.fn(ok), + clearAllTabStops: vi.fn(ok), + setBorder: vi.fn(ok), + clearBorder: vi.fn(ok), + setShading: vi.fn(ok), + clearShading: vi.fn(ok), }; } @@ -262,6 +291,7 @@ function makeApi() { comments: makeCommentsAdapter(), write: makeWriteAdapter(), format: makeFormatAdapter(), + paragraphs: makeParagraphsAdapter(), trackChanges: makeTrackChangesAdapter(), create: makeCreateAdapter(), lists: makeListsAdapter(), 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 163fa1b3c7..30f68ccbd6 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 @@ -19,7 +19,28 @@ import { ListHelpers } from '../../core/helpers/list-numbering-helpers.js'; import { createCommentsWrapper } from '../plan-engine/comments-wrappers.js'; import { createParagraphWrapper, createHeadingWrapper } from '../plan-engine/create-wrappers.js'; import { blocksDeleteWrapper } from '../plan-engine/blocks-wrappers.js'; -import { styleApplyWrapper, formatAlignWrapper } from '../plan-engine/plan-wrappers.js'; +import { styleApplyWrapper } from '../plan-engine/plan-wrappers.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 { stylesApplyAdapter } from '../styles-adapter.js'; import { createTableWrapper } from '../plan-engine/create-table-wrapper.js'; import { @@ -1293,6 +1314,500 @@ const formatInlineDryRunVectors = Object.fromEntries( }), ) as Partial unknown>>; +const PARAGRAPH_TARGET = { kind: 'block', nodeType: 'paragraph', nodeId: 'p1' } as const; +const MISSING_PARAGRAPH_TARGET = { kind: 'block', nodeType: 'paragraph', nodeId: 'missing' } as const; + +function makeParagraphEditor(paragraphProperties: Record = {}) { + const { editor, dispatch, tr } = makeTextEditor(); + const transaction = tr as unknown as { setNodeMarkup?: ReturnType }; + transaction.setNodeMarkup = vi.fn().mockReturnValue(tr); + + const paragraphNode = { + attrs: { + sdBlockId: 'p1', + paragraphProperties, + }, + }; + + ( + editor.state.doc as unknown as { + nodeAt: ReturnType; + } + ).nodeAt = vi.fn((pos: number) => (pos === 0 ? paragraphNode : null)); + + return { editor, dispatch }; +} + +const paragraphMutationVectors: Partial> = { + 'styles.paragraph.setStyle': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetStyleWrapper(editor, { target: MISSING_PARAGRAPH_TARGET, styleId: 'Normal' }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ styleId: 'Normal' }); + return paragraphsSetStyleWrapper(editor, { target: PARAGRAPH_TARGET, styleId: 'Normal' }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetStyleWrapper(editor, { target: PARAGRAPH_TARGET, styleId: 'Normal' }); + }, + }, + 'styles.paragraph.clearStyle': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearStyleWrapper(editor, { target: MISSING_PARAGRAPH_TARGET }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearStyleWrapper(editor, { target: PARAGRAPH_TARGET }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor({ styleId: 'Normal' }); + return paragraphsClearStyleWrapper(editor, { target: PARAGRAPH_TARGET }); + }, + }, + 'format.paragraph.resetDirectFormatting': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsResetDirectFormattingWrapper(editor, { target: MISSING_PARAGRAPH_TARGET }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsResetDirectFormattingWrapper(editor, { target: PARAGRAPH_TARGET }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor({ justification: 'center', styleId: 'Normal' }); + return paragraphsResetDirectFormattingWrapper(editor, { target: PARAGRAPH_TARGET }); + }, + }, + 'format.paragraph.setAlignment': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetAlignmentWrapper(editor, { target: MISSING_PARAGRAPH_TARGET, alignment: 'center' }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ justification: 'center' }); + return paragraphsSetAlignmentWrapper(editor, { target: PARAGRAPH_TARGET, alignment: 'center' }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetAlignmentWrapper(editor, { target: PARAGRAPH_TARGET, alignment: 'center' }); + }, + }, + 'format.paragraph.clearAlignment': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearAlignmentWrapper(editor, { target: MISSING_PARAGRAPH_TARGET }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearAlignmentWrapper(editor, { target: PARAGRAPH_TARGET }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor({ justification: 'right' }); + return paragraphsClearAlignmentWrapper(editor, { target: PARAGRAPH_TARGET }); + }, + }, + 'format.paragraph.setIndentation': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetIndentationWrapper(editor, { target: MISSING_PARAGRAPH_TARGET, left: 720 }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ indent: { left: 720 } }); + return paragraphsSetIndentationWrapper(editor, { target: PARAGRAPH_TARGET, left: 720 }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetIndentationWrapper(editor, { target: PARAGRAPH_TARGET, left: 720 }); + }, + }, + 'format.paragraph.clearIndentation': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearIndentationWrapper(editor, { target: MISSING_PARAGRAPH_TARGET }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearIndentationWrapper(editor, { target: PARAGRAPH_TARGET }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor({ indent: { left: 720 } }); + return paragraphsClearIndentationWrapper(editor, { target: PARAGRAPH_TARGET }); + }, + }, + 'format.paragraph.setSpacing': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetSpacingWrapper(editor, { target: MISSING_PARAGRAPH_TARGET, before: 120 }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ spacing: { before: 120 } }); + return paragraphsSetSpacingWrapper(editor, { target: PARAGRAPH_TARGET, before: 120 }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetSpacingWrapper(editor, { target: PARAGRAPH_TARGET, before: 120, after: 120 }); + }, + }, + 'format.paragraph.clearSpacing': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearSpacingWrapper(editor, { target: MISSING_PARAGRAPH_TARGET }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearSpacingWrapper(editor, { target: PARAGRAPH_TARGET }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor({ spacing: { before: 120 } }); + return paragraphsClearSpacingWrapper(editor, { target: PARAGRAPH_TARGET }); + }, + }, + 'format.paragraph.setKeepOptions': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetKeepOptionsWrapper(editor, { target: MISSING_PARAGRAPH_TARGET, keepNext: true }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ keepNext: true }); + return paragraphsSetKeepOptionsWrapper(editor, { target: PARAGRAPH_TARGET, keepNext: true }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetKeepOptionsWrapper(editor, { target: PARAGRAPH_TARGET, keepNext: true }); + }, + }, + 'format.paragraph.setOutlineLevel': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetOutlineLevelWrapper(editor, { target: MISSING_PARAGRAPH_TARGET, outlineLevel: 2 }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ outlineLvl: 2 }); + return paragraphsSetOutlineLevelWrapper(editor, { target: PARAGRAPH_TARGET, outlineLevel: 2 }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetOutlineLevelWrapper(editor, { target: PARAGRAPH_TARGET, outlineLevel: 2 }); + }, + }, + 'format.paragraph.setFlowOptions': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetFlowOptionsWrapper(editor, { target: MISSING_PARAGRAPH_TARGET, contextualSpacing: true }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ contextualSpacing: true }); + return paragraphsSetFlowOptionsWrapper(editor, { target: PARAGRAPH_TARGET, contextualSpacing: true }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetFlowOptionsWrapper(editor, { target: PARAGRAPH_TARGET, contextualSpacing: true }); + }, + }, + 'format.paragraph.setTabStop': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetTabStopWrapper(editor, { + target: MISSING_PARAGRAPH_TARGET, + position: 720, + alignment: 'left', + }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ tabStops: [{ tab: { pos: 720, tabType: 'left' } }] }); + return paragraphsSetTabStopWrapper(editor, { target: PARAGRAPH_TARGET, position: 720, alignment: 'left' }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetTabStopWrapper(editor, { target: PARAGRAPH_TARGET, position: 720, alignment: 'left' }); + }, + }, + 'format.paragraph.clearTabStop': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearTabStopWrapper(editor, { target: MISSING_PARAGRAPH_TARGET, position: 720 }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearTabStopWrapper(editor, { target: PARAGRAPH_TARGET, position: 720 }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor({ tabStops: [{ tab: { pos: 720, tabType: 'left' } }] }); + return paragraphsClearTabStopWrapper(editor, { target: PARAGRAPH_TARGET, position: 720 }); + }, + }, + 'format.paragraph.clearAllTabStops': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearAllTabStopsWrapper(editor, { target: MISSING_PARAGRAPH_TARGET }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearAllTabStopsWrapper(editor, { target: PARAGRAPH_TARGET }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor({ tabStops: [{ tab: { pos: 720, tabType: 'left' } }] }); + return paragraphsClearAllTabStopsWrapper(editor, { target: PARAGRAPH_TARGET }); + }, + }, + 'format.paragraph.setBorder': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetBorderWrapper(editor, { + target: MISSING_PARAGRAPH_TARGET, + side: 'top', + style: 'single', + }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ borders: { top: { val: 'single' } } }); + return paragraphsSetBorderWrapper(editor, { target: PARAGRAPH_TARGET, side: 'top', style: 'single' }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetBorderWrapper(editor, { target: PARAGRAPH_TARGET, side: 'top', style: 'single' }); + }, + }, + 'format.paragraph.clearBorder': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearBorderWrapper(editor, { target: MISSING_PARAGRAPH_TARGET, side: 'top' }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearBorderWrapper(editor, { target: PARAGRAPH_TARGET, side: 'top' }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor({ borders: { top: { val: 'single' } } }); + return paragraphsClearBorderWrapper(editor, { target: PARAGRAPH_TARGET, side: 'top' }); + }, + }, + 'format.paragraph.setShading': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetShadingWrapper(editor, { target: MISSING_PARAGRAPH_TARGET, fill: 'FFFF00' }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor({ shading: { fill: 'FFFF00' } }); + return paragraphsSetShadingWrapper(editor, { target: PARAGRAPH_TARGET, fill: 'FFFF00' }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsSetShadingWrapper(editor, { target: PARAGRAPH_TARGET, fill: 'FFFF00' }); + }, + }, + 'format.paragraph.clearShading': { + throwCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearShadingWrapper(editor, { target: MISSING_PARAGRAPH_TARGET }); + }, + failureCase: () => { + const { editor } = makeParagraphEditor(); + return paragraphsClearShadingWrapper(editor, { target: PARAGRAPH_TARGET }); + }, + applyCase: () => { + const { editor } = makeParagraphEditor({ shading: { fill: 'FFFF00' } }); + return paragraphsClearShadingWrapper(editor, { target: PARAGRAPH_TARGET }); + }, + }, +}; + +const paragraphDryRunVectors: Partial unknown>> = { + 'styles.paragraph.setStyle': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsSetStyleWrapper( + editor, + { target: PARAGRAPH_TARGET, styleId: 'Normal' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'styles.paragraph.clearStyle': () => { + const { editor, dispatch } = makeParagraphEditor({ styleId: 'Normal' }); + const result = paragraphsClearStyleWrapper( + editor, + { target: PARAGRAPH_TARGET }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.resetDirectFormatting': () => { + const { editor, dispatch } = makeParagraphEditor({ styleId: 'Normal', justification: 'center' }); + const result = paragraphsResetDirectFormattingWrapper( + editor, + { target: PARAGRAPH_TARGET }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.setAlignment': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsSetAlignmentWrapper( + editor, + { target: PARAGRAPH_TARGET, alignment: 'center' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.clearAlignment': () => { + const { editor, dispatch } = makeParagraphEditor({ justification: 'right' }); + const result = paragraphsClearAlignmentWrapper( + editor, + { target: PARAGRAPH_TARGET }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.setIndentation': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsSetIndentationWrapper( + editor, + { target: PARAGRAPH_TARGET, left: 720 }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.clearIndentation': () => { + const { editor, dispatch } = makeParagraphEditor({ indent: { left: 720 } }); + const result = paragraphsClearIndentationWrapper( + editor, + { target: PARAGRAPH_TARGET }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.setSpacing': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsSetSpacingWrapper( + editor, + { target: PARAGRAPH_TARGET, before: 120 }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.clearSpacing': () => { + const { editor, dispatch } = makeParagraphEditor({ spacing: { before: 120 } }); + const result = paragraphsClearSpacingWrapper( + editor, + { target: PARAGRAPH_TARGET }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.setKeepOptions': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsSetKeepOptionsWrapper( + editor, + { target: PARAGRAPH_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: PARAGRAPH_TARGET, outlineLevel: 2 }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.setFlowOptions': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsSetFlowOptionsWrapper( + editor, + { target: PARAGRAPH_TARGET, contextualSpacing: true }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.setTabStop': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsSetTabStopWrapper( + editor, + { target: PARAGRAPH_TARGET, position: 720, alignment: 'left' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.clearTabStop': () => { + const { editor, dispatch } = makeParagraphEditor({ tabStops: [{ tab: { pos: 720, tabType: 'left' } }] }); + const result = paragraphsClearTabStopWrapper( + editor, + { target: PARAGRAPH_TARGET, position: 720 }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.clearAllTabStops': () => { + const { editor, dispatch } = makeParagraphEditor({ tabStops: [{ tab: { pos: 720, tabType: 'left' } }] }); + const result = paragraphsClearAllTabStopsWrapper( + editor, + { target: PARAGRAPH_TARGET }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.setBorder': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsSetBorderWrapper( + editor, + { target: PARAGRAPH_TARGET, side: 'top', style: 'single' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.clearBorder': () => { + const { editor, dispatch } = makeParagraphEditor({ borders: { top: { val: 'single' } } }); + const result = paragraphsClearBorderWrapper( + editor, + { target: PARAGRAPH_TARGET, side: 'top' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.setShading': () => { + const { editor, dispatch } = makeParagraphEditor(); + const result = paragraphsSetShadingWrapper( + editor, + { target: PARAGRAPH_TARGET, fill: 'FFFF00' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'format.paragraph.clearShading': () => { + const { editor, dispatch } = makeParagraphEditor({ shading: { fill: 'FFFF00' } }); + const result = paragraphsClearShadingWrapper( + editor, + { target: PARAGRAPH_TARGET }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, +}; + function makeTocEditor(commandOverrides: Record = {}): Editor { const tocParagraph = createNode('paragraph', [createNode('text', [], { text: 'TOC entry' })], { attrs: { sdBlockId: 'toc-entry-p1' }, @@ -1467,32 +1982,7 @@ const mutationVectors: Partial> = { }, }, ...formatInlineMutationVectors, - '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' }, - ); - }, - }, + ...paragraphMutationVectors, 'create.paragraph': { throwCase: () => { const { editor } = makeTextEditor('Hello', { commands: { insertParagraphAt: undefined } }); @@ -3101,16 +3591,7 @@ const dryRunVectors: Partial unknown>> = { return result; }, ...formatInlineDryRunVectors, - '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; - }, + ...paragraphDryRunVectors, 'create.paragraph': () => { const insertParagraphAt = vi.fn(() => true); const { editor } = makeTextEditor('Hello', { commands: { insertParagraphAt } }); @@ -3932,7 +4413,7 @@ describe('document-api adapter conformance', () => { const vector = mutationVectors[operationId]; expect(typeof vector?.failureCase, `${operationId} is missing failureCase`).toBe('function'); const result = vector!.failureCase!() as { success?: boolean; failure?: { code: string } }; - expect(result.success).toBe(false); + expect(result.success, `${operationId} failureCase should return success=false`).toBe(false); if (result.success !== false || !result.failure) continue; expect(COMMAND_CATALOG[operationId].possibleFailureCodes).toContain(result.failure.code); assertSchema(operationId, 'output', result); 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 9fb72fc26b..41d827e9b9 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 @@ -24,7 +24,25 @@ describe('assembleDocumentApiAdapters', () => { expect(adapters).toHaveProperty('comments'); expect(adapters).toHaveProperty('write.write'); expect(adapters).toHaveProperty('format.apply'); - expect(adapters).toHaveProperty('format.align'); + 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'); expect(adapters).toHaveProperty('trackChanges.list'); expect(adapters).toHaveProperty('trackChanges.get'); expect(adapters).toHaveProperty('trackChanges.accept'); @@ -77,7 +95,9 @@ describe('assembleDocumentApiAdapters', () => { expect(typeof adapters.find.find).toBe('function'); expect(typeof adapters.write.write).toBe('function'); expect(typeof adapters.format.apply).toBe('function'); - expect(typeof adapters.format.align).toBe('function'); + expect(typeof adapters.paragraphs.setStyle).toBe('function'); + expect(typeof adapters.paragraphs.setAlignment).toBe('function'); + expect(typeof adapters.paragraphs.setBorder).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 b9f4a9619d..d29e18ff6f 100644 --- a/packages/super-editor/src/document-api-adapters/assemble-adapters.ts +++ b/packages/super-editor/src/document-api-adapters/assemble-adapters.ts @@ -6,13 +6,29 @@ import { getTextAdapter } from './get-text-adapter.js'; import { infoAdapter } from './info-adapter.js'; import { getDocumentApiCapabilities } from './capabilities-adapter.js'; import { createCommentsWrapper } from './plan-engine/comments-wrappers.js'; -import { - writeWrapper, - insertStructuredWrapper, - styleApplyWrapper, - formatAlignWrapper, -} from './plan-engine/plan-wrappers.js'; +import { writeWrapper, insertStructuredWrapper, styleApplyWrapper } from './plan-engine/plan-wrappers.js'; import { stylesApplyAdapter } from './styles-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 { trackChangesListWrapper, trackChangesGetWrapper, @@ -143,11 +159,31 @@ export function assembleDocumentApiAdapters(editor: Editor): DocumentApiAdapters }, format: { apply: (input, options) => styleApplyWrapper(editor, input, options), - align: (input, options) => formatAlignWrapper(editor, input, options), }, styles: { apply: (input, options) => stylesApplyAdapter(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), + }, trackChanges: { list: (input) => trackChangesListWrapper(editor, input), get: (input) => trackChangesGetWrapper(editor, input), 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 a329bffed1..4086065e68 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.apply / format.align capability reporting + // format.apply / format. capability reporting // --------------------------------------------------------------------------- describe('format capabilities', () => { @@ -272,8 +272,6 @@ describe('getDocumentApiCapabilities', () => { ) { return makeEditor({ commands: { - setTextAlign: vi.fn(() => true), - unsetTextAlign: vi.fn(() => true), ...overrides.commands, } as unknown as Editor['commands'], schema: { @@ -348,19 +346,6 @@ describe('getDocumentApiCapabilities', () => { expect(capabilities.operations['format.apply'].tracked).toBe(false); }); - 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); - expect(capabilities.operations['format.align'].dryRun).toBe(true); - expect(capabilities.operations['format.align'].tracked).toBe(false); - }); - - 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'); - }); - // ----------------------------------------------------------------------- // format. operation-level capability parity // ----------------------------------------------------------------------- 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 a5e8dd03bc..12f5ad1ebd 100644 --- a/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts +++ b/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts @@ -30,7 +30,6 @@ type EditorWithBlockNodeHelper = Editor & { // they are backed by writeAdapter which is always available when the editor exists. // Read-only operations (find, getNode, getText, info, etc.) similarly need no commands. const REQUIRED_COMMANDS: Partial> = { - 'format.align': ['setTextSelection', 'setTextAlign', 'unsetTextAlign'], 'create.paragraph': ['insertParagraphAt'], 'create.heading': ['insertHeadingAt'], 'lists.insert': ['insertListItemAt'], diff --git a/packages/super-editor/src/document-api-adapters/helpers/node-info-mapper.ts b/packages/super-editor/src/document-api-adapters/helpers/node-info-mapper.ts index 6a3aa179ad..7b6d8e7fc7 100644 --- a/packages/super-editor/src/document-api-adapters/helpers/node-info-mapper.ts +++ b/packages/super-editor/src/document-api-adapters/helpers/node-info-mapper.ts @@ -43,7 +43,7 @@ function resolveMeasurement(value: number | TableMeasurement | null | undefined) } function mapTableAlignment( - justification: TableAttrs['tableProperties'] extends { justification?: infer J } ? J : never, + justification: NonNullable['justification'], ): TableNodeInfo['properties']['alignment'] { switch (justification) { case 'start': @@ -61,12 +61,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; @@ -94,7 +94,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/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 new file mode 100644 index 0000000000..4c3e0b5a7b --- /dev/null +++ b/packages/super-editor/src/document-api-adapters/plan-engine/paragraphs-wrappers.ts @@ -0,0 +1,643 @@ +/** + * 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; +} + +/** + * Merges indentation fields while enforcing OOXML exclusivity: + * `firstLine` and `hanging` cannot co-exist on the same paragraph. + */ +function mergeIndentationFields(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 }; + 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: mergeIndentationFields(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/document-api-adapters/plan-engine/plan-wrappers.ts b/packages/super-editor/src/document-api-adapters/plan-engine/plan-wrappers.ts index 10dca56b71..b86aec2c20 100644 --- a/packages/super-editor/src/document-api-adapters/plan-engine/plan-wrappers.ts +++ b/packages/super-editor/src/document-api-adapters/plan-engine/plan-wrappers.ts @@ -16,7 +16,6 @@ import type { TextMutationResolution, WriteRequest, StyleApplyInput, - FormatAlignInput, InlineRunPatchKey, PlanReceipt, ReceiptFailure, @@ -503,57 +502,6 @@ export function styleApplyWrapper( return mapPlanReceiptToTextReceipt(receipt, resolved.resolution); } -export function formatAlignWrapper( - editor: Editor, - input: FormatAlignInput, - options?: MutationOptions, -): TextMutationReceipt { - const operation = 'format.align'; - rejectTrackedMode(operation, options); - - const normalizedInput = normalizeFormatLocator(input); - const resolved = resolveFormatTarget(editor, normalizedInput.target!, operation); - - 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.range.from, to: resolved.range.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 }; -} - // --------------------------------------------------------------------------- // Structured content insertion (markdown / html) // --------------------------------------------------------------------------- diff --git a/packages/super-editor/src/extensions/index.js b/packages/super-editor/src/extensions/index.js index 565662e1db..869d43dbc9 100644 --- a/packages/super-editor/src/extensions/index.js +++ b/packages/super-editor/src/extensions/index.js @@ -91,6 +91,8 @@ const getRichTextExtensions = () => { Italic, Link, Paragraph, + TableOfContents, + DocumentIndex, Strike, Text, TextAlign, diff --git a/packages/super-editor/src/extensions/paragraph/helpers/parseAttrs.test.js b/packages/super-editor/src/extensions/paragraph/helpers/parseAttrs.test.js index 28c83c0727..539813d153 100644 --- a/packages/super-editor/src/extensions/paragraph/helpers/parseAttrs.test.js +++ b/packages/super-editor/src/extensions/paragraph/helpers/parseAttrs.test.js @@ -94,7 +94,7 @@ describe('parseAttrs', () => { const node = createMockNode({}, { marginTop: '16px' }); const result = parseAttrs(node); // 16px / 1.333 = ~12pt, * 20 = ~240 twips - const expectedPt = 16 * 72 / 96; + const expectedPt = (16 * 72) / 96; expect(result.paragraphProperties.spacing.before).toBe(Math.round(expectedPt * 20)); }); @@ -108,7 +108,7 @@ describe('parseAttrs', () => { it('extracts marginLeft in px and converts to twips', () => { const node = createMockNode({}, { marginLeft: '48px' }); const result = parseAttrs(node); - const expectedPt = 48 * 72 / 96; + const expectedPt = (48 * 72) / 96; expect(result.paragraphProperties.indent.left).toBe(Math.round(expectedPt * 20)); }); @@ -133,7 +133,7 @@ describe('parseAttrs', () => { const node = createMockNode({}, { lineHeight: '24px' }); const result = parseAttrs(node); // 24px / 1.333 ≈ 18pt, * 20 = 360 twips - expect(result.paragraphProperties.spacing.line).toBe(Math.round((24 * 72 / 96) * 20)); + expect(result.paragraphProperties.spacing.line).toBe(Math.round(((24 * 72) / 96) * 20)); expect(result.paragraphProperties.spacing.lineRule).toBe('exact'); }); 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 1bc28c9cf9..90576979f2 100644 --- a/tests/doc-api-stories/tests/formatting/inline-formatting.ts +++ b/tests/doc-api-stories/tests/formatting/inline-formatting.ts @@ -15,7 +15,7 @@ import { unwrap, useStoryHarness } from '../harness'; * * Covered operations: * format.apply — boolean marks and value/object inline run patches - * format.align — paragraph-level alignment (center, right, justify) + * format.paragraph.setAlignment — paragraph-level alignment (center, right, justify) */ describe('document-api story: inline formatting', () => { const { client, outPath } = useStoryHarness('formatting/inline-formatting', { @@ -53,6 +53,16 @@ describe('document-api story: inline formatting', () => { }; } + /** Resolves the paragraph target associated with the inserted text range. */ + async function setupFormattableParagraph(sessionId: string, text: string) { + const textTarget = await setupFormattableText(sessionId, text); + return { + kind: 'block' as const, + nodeType: 'paragraph' as const, + nodeId: textTarget.blockId, + }; + } + /** Export the session's working doc to the results directory. */ async function saveResult(sessionId: string, docName: string) { await client.doc.save({ sessionId, out: outPath(docName) }); @@ -165,35 +175,41 @@ describe('document-api story: inline formatting', () => { }); // --------------------------------------------------------------------------- - // format.align (paragraph-level) + // format.paragraph.setAlignment (paragraph-level) // --------------------------------------------------------------------------- it('align center: centers the paragraph', async () => { const sid = `align-center-${Date.now()}`; - const target = await setupFormattableText(sid, 'This paragraph should be centered'); + const target = await setupFormattableParagraph(sid, 'This paragraph should be centered'); - const result = unwrap(await client.doc.format.align({ sessionId: sid, target, alignment: 'center' })); + const result = unwrap( + await client.doc.format.paragraph.setAlignment({ sessionId: sid, target, alignment: 'center' }), + ); expect(result.receipt?.success).toBe(true); await saveResult(sid, 'align-center.docx'); }); it('align right: right-aligns the paragraph', async () => { const sid = `align-right-${Date.now()}`; - const target = await setupFormattableText(sid, 'This paragraph should be right-aligned'); + const target = await setupFormattableParagraph(sid, 'This paragraph should be right-aligned'); - const result = unwrap(await client.doc.format.align({ sessionId: sid, target, alignment: 'right' })); + const result = unwrap( + await client.doc.format.paragraph.setAlignment({ sessionId: sid, target, alignment: 'right' }), + ); expect(result.receipt?.success).toBe(true); await saveResult(sid, 'align-right.docx'); }); it('align justify: justifies the paragraph', async () => { const sid = `align-justify-${Date.now()}`; - const target = await setupFormattableText( + const target = await setupFormattableParagraph( sid, 'This paragraph should be fully justified so that both the left and right edges align neatly. When the text is long enough to wrap across several lines, justified alignment becomes visually obvious because each line stretches to fill the full width of the page, distributing extra space evenly between words.', ); - const result = unwrap(await client.doc.format.align({ sessionId: sid, target, alignment: 'justify' })); + const result = unwrap( + await client.doc.format.paragraph.setAlignment({ sessionId: sid, target, alignment: 'justify' }), + ); expect(result.receipt?.success).toBe(true); await saveResult(sid, 'align-justify.docx'); });