diff --git a/apps/cli/scripts/export-sdk-contract.ts b/apps/cli/scripts/export-sdk-contract.ts index e8c15a0430..b57505253f 100644 --- a/apps/cli/scripts/export-sdk-contract.ts +++ b/apps/cli/scripts/export-sdk-contract.ts @@ -16,7 +16,7 @@ import { resolve, dirname } from 'node:path'; import { createHash } from 'node:crypto'; import { tmpdir } from 'node:os'; -import { COMMAND_CATALOG } from '@superdoc/document-api'; +import { COMMAND_CATALOG, INLINE_PROPERTY_REGISTRY } from '@superdoc/document-api'; import { CLI_OPERATION_METADATA } from '../src/cli/operation-params'; import { @@ -60,9 +60,12 @@ const INTENT_NAMES = { 'doc.delete': 'delete_content', 'doc.blocks.delete': 'delete_block', 'doc.format.apply': 'format_apply', - 'doc.format.fontSize': 'format_font_size', - 'doc.format.fontFamily': 'format_font_family', - 'doc.format.color': 'format_color', + ...Object.fromEntries( + INLINE_PROPERTY_REGISTRY.map((entry) => [ + `doc.format.${entry.key}`, + `format_${entry.key.replace(/[A-Z]/g, (char) => `_${char.toLowerCase()}`)}`, + ]), + ), 'doc.format.align': 'format_align', 'doc.styles.apply': 'styles_apply', 'doc.create.paragraph': 'create_paragraph', diff --git a/apps/cli/src/__tests__/conformance/scenarios.ts b/apps/cli/src/__tests__/conformance/scenarios.ts index c6993055b7..7296d79bd9 100644 --- a/apps/cli/src/__tests__/conformance/scenarios.ts +++ b/apps/cli/src/__tests__/conformance/scenarios.ts @@ -1,6 +1,7 @@ import type { CliOperationId } from '../../cli'; import { CLI_OPERATION_COMMAND_KEYS } from '../../cli'; import type { ConformanceHarness } from './harness'; +import { INLINE_PROPERTY_REGISTRY } from '@superdoc/document-api'; export type ScenarioInvocation = { stateDir: string; @@ -138,6 +139,99 @@ function sectionMutationScenario( }; } +type InlineAliasKey = (typeof INLINE_PROPERTY_REGISTRY)[number]['key']; +type FormatInlineAliasCliOperationId = `doc.format.${InlineAliasKey}`; + +function sampleInlineAliasValue(key: InlineAliasKey): unknown { + switch (key) { + case 'underline': + return true; + case 'vertAlign': + return 'superscript'; + case 'shading': + return { fill: 'FFFF00' }; + case 'border': + return { val: 'single' }; + case 'fitText': + return { val: 12 }; + case 'lang': + return { val: 'en-US' }; + case 'rFonts': + return { ascii: 'Calibri', hAnsi: 'Calibri' }; + case 'eastAsianLayout': + return { vert: true }; + case 'stylisticSets': + return [{ id: 1, val: true }]; + case 'rStyle': + return 'DefaultParagraphFont'; + case 'color': + return '#FF0000'; + case 'highlight': + return 'yellow'; + case 'em': + return 'dot'; + case 'ligatures': + return 'standard'; + case 'numForm': + return 'lining'; + case 'numSpacing': + return 'proportional'; + case 'fontSize': + case 'fontSizeCs': + return 14; + case 'letterSpacing': + return 0.5; + case 'position': + return 1; + case 'charScale': + return 100; + case 'kerning': + return 8; + default: { + const entry = INLINE_PROPERTY_REGISTRY.find((candidate) => candidate.key === key); + if (!entry) throw new Error(`Unknown inline alias key: ${key}`); + if (entry.type === 'boolean') return true; + if (entry.type === 'number') return 1; + if (entry.type === 'string') return 'on'; + if (entry.type === 'array') return [{ id: 1, val: true }]; + return { val: 'on' }; + } + } +} + +function formatInlineAliasSuccessScenario( + operationId: FormatInlineAliasCliOperationId, +): (harness: ConformanceHarness) => Promise { + return async (harness: ConformanceHarness): Promise => { + const key = operationId.slice('doc.format.'.length) as InlineAliasKey; + const stateDir = await harness.createStateDir(`${operationId.replace(/\./g, '-')}-success`); + const docPath = await harness.copyFixtureDoc(`${operationId.replace(/\./g, '-')}`); + const target = await harness.firstTextRange(docPath, stateDir); + return { + stateDir, + args: [ + ...commandTokens(operationId), + docPath, + '--target-json', + JSON.stringify(target), + '--value-json', + JSON.stringify(sampleInlineAliasValue(key)), + '--out', + harness.createOutputPath(`${operationId.replace(/\./g, '-')}-output`), + ], + }; + }; +} + +const FORMAT_INLINE_ALIAS_SUCCESS_SCENARIOS: Record< + FormatInlineAliasCliOperationId, + (harness: ConformanceHarness) => Promise +> = Object.fromEntries( + INLINE_PROPERTY_REGISTRY.map((entry) => { + const operationId = `doc.format.${entry.key}` as FormatInlineAliasCliOperationId; + return [operationId, formatInlineAliasSuccessScenario(operationId)]; + }), +) as Record Promise>; // --------------------------------------------------------------------------- // Table scenario helpers (DRY builders for the 40 table operations) // --------------------------------------------------------------------------- @@ -988,69 +1082,13 @@ export const SUCCESS_SCENARIOS = { '--target-json', JSON.stringify(target), '--inline-json', - JSON.stringify({ bold: 'on' }), + JSON.stringify({ bold: true }), '--out', harness.createOutputPath('doc-style-apply-output'), ], }; }, - 'doc.format.fontSize': async (harness: ConformanceHarness): Promise => { - const stateDir = await harness.createStateDir('doc-format-font-size-success'); - const docPath = await harness.copyFixtureDoc('doc-format-font-size'); - const target = await harness.firstTextRange(docPath, stateDir); - return { - stateDir, - args: [ - 'format', - 'font-size', - docPath, - '--target-json', - JSON.stringify(target), - '--value-json', - JSON.stringify('14pt'), - '--out', - harness.createOutputPath('doc-format-font-size-output'), - ], - }; - }, - 'doc.format.fontFamily': async (harness: ConformanceHarness): Promise => { - const stateDir = await harness.createStateDir('doc-format-font-family-success'); - const docPath = await harness.copyFixtureDoc('doc-format-font-family'); - const target = await harness.firstTextRange(docPath, stateDir); - return { - stateDir, - args: [ - 'format', - 'font-family', - docPath, - '--target-json', - JSON.stringify(target), - '--value-json', - JSON.stringify('Arial'), - '--out', - harness.createOutputPath('doc-format-font-family-output'), - ], - }; - }, - 'doc.format.color': async (harness: ConformanceHarness): Promise => { - const stateDir = await harness.createStateDir('doc-format-color-success'); - const docPath = await harness.copyFixtureDoc('doc-format-color'); - const target = await harness.firstTextRange(docPath, stateDir); - return { - stateDir, - args: [ - 'format', - 'color', - docPath, - '--target-json', - JSON.stringify(target), - '--value-json', - JSON.stringify('#ff0000'), - '--out', - harness.createOutputPath('doc-format-color-output'), - ], - }; - }, + ...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'); diff --git a/apps/cli/src/cli/helper-commands.ts b/apps/cli/src/cli/helper-commands.ts index 6a7de503b7..a3b21ac399 100644 --- a/apps/cli/src/cli/helper-commands.ts +++ b/apps/cli/src/cli/helper-commands.ts @@ -44,46 +44,14 @@ function mapIdToTarget(input: Record): Record } /** - * Format helper commands — map `format ` to `format.apply` with pre-filled inline styles. - * These keep `superdoc format bold|italic|underline|strikethrough` as ergonomic - * shortcuts over the canonical `format.apply` contract operation. + * Helper commands for compatibility/ergonomics where no direct canonical key exists. */ export const CLI_HELPER_COMMANDS: readonly CliHelperCommand[] = [ - // --- Format helpers (route to format.apply) --- - { - tokens: ['format', 'bold'], - canonicalOperationId: 'format.apply', - defaultInput: { inline: { bold: 'on' } }, - description: 'Apply bold formatting to a text range.', - category: 'format', - mutates: true, - examples: [ - 'superdoc format bold --blockId p1 --start 0 --end 5', - 'superdoc format bold --target \'{"kind":"text","blockId":"p1","range":{"start":0,"end":5}}\'', - ], - }, - { - tokens: ['format', 'italic'], - canonicalOperationId: 'format.apply', - defaultInput: { inline: { italic: 'on' } }, - description: 'Apply italic formatting to a text range.', - category: 'format', - mutates: true, - examples: ['superdoc format italic --blockId p1 --start 0 --end 5'], - }, - { - tokens: ['format', 'underline'], - canonicalOperationId: 'format.apply', - defaultInput: { inline: { underline: 'on' } }, - description: 'Apply underline formatting to a text range.', - category: 'format', - mutates: true, - examples: ['superdoc format underline --blockId p1 --start 0 --end 5'], - }, + // --- Format helper --- { tokens: ['format', 'strikethrough'], - canonicalOperationId: 'format.apply', - defaultInput: { inline: { strike: 'on' } }, + canonicalOperationId: 'format.strike', + defaultInput: { value: true }, description: 'Apply strikethrough formatting to a text range.', category: 'format', mutates: true, diff --git a/apps/cli/src/cli/operation-hints.ts b/apps/cli/src/cli/operation-hints.ts index b002e3fdcf..78afc7b31b 100644 --- a/apps/cli/src/cli/operation-hints.ts +++ b/apps/cli/src/cli/operation-hints.ts @@ -12,6 +12,21 @@ import { COMMAND_CATALOG } from '@superdoc/document-api'; import type { CliExposedOperationId } from './operation-set.js'; +type FormatOperationId = Extract; +type FormatInlineAliasOperationId = Exclude; + +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', +); + +function buildFormatInlineAliasRecord(value: T): Record { + return Object.fromEntries(FORMAT_INLINE_ALIAS_OPERATION_IDS.map((operationId) => [operationId, value])) as Record< + FormatInlineAliasOperationId, + T + >; +} + // --------------------------------------------------------------------------- // Orchestration kind (derived from COMMAND_CATALOG) // --------------------------------------------------------------------------- @@ -37,10 +52,8 @@ export const SUCCESS_VERB: Record = { delete: 'deleted text', 'blocks.delete': 'deleted block', 'format.apply': 'applied style', - 'format.fontSize': 'set font size', - 'format.fontFamily': 'set font family', - 'format.color': 'set text color', 'format.align': 'set alignment', + ...buildFormatInlineAliasRecord('applied style'), 'styles.apply': 'applied stylesheet defaults', 'create.paragraph': 'created paragraph', 'create.heading': 'created heading', @@ -146,10 +159,8 @@ export const OUTPUT_FORMAT: Record = { delete: 'mutationReceipt', 'blocks.delete': 'plain', 'format.apply': 'mutationReceipt', - 'format.fontSize': 'mutationReceipt', - 'format.fontFamily': 'mutationReceipt', - 'format.color': 'mutationReceipt', 'format.align': 'mutationReceipt', + ...buildFormatInlineAliasRecord('mutationReceipt'), 'styles.apply': 'receipt', 'create.paragraph': 'createResult', 'create.heading': 'createResult', @@ -239,10 +250,8 @@ export const RESPONSE_ENVELOPE_KEY: Record delete: null, 'blocks.delete': 'result', 'format.apply': null, - 'format.fontSize': null, - 'format.fontFamily': null, - 'format.color': null, 'format.align': null, + ...buildFormatInlineAliasRecord(null), 'styles.apply': 'receipt', 'create.paragraph': 'result', 'create.heading': 'result', @@ -326,10 +335,8 @@ export const RESPONSE_VALIDATION_KEY: Partial = delete: 'textMutation', 'blocks.delete': 'blocks', 'format.apply': 'textMutation', - 'format.fontSize': 'textMutation', - 'format.fontFamily': 'textMutation', - 'format.color': 'textMutation', 'format.align': 'textMutation', + ...buildFormatInlineAliasRecord('textMutation'), 'styles.apply': 'general', 'create.paragraph': 'create', 'create.heading': 'create', diff --git a/apps/cli/src/cli/operation-params.ts b/apps/cli/src/cli/operation-params.ts index 15f615fb99..bc6541f55d 100644 --- a/apps/cli/src/cli/operation-params.ts +++ b/apps/cli/src/cli/operation-params.ts @@ -329,6 +329,10 @@ const LIST_TARGET_FLAT_PARAMS: CliOperationParamSpec[] = [ { name: 'nodeId', kind: 'flag', flag: 'node-id', type: 'string' }, ]; +const FORMAT_OPERATION_IDS = CLI_DOC_OPERATIONS.filter((operationId): operationId is OperationId => + operationId.startsWith('format.'), +); + const EXTRA_CLI_PARAMS: Partial> = { 'doc.find': [ { name: 'type', kind: 'flag', type: 'string' }, @@ -346,11 +350,6 @@ const EXTRA_CLI_PARAMS: Partial> = { 'doc.insert': [...INSERT_FLAT_PARAMS], 'doc.replace': [...TEXT_TARGET_FLAT_PARAMS], 'doc.delete': [...TEXT_TARGET_FLAT_PARAMS], - 'doc.format.apply': [...TEXT_TARGET_FLAT_PARAMS], - 'doc.format.fontSize': [...TEXT_TARGET_FLAT_PARAMS], - 'doc.format.fontFamily': [...TEXT_TARGET_FLAT_PARAMS], - 'doc.format.color': [...TEXT_TARGET_FLAT_PARAMS], - 'doc.format.align': [...TEXT_TARGET_FLAT_PARAMS], 'doc.styles.apply': [ { name: 'target', kind: 'jsonFlag', flag: 'target-json', type: 'json' }, { name: 'patch', kind: 'jsonFlag', flag: 'patch-json', type: 'json' }, @@ -387,6 +386,10 @@ const EXTRA_CLI_PARAMS: Partial> = { 'doc.create.heading': [{ name: 'input', kind: 'jsonFlag', flag: 'input-json', type: 'json' }], }; +for (const operationId of FORMAT_OPERATION_IDS) { + EXTRA_CLI_PARAMS[`doc.${operationId}`] = [...TEXT_TARGET_FLAT_PARAMS]; +} + // --------------------------------------------------------------------------- // Doc requirement derivation // --------------------------------------------------------------------------- diff --git a/apps/cli/src/lib/invoke-input.ts b/apps/cli/src/lib/invoke-input.ts index 80c66a7d83..d984df8c9e 100644 --- a/apps/cli/src/lib/invoke-input.ts +++ b/apps/cli/src/lib/invoke-input.ts @@ -12,7 +12,7 @@ * receives the correct input shape. */ -import type { CliExposedOperationId } from '../cli/operation-set.js'; +import { CLI_DOC_OPERATIONS, type CliExposedOperationId } from '../cli/operation-set.js'; /** * Operations whose API input is wrapped in a named field on the CLI input object. @@ -59,6 +59,10 @@ const CLI_LEVEL_KEYS = new Set(['doc', 'sessionId', 'out', 'dryRun', 'force', 'e */ const CHANGEMODE_IN_INPUT = new Set(['mutations.apply', 'mutations.preview']); +const FORMAT_TARGET_OPERATIONS = CLI_DOC_OPERATIONS.filter((operationId): operationId is CliExposedOperationId => + operationId.startsWith('format.'), +); + // --------------------------------------------------------------------------- // Flat-flag → canonical target normalization // --------------------------------------------------------------------------- @@ -73,11 +77,7 @@ const CHANGEMODE_IN_INPUT = new Set(['mutations.apply', ' const TEXT_TARGET_OPERATIONS = new Set([ 'replace', 'delete', - 'format.apply', - 'format.fontSize', - 'format.fontFamily', - 'format.color', - 'format.align', + ...FORMAT_TARGET_OPERATIONS, 'comments.create', 'comments.patch', ]); diff --git a/apps/cli/src/lib/special-handlers.ts b/apps/cli/src/lib/special-handlers.ts index fe37441e4b..b3f4727307 100644 --- a/apps/cli/src/lib/special-handlers.ts +++ b/apps/cli/src/lib/special-handlers.ts @@ -9,7 +9,7 @@ */ import { createHash } from 'node:crypto'; -import type { CliExposedOperationId } from '../cli/operation-set.js'; +import { CLI_DOC_OPERATIONS, type CliExposedOperationId } from '../cli/operation-set.js'; import type { EditorWithDoc } from './document.js'; // --------------------------------------------------------------------------- @@ -25,6 +25,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.'), +); + // --------------------------------------------------------------------------- // Track-changes stable-ID helpers // --------------------------------------------------------------------------- @@ -226,6 +230,10 @@ const flattenTextMutationReceipt: PostInvokeHook = (result) => { }; }; +const FORMAT_POST_INVOKE_HOOKS: Partial> = Object.fromEntries( + FORMAT_OPERATION_IDS.map((operationId) => [operationId, flattenTextMutationReceipt]), +) as Partial>; + /** Pre-invoke: custom input resolution before calling editor.doc.invoke(). */ export const PRE_INVOKE_HOOKS: Partial> = { // Track-changes get needs stable-ID → raw-ID translation @@ -243,11 +251,7 @@ export const POST_INVOKE_HOOKS: Partial { const record = asRecord(result); diff --git a/apps/docs/document-api/available-operations.mdx b/apps/docs/document-api/available-operations.mdx index 8cf00b3f6b..a254334efe 100644 --- a/apps/docs/document-api/available-operations.mdx +++ b/apps/docs/document-api/available-operations.mdx @@ -19,7 +19,7 @@ Use the tables below to see what operations are available and where each one is | Comments | 5 | 0 | 5 | [Reference](/document-api/reference/comments/index) | | Core | 8 | 0 | 8 | [Reference](/document-api/reference/core/index) | | Create | 4 | 0 | 4 | [Reference](/document-api/reference/create/index) | -| Format | 5 | 4 | 9 | [Reference](/document-api/reference/format/index) | +| Format | 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) | | Query | 1 | 0 | 1 | [Reference](/document-api/reference/query/index) | @@ -50,14 +50,50 @@ Use the tables below to see what operations are available and where each one is | editor.doc.create.sectionBreak(...) | [`create.sectionBreak`](/document-api/reference/create/section-break) | | editor.doc.create.table(...) | [`create.table`](/document-api/reference/create/table) | | editor.doc.format.apply(...) | [`format.apply`](/document-api/reference/format/apply) | -| 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.bold(...) | [`format.bold`](/document-api/reference/format/bold) | +| editor.doc.format.italic(...) | [`format.italic`](/document-api/reference/format/italic) | +| editor.doc.format.strike(...) | [`format.strike`](/document-api/reference/format/strike) | +| editor.doc.format.underline(...) | [`format.underline`](/document-api/reference/format/underline) | +| 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.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) | +| editor.doc.format.dstrike(...) | [`format.dstrike`](/document-api/reference/format/dstrike) | +| editor.doc.format.smallCaps(...) | [`format.smallCaps`](/document-api/reference/format/small-caps) | +| editor.doc.format.caps(...) | [`format.caps`](/document-api/reference/format/caps) | +| editor.doc.format.shading(...) | [`format.shading`](/document-api/reference/format/shading) | +| editor.doc.format.border(...) | [`format.border`](/document-api/reference/format/border) | +| editor.doc.format.outline(...) | [`format.outline`](/document-api/reference/format/outline) | +| editor.doc.format.shadow(...) | [`format.shadow`](/document-api/reference/format/shadow) | +| editor.doc.format.emboss(...) | [`format.emboss`](/document-api/reference/format/emboss) | +| editor.doc.format.imprint(...) | [`format.imprint`](/document-api/reference/format/imprint) | +| editor.doc.format.charScale(...) | [`format.charScale`](/document-api/reference/format/char-scale) | +| editor.doc.format.kerning(...) | [`format.kerning`](/document-api/reference/format/kerning) | +| editor.doc.format.vanish(...) | [`format.vanish`](/document-api/reference/format/vanish) | +| editor.doc.format.webHidden(...) | [`format.webHidden`](/document-api/reference/format/web-hidden) | +| editor.doc.format.specVanish(...) | [`format.specVanish`](/document-api/reference/format/spec-vanish) | +| editor.doc.format.rtl(...) | [`format.rtl`](/document-api/reference/format/rtl) | +| editor.doc.format.cs(...) | [`format.cs`](/document-api/reference/format/cs) | +| editor.doc.format.bCs(...) | [`format.bCs`](/document-api/reference/format/b-cs) | +| editor.doc.format.iCs(...) | [`format.iCs`](/document-api/reference/format/i-cs) | +| editor.doc.format.eastAsianLayout(...) | [`format.eastAsianLayout`](/document-api/reference/format/east-asian-layout) | +| editor.doc.format.em(...) | [`format.em`](/document-api/reference/format/em) | +| editor.doc.format.fitText(...) | [`format.fitText`](/document-api/reference/format/fit-text) | +| editor.doc.format.snapToGrid(...) | [`format.snapToGrid`](/document-api/reference/format/snap-to-grid) | +| editor.doc.format.lang(...) | [`format.lang`](/document-api/reference/format/lang) | +| editor.doc.format.oMath(...) | [`format.oMath`](/document-api/reference/format/o-math) | +| editor.doc.format.rStyle(...) | [`format.rStyle`](/document-api/reference/format/r-style) | +| editor.doc.format.rFonts(...) | [`format.rFonts`](/document-api/reference/format/r-fonts) | +| editor.doc.format.fontSizeCs(...) | [`format.fontSizeCs`](/document-api/reference/format/font-size-cs) | +| editor.doc.format.ligatures(...) | [`format.ligatures`](/document-api/reference/format/ligatures) | +| editor.doc.format.numForm(...) | [`format.numForm`](/document-api/reference/format/num-form) | +| 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.bold(...) | [`format.apply`](/document-api/reference/format/apply) | -| editor.doc.format.italic(...) | [`format.apply`](/document-api/reference/format/apply) | -| editor.doc.format.underline(...) | [`format.apply`](/document-api/reference/format/apply) | -| editor.doc.format.strikethrough(...) | [`format.apply`](/document-api/reference/format/apply) | +| 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) | | editor.doc.lists.insert(...) | [`lists.insert`](/document-api/reference/lists/insert) | diff --git a/apps/docs/document-api/reference/_generated-manifest.json b/apps/docs/document-api/reference/_generated-manifest.json index c7f7325269..bd15fcb380 100644 --- a/apps/docs/document-api/reference/_generated-manifest.json +++ b/apps/docs/document-api/reference/_generated-manifest.json @@ -21,10 +21,49 @@ "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", + "apps/docs/document-api/reference/format/border.mdx", + "apps/docs/document-api/reference/format/caps.mdx", + "apps/docs/document-api/reference/format/char-scale.mdx", "apps/docs/document-api/reference/format/color.mdx", - "apps/docs/document-api/reference/format/font-family.mdx", + "apps/docs/document-api/reference/format/contextual-alternates.mdx", + "apps/docs/document-api/reference/format/cs.mdx", + "apps/docs/document-api/reference/format/dstrike.mdx", + "apps/docs/document-api/reference/format/east-asian-layout.mdx", + "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-size-cs.mdx", "apps/docs/document-api/reference/format/font-size.mdx", + "apps/docs/document-api/reference/format/highlight.mdx", + "apps/docs/document-api/reference/format/i-cs.mdx", + "apps/docs/document-api/reference/format/imprint.mdx", "apps/docs/document-api/reference/format/index.mdx", + "apps/docs/document-api/reference/format/italic.mdx", + "apps/docs/document-api/reference/format/kerning.mdx", + "apps/docs/document-api/reference/format/lang.mdx", + "apps/docs/document-api/reference/format/letter-spacing.mdx", + "apps/docs/document-api/reference/format/ligatures.mdx", + "apps/docs/document-api/reference/format/num-form.mdx", + "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/position.mdx", + "apps/docs/document-api/reference/format/r-fonts.mdx", + "apps/docs/document-api/reference/format/r-style.mdx", + "apps/docs/document-api/reference/format/rtl.mdx", + "apps/docs/document-api/reference/format/shading.mdx", + "apps/docs/document-api/reference/format/shadow.mdx", + "apps/docs/document-api/reference/format/small-caps.mdx", + "apps/docs/document-api/reference/format/snap-to-grid.mdx", + "apps/docs/document-api/reference/format/spec-vanish.mdx", + "apps/docs/document-api/reference/format/strike.mdx", + "apps/docs/document-api/reference/format/stylistic-sets.mdx", + "apps/docs/document-api/reference/format/underline.mdx", + "apps/docs/document-api/reference/format/vanish.mdx", + "apps/docs/document-api/reference/format/vert-align.mdx", + "apps/docs/document-api/reference/format/web-hidden.mdx", "apps/docs/document-api/reference/get-node-by-id.mdx", "apps/docs/document-api/reference/get-node.mdx", "apps/docs/document-api/reference/get-text.mdx", @@ -169,9 +208,54 @@ "title": "Sections" }, { - "aliasMemberPaths": ["format.bold", "format.italic", "format.underline", "format.strikethrough"], + "aliasMemberPaths": ["format.strikethrough"], "key": "format", - "operationIds": ["format.apply", "format.fontSize", "format.fontFamily", "format.color", "format.align"], + "operationIds": [ + "format.apply", + "format.bold", + "format.italic", + "format.strike", + "format.underline", + "format.highlight", + "format.color", + "format.fontSize", + "format.letterSpacing", + "format.vertAlign", + "format.position", + "format.dstrike", + "format.smallCaps", + "format.caps", + "format.shading", + "format.border", + "format.outline", + "format.shadow", + "format.emboss", + "format.imprint", + "format.charScale", + "format.kerning", + "format.vanish", + "format.webHidden", + "format.specVanish", + "format.rtl", + "format.cs", + "format.bCs", + "format.iCs", + "format.eastAsianLayout", + "format.em", + "format.fitText", + "format.snapToGrid", + "format.lang", + "format.oMath", + "format.rStyle", + "format.rFonts", + "format.fontSizeCs", + "format.ligatures", + "format.numForm", + "format.numSpacing", + "format.stylisticSets", + "format.contextualAlternates", + "format.align" + ], "pagePath": "apps/docs/document-api/reference/format/index.mdx", "title": "Format" }, @@ -275,5 +359,5 @@ } ], "marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}", - "sourceHash": "8b0c27a3eff9d8548a4de7b931f4a577774027635048dc42492f0500d00963b1" + "sourceHash": "66d5302141a398cc2e70505bff9f241848811f14d0231392a08c57e885f903d4" } diff --git a/apps/docs/document-api/reference/capabilities/get.mdx b/apps/docs/document-api/reference/capabilities/get.mdx index f705fe7e39..e5e6354334 100644 --- a/apps/docs/document-api/reference/capabilities/get.mdx +++ b/apps/docs/document-api/reference/capabilities/get.mdx @@ -48,18 +48,258 @@ _No fields._ ```json { "format": { - "properties": { + "supportedInlineProperties": { + "bCs": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, "bold": { - "directives": [ - "example" - ], - "kind": "example" + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "border": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "caps": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "charScale": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "color": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "contextualAlternates": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "cs": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "dstrike": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "eastAsianLayout": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "em": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "emboss": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "fitText": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "fontSize": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "fontSizeCs": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "highlight": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "iCs": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "imprint": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" }, "italic": { - "directives": [ - "example" - ], - "kind": "example" + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "kerning": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "lang": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "letterSpacing": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "ligatures": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "numForm": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "numSpacing": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "oMath": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "outline": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "position": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "rFonts": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "rStyle": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "rtl": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "shading": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "shadow": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "smallCaps": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "snapToGrid": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "specVanish": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "strike": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "stylisticSets": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "underline": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "vanish": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "vertAlign": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" + }, + "webHidden": { + "available": true, + "storage": "mark", + "tracked": true, + "type": "boolean" } } }, @@ -210,6 +450,46 @@ _No fields._ ], "tracked": true }, + "format.bCs": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.bold": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.border": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.caps": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.charScale": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, "format.color": { "available": true, "dryRun": true, @@ -218,7 +498,55 @@ _No fields._ ], "tracked": true }, - "format.fontFamily": { + "format.contextualAlternates": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.cs": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.dstrike": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.eastAsianLayout": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.em": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.emboss": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "format.fitText": { "available": true, "dryRun": true, "reasons": [ @@ -234,7 +562,7 @@ _No fields._ ], "tracked": true }, - "getNode": { + "format.fontSizeCs": { "available": true, "dryRun": true, "reasons": [ @@ -242,7 +570,7 @@ _No fields._ ], "tracked": true }, - "getNodeById": { + "format.highlight": { "available": true, "dryRun": true, "reasons": [ @@ -250,7 +578,7 @@ _No fields._ ], "tracked": true }, - "getText": { + "format.iCs": { "available": true, "dryRun": true, "reasons": [ @@ -258,7 +586,7 @@ _No fields._ ], "tracked": true }, - "info": { + "format.imprint": { "available": true, "dryRun": true, "reasons": [ @@ -266,7 +594,7 @@ _No fields._ ], "tracked": true }, - "insert": { + "format.italic": { "available": true, "dryRun": true, "reasons": [ @@ -274,7 +602,7 @@ _No fields._ ], "tracked": true }, - "lists.exit": { + "format.kerning": { "available": true, "dryRun": true, "reasons": [ @@ -282,7 +610,7 @@ _No fields._ ], "tracked": true }, - "lists.get": { + "format.lang": { "available": true, "dryRun": true, "reasons": [ @@ -290,7 +618,7 @@ _No fields._ ], "tracked": true }, - "lists.indent": { + "format.letterSpacing": { "available": true, "dryRun": true, "reasons": [ @@ -298,7 +626,7 @@ _No fields._ ], "tracked": true }, - "lists.insert": { + "format.ligatures": { "available": true, "dryRun": true, "reasons": [ @@ -306,7 +634,7 @@ _No fields._ ], "tracked": true }, - "lists.list": { + "format.numForm": { "available": true, "dryRun": true, "reasons": [ @@ -314,7 +642,7 @@ _No fields._ ], "tracked": true }, - "lists.outdent": { + "format.numSpacing": { "available": true, "dryRun": true, "reasons": [ @@ -322,7 +650,7 @@ _No fields._ ], "tracked": true }, - "lists.restart": { + "format.oMath": { "available": true, "dryRun": true, "reasons": [ @@ -330,7 +658,7 @@ _No fields._ ], "tracked": true }, - "lists.setType": { + "format.outline": { "available": true, "dryRun": true, "reasons": [ @@ -338,7 +666,7 @@ _No fields._ ], "tracked": true }, - "mutations.apply": { + "format.position": { "available": true, "dryRun": true, "reasons": [ @@ -346,7 +674,7 @@ _No fields._ ], "tracked": true }, - "mutations.preview": { + "format.rFonts": { "available": true, "dryRun": true, "reasons": [ @@ -354,7 +682,7 @@ _No fields._ ], "tracked": true }, - "query.match": { + "format.rStyle": { "available": true, "dryRun": true, "reasons": [ @@ -362,7 +690,7 @@ _No fields._ ], "tracked": true }, - "replace": { + "format.rtl": { "available": true, "dryRun": true, "reasons": [ @@ -370,7 +698,7 @@ _No fields._ ], "tracked": true }, - "sections.clearHeaderFooterRef": { + "format.shading": { "available": true, "dryRun": true, "reasons": [ @@ -378,7 +706,7 @@ _No fields._ ], "tracked": true }, - "sections.clearPageBorders": { + "format.shadow": { "available": true, "dryRun": true, "reasons": [ @@ -386,7 +714,7 @@ _No fields._ ], "tracked": true }, - "sections.get": { + "format.smallCaps": { "available": true, "dryRun": true, "reasons": [ @@ -394,7 +722,7 @@ _No fields._ ], "tracked": true }, - "sections.list": { + "format.snapToGrid": { "available": true, "dryRun": true, "reasons": [ @@ -402,7 +730,7 @@ _No fields._ ], "tracked": true }, - "sections.setBreakType": { + "format.specVanish": { "available": true, "dryRun": true, "reasons": [ @@ -410,7 +738,7 @@ _No fields._ ], "tracked": true }, - "sections.setColumns": { + "format.strike": { "available": true, "dryRun": true, "reasons": [ @@ -418,7 +746,7 @@ _No fields._ ], "tracked": true }, - "sections.setHeaderFooterMargins": { + "format.stylisticSets": { "available": true, "dryRun": true, "reasons": [ @@ -426,7 +754,7 @@ _No fields._ ], "tracked": true }, - "sections.setHeaderFooterRef": { + "format.underline": { "available": true, "dryRun": true, "reasons": [ @@ -434,7 +762,7 @@ _No fields._ ], "tracked": true }, - "sections.setLineNumbering": { + "format.vanish": { "available": true, "dryRun": true, "reasons": [ @@ -442,7 +770,7 @@ _No fields._ ], "tracked": true }, - "sections.setLinkToPrevious": { + "format.vertAlign": { "available": true, "dryRun": true, "reasons": [ @@ -450,7 +778,7 @@ _No fields._ ], "tracked": true }, - "sections.setOddEvenHeadersFooters": { + "format.webHidden": { "available": true, "dryRun": true, "reasons": [ @@ -458,7 +786,7 @@ _No fields._ ], "tracked": true }, - "sections.setPageBorders": { + "getNode": { "available": true, "dryRun": true, "reasons": [ @@ -466,7 +794,7 @@ _No fields._ ], "tracked": true }, - "sections.setPageMargins": { + "getNodeById": { "available": true, "dryRun": true, "reasons": [ @@ -474,7 +802,7 @@ _No fields._ ], "tracked": true }, - "sections.setPageNumbering": { + "getText": { "available": true, "dryRun": true, "reasons": [ @@ -482,7 +810,7 @@ _No fields._ ], "tracked": true }, - "sections.setPageSetup": { + "info": { "available": true, "dryRun": true, "reasons": [ @@ -490,7 +818,7 @@ _No fields._ ], "tracked": true }, - "sections.setSectionDirection": { + "insert": { "available": true, "dryRun": true, "reasons": [ @@ -498,7 +826,7 @@ _No fields._ ], "tracked": true }, - "sections.setTitlePage": { + "lists.exit": { "available": true, "dryRun": true, "reasons": [ @@ -506,7 +834,7 @@ _No fields._ ], "tracked": true }, - "sections.setVerticalAlign": { + "lists.get": { "available": true, "dryRun": true, "reasons": [ @@ -514,7 +842,7 @@ _No fields._ ], "tracked": true }, - "styles.apply": { + "lists.indent": { "available": true, "dryRun": true, "reasons": [ @@ -522,7 +850,7 @@ _No fields._ ], "tracked": true }, - "tables.applyBorderPreset": { + "lists.insert": { "available": true, "dryRun": true, "reasons": [ @@ -530,7 +858,7 @@ _No fields._ ], "tracked": true }, - "tables.clearBorder": { + "lists.list": { "available": true, "dryRun": true, "reasons": [ @@ -538,7 +866,7 @@ _No fields._ ], "tracked": true }, - "tables.clearCellSpacing": { + "lists.outdent": { "available": true, "dryRun": true, "reasons": [ @@ -546,7 +874,7 @@ _No fields._ ], "tracked": true }, - "tables.clearContents": { + "lists.restart": { "available": true, "dryRun": true, "reasons": [ @@ -554,7 +882,7 @@ _No fields._ ], "tracked": true }, - "tables.clearShading": { + "lists.setType": { "available": true, "dryRun": true, "reasons": [ @@ -562,7 +890,7 @@ _No fields._ ], "tracked": true }, - "tables.clearStyle": { + "mutations.apply": { "available": true, "dryRun": true, "reasons": [ @@ -570,7 +898,7 @@ _No fields._ ], "tracked": true }, - "tables.convertFromText": { + "mutations.preview": { "available": true, "dryRun": true, "reasons": [ @@ -578,7 +906,7 @@ _No fields._ ], "tracked": true }, - "tables.convertToText": { + "query.match": { "available": true, "dryRun": true, "reasons": [ @@ -586,7 +914,7 @@ _No fields._ ], "tracked": true }, - "tables.delete": { + "replace": { "available": true, "dryRun": true, "reasons": [ @@ -594,7 +922,7 @@ _No fields._ ], "tracked": true }, - "tables.deleteCell": { + "sections.clearHeaderFooterRef": { "available": true, "dryRun": true, "reasons": [ @@ -602,7 +930,7 @@ _No fields._ ], "tracked": true }, - "tables.deleteColumn": { + "sections.clearPageBorders": { "available": true, "dryRun": true, "reasons": [ @@ -610,7 +938,7 @@ _No fields._ ], "tracked": true }, - "tables.deleteRow": { + "sections.get": { "available": true, "dryRun": true, "reasons": [ @@ -618,7 +946,7 @@ _No fields._ ], "tracked": true }, - "tables.distributeColumns": { + "sections.list": { "available": true, "dryRun": true, "reasons": [ @@ -626,7 +954,7 @@ _No fields._ ], "tracked": true }, - "tables.distributeRows": { + "sections.setBreakType": { "available": true, "dryRun": true, "reasons": [ @@ -634,7 +962,7 @@ _No fields._ ], "tracked": true }, - "tables.get": { + "sections.setColumns": { "available": true, "dryRun": true, "reasons": [ @@ -642,7 +970,7 @@ _No fields._ ], "tracked": true }, - "tables.getCells": { + "sections.setHeaderFooterMargins": { "available": true, "dryRun": true, "reasons": [ @@ -650,7 +978,7 @@ _No fields._ ], "tracked": true }, - "tables.getProperties": { + "sections.setHeaderFooterRef": { "available": true, "dryRun": true, "reasons": [ @@ -658,7 +986,7 @@ _No fields._ ], "tracked": true }, - "tables.insertCell": { + "sections.setLineNumbering": { "available": true, "dryRun": true, "reasons": [ @@ -666,7 +994,7 @@ _No fields._ ], "tracked": true }, - "tables.insertColumn": { + "sections.setLinkToPrevious": { "available": true, "dryRun": true, "reasons": [ @@ -674,7 +1002,7 @@ _No fields._ ], "tracked": true }, - "tables.insertRow": { + "sections.setOddEvenHeadersFooters": { "available": true, "dryRun": true, "reasons": [ @@ -682,7 +1010,7 @@ _No fields._ ], "tracked": true }, - "tables.mergeCells": { + "sections.setPageBorders": { "available": true, "dryRun": true, "reasons": [ @@ -690,7 +1018,7 @@ _No fields._ ], "tracked": true }, - "tables.move": { + "sections.setPageMargins": { "available": true, "dryRun": true, "reasons": [ @@ -698,7 +1026,7 @@ _No fields._ ], "tracked": true }, - "tables.setAltText": { + "sections.setPageNumbering": { "available": true, "dryRun": true, "reasons": [ @@ -706,7 +1034,7 @@ _No fields._ ], "tracked": true }, - "tables.setBorder": { + "sections.setPageSetup": { "available": true, "dryRun": true, "reasons": [ @@ -714,7 +1042,7 @@ _No fields._ ], "tracked": true }, - "tables.setCellPadding": { + "sections.setSectionDirection": { "available": true, "dryRun": true, "reasons": [ @@ -722,7 +1050,7 @@ _No fields._ ], "tracked": true }, - "tables.setCellProperties": { + "sections.setTitlePage": { "available": true, "dryRun": true, "reasons": [ @@ -730,7 +1058,7 @@ _No fields._ ], "tracked": true }, - "tables.setCellSpacing": { + "sections.setVerticalAlign": { "available": true, "dryRun": true, "reasons": [ @@ -738,7 +1066,7 @@ _No fields._ ], "tracked": true }, - "tables.setColumnWidth": { + "styles.apply": { "available": true, "dryRun": true, "reasons": [ @@ -746,7 +1074,7 @@ _No fields._ ], "tracked": true }, - "tables.setLayout": { + "tables.applyBorderPreset": { "available": true, "dryRun": true, "reasons": [ @@ -754,7 +1082,7 @@ _No fields._ ], "tracked": true }, - "tables.setRowHeight": { + "tables.clearBorder": { "available": true, "dryRun": true, "reasons": [ @@ -762,7 +1090,7 @@ _No fields._ ], "tracked": true }, - "tables.setRowOptions": { + "tables.clearCellSpacing": { "available": true, "dryRun": true, "reasons": [ @@ -770,7 +1098,7 @@ _No fields._ ], "tracked": true }, - "tables.setShading": { + "tables.clearContents": { "available": true, "dryRun": true, "reasons": [ @@ -778,7 +1106,7 @@ _No fields._ ], "tracked": true }, - "tables.setStyle": { + "tables.clearShading": { "available": true, "dryRun": true, "reasons": [ @@ -786,7 +1114,7 @@ _No fields._ ], "tracked": true }, - "tables.setStyleOption": { + "tables.clearStyle": { "available": true, "dryRun": true, "reasons": [ @@ -794,7 +1122,7 @@ _No fields._ ], "tracked": true }, - "tables.setTablePadding": { + "tables.convertFromText": { "available": true, "dryRun": true, "reasons": [ @@ -802,7 +1130,7 @@ _No fields._ ], "tracked": true }, - "tables.sort": { + "tables.convertToText": { "available": true, "dryRun": true, "reasons": [ @@ -810,7 +1138,7 @@ _No fields._ ], "tracked": true }, - "tables.split": { + "tables.delete": { "available": true, "dryRun": true, "reasons": [ @@ -818,7 +1146,7 @@ _No fields._ ], "tracked": true }, - "tables.splitCell": { + "tables.deleteCell": { "available": true, "dryRun": true, "reasons": [ @@ -826,7 +1154,7 @@ _No fields._ ], "tracked": true }, - "tables.unmergeCells": { + "tables.deleteColumn": { "available": true, "dryRun": true, "reasons": [ @@ -834,7 +1162,7 @@ _No fields._ ], "tracked": true }, - "trackChanges.decide": { + "tables.deleteRow": { "available": true, "dryRun": true, "reasons": [ @@ -842,7 +1170,7 @@ _No fields._ ], "tracked": true }, - "trackChanges.get": { + "tables.distributeColumns": { "available": true, "dryRun": true, "reasons": [ @@ -850,25 +1178,249 @@ _No fields._ ], "tracked": true }, - "trackChanges.list": { + "tables.distributeRows": { "available": true, "dryRun": true, "reasons": [ "COMMAND_UNAVAILABLE" ], "tracked": true - } - }, - "planEngine": { - "regex": { - "maxPatternLength": 1 }, - "supportedNonUniformStrategies": [ - "example" - ], - "supportedSetMarks": [ - "example" - ], + "tables.get": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.getCells": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.getProperties": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.insertCell": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.insertColumn": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.insertRow": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.mergeCells": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.move": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.setAltText": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.setBorder": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.setCellPadding": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.setCellProperties": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.setCellSpacing": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.setColumnWidth": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.setLayout": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.setRowHeight": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.setRowOptions": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.setShading": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.setStyle": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.setStyleOption": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.setTablePadding": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.sort": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.split": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.splitCell": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "tables.unmergeCells": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "trackChanges.decide": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "trackChanges.get": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, + "trackChanges.list": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + } + }, + "planEngine": { + "regex": { + "maxPatternLength": 1 + }, + "supportedNonUniformStrategies": [ + "example" + ], + "supportedSetMarks": [ + "example" + ], "supportedStepOps": [ "example" ] @@ -904,101 +1456,2803 @@ _No fields._ "format": { "additionalProperties": false, "properties": { - "properties": { + "supportedInlineProperties": { + "additionalProperties": false, + "properties": { + "bCs": { + "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" + }, + "bold": { + "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" + }, + "border": { + "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" + }, + "caps": { + "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" + }, + "charScale": { + "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" + }, + "color": { + "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" + }, + "contextualAlternates": { + "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" + }, + "cs": { + "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" + }, + "dstrike": { + "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" + }, + "eastAsianLayout": { + "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" + }, + "em": { + "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" + }, + "emboss": { + "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" + }, + "fitText": { + "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": { + "available": { + "type": "boolean" + }, + "storage": { + "enum": [ + "mark", + "runAttribute" + ] + }, + "tracked": { + "type": "boolean" + }, + "type": { + "enum": [ + "boolean", + "string", + "number", + "object", + "array" + ] + } + }, + "required": [ + "available", + "tracked", + "type", + "storage" + ], + "type": "object" + }, + "fontSizeCs": { + "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" + }, + "highlight": { + "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" + }, + "iCs": { + "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" + }, + "imprint": { + "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" + }, + "italic": { + "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" + }, + "kerning": { + "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" + }, + "lang": { + "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" + }, + "letterSpacing": { + "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" + }, + "ligatures": { + "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" + }, + "numForm": { + "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" + }, + "numSpacing": { + "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" + }, + "oMath": { + "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" + }, + "outline": { + "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" + }, + "position": { + "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" + }, + "rFonts": { + "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" + }, + "rStyle": { + "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" + }, + "rtl": { + "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" + }, + "shading": { + "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" + }, + "shadow": { + "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" + }, + "smallCaps": { + "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" + }, + "snapToGrid": { + "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" + }, + "specVanish": { + "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" + }, + "strike": { + "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" + }, + "stylisticSets": { + "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" + }, + "underline": { + "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" + }, + "vanish": { + "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" + }, + "vertAlign": { + "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" + }, + "webHidden": { + "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" + } + }, + "required": [ + "bold", + "italic", + "strike", + "underline", + "highlight", + "color", + "fontSize", + "letterSpacing", + "vertAlign", + "position", + "dstrike", + "smallCaps", + "caps", + "shading", + "border", + "outline", + "shadow", + "emboss", + "imprint", + "charScale", + "kerning", + "vanish", + "webHidden", + "specVanish", + "rtl", + "cs", + "bCs", + "iCs", + "eastAsianLayout", + "em", + "fitText", + "snapToGrid", + "lang", + "oMath", + "rStyle", + "rFonts", + "fontSizeCs", + "ligatures", + "numForm", + "numSpacing", + "stylisticSets", + "contextualAlternates" + ], + "type": "object" + } + }, + "required": [ + "supportedInlineProperties" + ], + "type": "object" + }, + "global": { + "additionalProperties": false, + "properties": { + "comments": { + "additionalProperties": false, + "properties": { + "enabled": { + "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" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "dryRun": { + "additionalProperties": false, + "properties": { + "enabled": { + "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" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "lists": { + "additionalProperties": false, + "properties": { + "enabled": { + "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" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "trackChanges": { + "additionalProperties": false, + "properties": { + "enabled": { + "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" + } + }, + "required": [ + "enabled" + ], + "type": "object" + } + }, + "required": [ + "trackChanges", + "comments", + "lists", + "dryRun" + ], + "type": "object" + }, + "operations": { + "additionalProperties": false, + "properties": { + "blocks.delete": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "capabilities.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" + }, + "comments.create": { + "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" + }, + "comments.delete": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "comments.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" + }, + "comments.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" + }, + "comments.patch": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "create.heading": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "create.paragraph": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "create.sectionBreak": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "create.table": { + "additionalProperties": false, + "properties": { + "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" + }, + "delete": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "find": { + "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.align": { + "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.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" + }, + "format.bCs": { + "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.bold": { + "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.border": { + "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.caps": { + "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.charScale": { + "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.color": { + "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.contextualAlternates": { + "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.cs": { + "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.dstrike": { + "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.eastAsianLayout": { + "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.em": { + "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.emboss": { + "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.fitText": { + "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.fontSize": { + "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.fontSizeCs": { + "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.highlight": { + "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.iCs": { "additionalProperties": false, "properties": { - "bold": { - "additionalProperties": false, - "properties": { - "directives": { - "items": { - "type": "string" - }, - "type": "array" - }, - "kind": { - "type": "string" - } + "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" + ] }, - "required": [ - "kind", - "directives" - ], - "type": "object" + "type": "array" }, - "italic": { - "additionalProperties": false, - "properties": { - "directives": { - "items": { - "type": "string" - }, - "type": "array" - }, - "kind": { - "type": "string" - } + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "format.imprint": { + "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" + ] }, - "required": [ - "kind", - "directives" - ], - "type": "object" + "type": "array" }, - "strike": { - "additionalProperties": false, - "properties": { - "directives": { - "items": { - "type": "string" - }, - "type": "array" - }, - "kind": { - "type": "string" - } + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "format.italic": { + "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" + ] }, - "required": [ - "kind", - "directives" - ], - "type": "object" + "type": "array" }, - "underline": { - "additionalProperties": false, - "properties": { - "directives": { - "items": { - "type": "string" - }, - "type": "array" - }, - "kind": { - "type": "string" - } + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "format.kerning": { + "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" + ] }, - "required": [ - "kind", - "directives" - ], - "type": "object" + "type": "array" + }, + "tracked": { + "type": "boolean" } }, + "required": [ + "available", + "tracked", + "dryRun" + ], "type": "object" - } - }, - "required": [ - "properties" - ], - "type": "object" - }, - "global": { - "additionalProperties": false, - "properties": { - "comments": { + }, + "format.lang": { "additionalProperties": false, "properties": { - "enabled": { + "available": { + "type": "boolean" + }, + "dryRun": { "type": "boolean" }, "reasons": { @@ -1015,17 +4269,25 @@ _No fields._ ] }, "type": "array" + }, + "tracked": { + "type": "boolean" } }, "required": [ - "enabled" + "available", + "tracked", + "dryRun" ], "type": "object" }, - "dryRun": { + "format.letterSpacing": { "additionalProperties": false, "properties": { - "enabled": { + "available": { + "type": "boolean" + }, + "dryRun": { "type": "boolean" }, "reasons": { @@ -1042,17 +4304,25 @@ _No fields._ ] }, "type": "array" + }, + "tracked": { + "type": "boolean" } }, "required": [ - "enabled" + "available", + "tracked", + "dryRun" ], "type": "object" }, - "lists": { + "format.ligatures": { "additionalProperties": false, "properties": { - "enabled": { + "available": { + "type": "boolean" + }, + "dryRun": { "type": "boolean" }, "reasons": { @@ -1069,17 +4339,25 @@ _No fields._ ] }, "type": "array" + }, + "tracked": { + "type": "boolean" } }, "required": [ - "enabled" + "available", + "tracked", + "dryRun" ], "type": "object" }, - "trackChanges": { + "format.numForm": { "additionalProperties": false, "properties": { - "enabled": { + "available": { + "type": "boolean" + }, + "dryRun": { "type": "boolean" }, "reasons": { @@ -1096,26 +4374,19 @@ _No fields._ ] }, "type": "array" + }, + "tracked": { + "type": "boolean" } }, "required": [ - "enabled" + "available", + "tracked", + "dryRun" ], "type": "object" - } - }, - "required": [ - "trackChanges", - "comments", - "lists", - "dryRun" - ], - "type": "object" - }, - "operations": { - "additionalProperties": false, - "properties": { - "blocks.delete": { + }, + "format.numSpacing": { "additionalProperties": false, "properties": { "available": { @@ -1150,7 +4421,7 @@ _No fields._ ], "type": "object" }, - "capabilities.get": { + "format.oMath": { "additionalProperties": false, "properties": { "available": { @@ -1185,7 +4456,7 @@ _No fields._ ], "type": "object" }, - "comments.create": { + "format.outline": { "additionalProperties": false, "properties": { "available": { @@ -1220,7 +4491,7 @@ _No fields._ ], "type": "object" }, - "comments.delete": { + "format.position": { "additionalProperties": false, "properties": { "available": { @@ -1255,7 +4526,7 @@ _No fields._ ], "type": "object" }, - "comments.get": { + "format.rFonts": { "additionalProperties": false, "properties": { "available": { @@ -1290,7 +4561,7 @@ _No fields._ ], "type": "object" }, - "comments.list": { + "format.rStyle": { "additionalProperties": false, "properties": { "available": { @@ -1325,7 +4596,7 @@ _No fields._ ], "type": "object" }, - "comments.patch": { + "format.rtl": { "additionalProperties": false, "properties": { "available": { @@ -1360,7 +4631,7 @@ _No fields._ ], "type": "object" }, - "create.heading": { + "format.shading": { "additionalProperties": false, "properties": { "available": { @@ -1395,7 +4666,7 @@ _No fields._ ], "type": "object" }, - "create.paragraph": { + "format.shadow": { "additionalProperties": false, "properties": { "available": { @@ -1430,7 +4701,7 @@ _No fields._ ], "type": "object" }, - "create.sectionBreak": { + "format.smallCaps": { "additionalProperties": false, "properties": { "available": { @@ -1465,7 +4736,7 @@ _No fields._ ], "type": "object" }, - "create.table": { + "format.snapToGrid": { "additionalProperties": false, "properties": { "available": { @@ -1500,7 +4771,7 @@ _No fields._ ], "type": "object" }, - "delete": { + "format.specVanish": { "additionalProperties": false, "properties": { "available": { @@ -1535,7 +4806,7 @@ _No fields._ ], "type": "object" }, - "find": { + "format.strike": { "additionalProperties": false, "properties": { "available": { @@ -1570,7 +4841,7 @@ _No fields._ ], "type": "object" }, - "format.align": { + "format.stylisticSets": { "additionalProperties": false, "properties": { "available": { @@ -1605,7 +4876,7 @@ _No fields._ ], "type": "object" }, - "format.apply": { + "format.underline": { "additionalProperties": false, "properties": { "available": { @@ -1640,7 +4911,7 @@ _No fields._ ], "type": "object" }, - "format.color": { + "format.vanish": { "additionalProperties": false, "properties": { "available": { @@ -1675,7 +4946,7 @@ _No fields._ ], "type": "object" }, - "format.fontFamily": { + "format.vertAlign": { "additionalProperties": false, "properties": { "available": { @@ -1710,7 +4981,7 @@ _No fields._ ], "type": "object" }, - "format.fontSize": { + "format.webHidden": { "additionalProperties": false, "properties": { "available": { @@ -4487,9 +7758,48 @@ _No fields._ "delete", "blocks.delete", "format.apply", - "format.fontSize", - "format.fontFamily", + "format.bold", + "format.italic", + "format.strike", + "format.underline", + "format.highlight", "format.color", + "format.fontSize", + "format.letterSpacing", + "format.vertAlign", + "format.position", + "format.dstrike", + "format.smallCaps", + "format.caps", + "format.shading", + "format.border", + "format.outline", + "format.shadow", + "format.emboss", + "format.imprint", + "format.charScale", + "format.kerning", + "format.vanish", + "format.webHidden", + "format.specVanish", + "format.rtl", + "format.cs", + "format.bCs", + "format.iCs", + "format.eastAsianLayout", + "format.em", + "format.fitText", + "format.snapToGrid", + "format.lang", + "format.oMath", + "format.rStyle", + "format.rFonts", + "format.fontSizeCs", + "format.ligatures", + "format.numForm", + "format.numSpacing", + "format.stylisticSets", + "format.contextualAlternates", "format.align", "styles.apply", "create.paragraph", diff --git a/apps/docs/document-api/reference/format/apply.mdx b/apps/docs/document-api/reference/format/apply.mdx index 89611ab9f3..dc180fe1bc 100644 --- a/apps/docs/document-api/reference/format/apply.mdx +++ b/apps/docs/document-api/reference/format/apply.mdx @@ -36,8 +36,8 @@ Returns a TextMutationReceipt confirming inline styles were applied to the targe ```json { "inline": { - "bold": "on", - "italic": "on" + "bold": true, + "italic": true }, "target": { "blockId": "block-abc123", @@ -121,32 +121,795 @@ _No fields._ "additionalProperties": false, "minProperties": 1, "properties": { + "bCs": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, "bold": { - "enum": [ - "on", - "off", - "clear" + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "border": { + "oneOf": [ + { + "additionalProperties": false, + "minProperties": 1, + "properties": { + "color": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "space": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "sz": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "val": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + { + "type": "null" + } + ] + }, + "caps": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "charScale": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "color": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "contextualAlternates": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "cs": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "dstrike": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "eastAsianLayout": { + "oneOf": [ + { + "additionalProperties": false, + "minProperties": 1, + "properties": { + "combine": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "combineBrackets": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "id": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "vert": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "vertCompress": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + { + "type": "null" + } + ] + }, + "em": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "emboss": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "fitText": { + "oneOf": [ + { + "additionalProperties": false, + "minProperties": 1, + "properties": { + "id": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "val": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + { + "type": "null" + } + ] + }, + "fontSize": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "fontSizeCs": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "highlight": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "iCs": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "imprint": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } ] }, "italic": { - "enum": [ - "on", - "off", - "clear" + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "kerning": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "lang": { + "oneOf": [ + { + "additionalProperties": false, + "minProperties": 1, + "properties": { + "bidi": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "eastAsia": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "val": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + { + "type": "null" + } + ] + }, + "letterSpacing": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "ligatures": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "numForm": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "numSpacing": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "oMath": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "outline": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "position": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "rFonts": { + "oneOf": [ + { + "additionalProperties": false, + "minProperties": 1, + "properties": { + "ascii": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "asciiTheme": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "cs": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "csTheme": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "eastAsia": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "eastAsiaTheme": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hAnsi": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hAnsiTheme": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hint": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + { + "type": "null" + } + ] + }, + "rStyle": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "rtl": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "shading": { + "oneOf": [ + { + "additionalProperties": false, + "minProperties": 1, + "properties": { + "color": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "fill": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "val": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + { + "type": "null" + } + ] + }, + "shadow": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "smallCaps": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "snapToGrid": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "specVanish": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } ] }, "strike": { - "enum": [ - "on", - "off", - "clear" + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "stylisticSets": { + "oneOf": [ + { + "items": { + "additionalProperties": false, + "properties": { + "id": { + "type": "number" + }, + "val": { + "type": "boolean" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + }, + { + "type": "null" + } ] }, "underline": { - "enum": [ - "on", - "off", - "clear" + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + }, + { + "additionalProperties": false, + "minProperties": 1, + "properties": { + "color": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "style": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "themeColor": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + } + ] + }, + "vanish": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "vertAlign": { + "oneOf": [ + { + "enum": [ + "superscript", + "subscript", + "baseline" + ] + }, + { + "type": "null" + } + ] + }, + "webHidden": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } ] } }, diff --git a/apps/docs/document-api/reference/format/b-cs.mdx b/apps/docs/document-api/reference/format/b-cs.mdx new file mode 100644 index 0000000000..a103cae15e --- /dev/null +++ b/apps/docs/document-api/reference/format/b-cs.mdx @@ -0,0 +1,228 @@ +--- +title: format.bCs +sidebarTitle: format.bCs +description: Reference for format.bCs +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.bCs` +- API member path: `editor.doc.format.bCs(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | boolean \\| null | no | One of: boolean, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": true +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/bold.mdx b/apps/docs/document-api/reference/format/bold.mdx new file mode 100644 index 0000000000..f0f7273150 --- /dev/null +++ b/apps/docs/document-api/reference/format/bold.mdx @@ -0,0 +1,228 @@ +--- +title: format.bold +sidebarTitle: format.bold +description: Reference for format.bold +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.bold` +- API member path: `editor.doc.format.bold(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `yes` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | boolean \\| null | no | One of: boolean, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": true +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/border.mdx b/apps/docs/document-api/reference/format/border.mdx new file mode 100644 index 0000000000..93663ee0d4 --- /dev/null +++ b/apps/docs/document-api/reference/format/border.mdx @@ -0,0 +1,278 @@ +--- +title: format.border +sidebarTitle: format.border +description: Reference for format.border +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.border` +- API member path: `editor.doc.format.border(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | object \\| null | yes | One of: object, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": { + "sz": 12.5, + "val": "example" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "additionalProperties": false, + "minProperties": 1, + "properties": { + "color": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "space": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "sz": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "val": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target", + "value" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/caps.mdx b/apps/docs/document-api/reference/format/caps.mdx new file mode 100644 index 0000000000..49f84e9b26 --- /dev/null +++ b/apps/docs/document-api/reference/format/caps.mdx @@ -0,0 +1,228 @@ +--- +title: format.caps +sidebarTitle: format.caps +description: Reference for format.caps +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.caps` +- API member path: `editor.doc.format.caps(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | boolean \\| null | no | One of: boolean, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": true +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/char-scale.mdx b/apps/docs/document-api/reference/format/char-scale.mdx new file mode 100644 index 0000000000..7b240f361d --- /dev/null +++ b/apps/docs/document-api/reference/format/char-scale.mdx @@ -0,0 +1,229 @@ +--- +title: format.charScale +sidebarTitle: format.charScale +description: Reference for format.charScale +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.charScale` +- API member path: `editor.doc.format.charScale(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | number \\| null | yes | One of: number, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": 12.5 +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target", + "value" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/color.mdx b/apps/docs/document-api/reference/format/color.mdx index d3a1d75c69..180c1b0de2 100644 --- a/apps/docs/document-api/reference/format/color.mdx +++ b/apps/docs/document-api/reference/format/color.mdx @@ -16,7 +16,7 @@ Set or unset the text color on the target text range. Pass null to remove. - API member path: `editor.doc.format.color(...)` - Mutates document: `yes` - Idempotency: `conditional` -- Supports tracked mode: `no` +- Supports tracked mode: `yes` - Supports dry run: `yes` - Deterministic target resolution: `yes` @@ -106,7 +106,6 @@ _No fields._ ## Non-applied failure codes - `INVALID_TARGET` -- `NO_OP` ## Raw schemas @@ -154,8 +153,7 @@ _No fields._ "properties": { "code": { "enum": [ - "INVALID_TARGET", - "NO_OP" + "INVALID_TARGET" ] }, "details": {}, @@ -206,8 +204,7 @@ _No fields._ "properties": { "code": { "enum": [ - "INVALID_TARGET", - "NO_OP" + "INVALID_TARGET" ] }, "details": {}, diff --git a/apps/docs/document-api/reference/format/contextual-alternates.mdx b/apps/docs/document-api/reference/format/contextual-alternates.mdx new file mode 100644 index 0000000000..052b120d06 --- /dev/null +++ b/apps/docs/document-api/reference/format/contextual-alternates.mdx @@ -0,0 +1,228 @@ +--- +title: format.contextualAlternates +sidebarTitle: format.contextualAlternates +description: Reference for format.contextualAlternates +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.contextualAlternates` +- API member path: `editor.doc.format.contextualAlternates(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | boolean \\| null | no | One of: boolean, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": true +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/cs.mdx b/apps/docs/document-api/reference/format/cs.mdx new file mode 100644 index 0000000000..e0d6ba448d --- /dev/null +++ b/apps/docs/document-api/reference/format/cs.mdx @@ -0,0 +1,228 @@ +--- +title: format.cs +sidebarTitle: format.cs +description: Reference for format.cs +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.cs` +- API member path: `editor.doc.format.cs(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | boolean \\| null | no | One of: boolean, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": true +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/dstrike.mdx b/apps/docs/document-api/reference/format/dstrike.mdx new file mode 100644 index 0000000000..6b9d584baf --- /dev/null +++ b/apps/docs/document-api/reference/format/dstrike.mdx @@ -0,0 +1,228 @@ +--- +title: format.dstrike +sidebarTitle: format.dstrike +description: Reference for format.dstrike +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.dstrike` +- API member path: `editor.doc.format.dstrike(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | boolean \\| null | no | One of: boolean, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": true +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/east-asian-layout.mdx b/apps/docs/document-api/reference/format/east-asian-layout.mdx new file mode 100644 index 0000000000..26075fbc8a --- /dev/null +++ b/apps/docs/document-api/reference/format/east-asian-layout.mdx @@ -0,0 +1,288 @@ +--- +title: format.eastAsianLayout +sidebarTitle: format.eastAsianLayout +description: Reference for format.eastAsianLayout +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.eastAsianLayout` +- API member path: `editor.doc.format.eastAsianLayout(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | object \\| null | yes | One of: object, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": { + "combine": true, + "id": "id-001" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "additionalProperties": false, + "minProperties": 1, + "properties": { + "combine": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "combineBrackets": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "id": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "vert": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "vertCompress": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target", + "value" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/font-family.mdx b/apps/docs/document-api/reference/format/em.mdx similarity index 86% rename from apps/docs/document-api/reference/format/font-family.mdx rename to apps/docs/document-api/reference/format/em.mdx index 339c0d9657..66efd81810 100644 --- a/apps/docs/document-api/reference/format/font-family.mdx +++ b/apps/docs/document-api/reference/format/em.mdx @@ -1,7 +1,7 @@ --- -title: format.fontFamily -sidebarTitle: format.fontFamily -description: Set or unset the font family on the target text range. Pass null to remove. +title: format.em +sidebarTitle: format.em +description: Reference for format.em --- {/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} @@ -10,20 +10,14 @@ description: Set or unset the font family on the target text range. Pass null to ## Summary -Set or unset the font family on the target text range. Pass null to remove. - -- Operation ID: `format.fontFamily` -- API member path: `editor.doc.format.fontFamily(...)` +- Operation ID: `format.em` +- API member path: `editor.doc.format.em(...)` - Mutates document: `yes` - Idempotency: `conditional` - Supports tracked mode: `no` - Supports dry run: `yes` - Deterministic target resolution: `yes` -## Expected result - -Returns a TextMutationReceipt; receipt reports NO_OP if the target already has the requested font family. - ## Input fields | Field | Type | Required | Description | @@ -106,7 +100,6 @@ _No fields._ ## Non-applied failure codes - `INVALID_TARGET` -- `NO_OP` ## Raw schemas @@ -154,8 +147,7 @@ _No fields._ "properties": { "code": { "enum": [ - "INVALID_TARGET", - "NO_OP" + "INVALID_TARGET" ] }, "details": {}, @@ -206,8 +198,7 @@ _No fields._ "properties": { "code": { "enum": [ - "INVALID_TARGET", - "NO_OP" + "INVALID_TARGET" ] }, "details": {}, diff --git a/apps/docs/document-api/reference/format/emboss.mdx b/apps/docs/document-api/reference/format/emboss.mdx new file mode 100644 index 0000000000..91f76a4f67 --- /dev/null +++ b/apps/docs/document-api/reference/format/emboss.mdx @@ -0,0 +1,228 @@ +--- +title: format.emboss +sidebarTitle: format.emboss +description: Reference for format.emboss +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.emboss` +- API member path: `editor.doc.format.emboss(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | boolean \\| null | no | One of: boolean, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": true +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/fit-text.mdx b/apps/docs/document-api/reference/format/fit-text.mdx new file mode 100644 index 0000000000..a11f0f0490 --- /dev/null +++ b/apps/docs/document-api/reference/format/fit-text.mdx @@ -0,0 +1,257 @@ +--- +title: format.fitText +sidebarTitle: format.fitText +description: Reference for format.fitText +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.fitText` +- API member path: `editor.doc.format.fitText(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | object \\| null | yes | One of: object, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": { + "id": "id-001", + "val": 12.5 + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "additionalProperties": false, + "minProperties": 1, + "properties": { + "id": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "val": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target", + "value" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/font-size-cs.mdx b/apps/docs/document-api/reference/format/font-size-cs.mdx new file mode 100644 index 0000000000..373c024faa --- /dev/null +++ b/apps/docs/document-api/reference/format/font-size-cs.mdx @@ -0,0 +1,229 @@ +--- +title: format.fontSizeCs +sidebarTitle: format.fontSizeCs +description: Reference for format.fontSizeCs +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.fontSizeCs` +- API member path: `editor.doc.format.fontSizeCs(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | number \\| null | yes | One of: number, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": 12.5 +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target", + "value" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/font-size.mdx b/apps/docs/document-api/reference/format/font-size.mdx index 3d17bde201..cc8b8fbf12 100644 --- a/apps/docs/document-api/reference/format/font-size.mdx +++ b/apps/docs/document-api/reference/format/font-size.mdx @@ -16,7 +16,7 @@ Set or unset the font size on the target text range. Pass null to remove. - API member path: `editor.doc.format.fontSize(...)` - Mutates document: `yes` - Idempotency: `conditional` -- Supports tracked mode: `no` +- Supports tracked mode: `yes` - Supports dry run: `yes` - Deterministic target resolution: `yes` @@ -29,7 +29,7 @@ Returns a TextMutationReceipt; receipt reports NO_OP if the target already has t | Field | Type | Required | Description | | --- | --- | --- | --- | | `target` | TextAddress | yes | TextAddress | -| `value` | string \\| number \\| null | yes | One of: string, number, null | +| `value` | number \\| null | yes | One of: number, null | ### Example request @@ -43,7 +43,7 @@ Returns a TextMutationReceipt; receipt reports NO_OP if the target already has t "start": 0 } }, - "value": "example" + "value": 12.5 } ``` @@ -106,7 +106,6 @@ _No fields._ ## Non-applied failure codes - `INVALID_TARGET` -- `NO_OP` ## Raw schemas @@ -120,10 +119,6 @@ _No fields._ }, "value": { "oneOf": [ - { - "minLength": 1, - "type": "string" - }, { "type": "number" }, @@ -157,8 +152,7 @@ _No fields._ "properties": { "code": { "enum": [ - "INVALID_TARGET", - "NO_OP" + "INVALID_TARGET" ] }, "details": {}, @@ -209,8 +203,7 @@ _No fields._ "properties": { "code": { "enum": [ - "INVALID_TARGET", - "NO_OP" + "INVALID_TARGET" ] }, "details": {}, diff --git a/apps/docs/document-api/reference/format/highlight.mdx b/apps/docs/document-api/reference/format/highlight.mdx new file mode 100644 index 0000000000..0863247888 --- /dev/null +++ b/apps/docs/document-api/reference/format/highlight.mdx @@ -0,0 +1,230 @@ +--- +title: format.highlight +sidebarTitle: format.highlight +description: Reference for format.highlight +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.highlight` +- API member path: `editor.doc.format.highlight(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `yes` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | string \\| null | yes | One of: string, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": "example" +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target", + "value" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/i-cs.mdx b/apps/docs/document-api/reference/format/i-cs.mdx new file mode 100644 index 0000000000..042a89e15a --- /dev/null +++ b/apps/docs/document-api/reference/format/i-cs.mdx @@ -0,0 +1,228 @@ +--- +title: format.iCs +sidebarTitle: format.iCs +description: Reference for format.iCs +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.iCs` +- API member path: `editor.doc.format.iCs(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | boolean \\| null | no | One of: boolean, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": true +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/imprint.mdx b/apps/docs/document-api/reference/format/imprint.mdx new file mode 100644 index 0000000000..6a02c4654c --- /dev/null +++ b/apps/docs/document-api/reference/format/imprint.mdx @@ -0,0 +1,228 @@ +--- +title: format.imprint +sidebarTitle: format.imprint +description: Reference for format.imprint +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.imprint` +- API member path: `editor.doc.format.imprint(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | boolean \\| null | no | One of: boolean, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": true +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/index.mdx b/apps/docs/document-api/reference/format/index.mdx index 477b407ff1..99f020687f 100644 --- a/apps/docs/document-api/reference/format/index.mdx +++ b/apps/docs/document-api/reference/format/index.mdx @@ -15,9 +15,48 @@ Canonical formatting mutation with directive semantics ('on', 'off', 'clear'). | Operation | Member path | Mutates | Idempotency | Tracked | Dry run | | --- | --- | --- | --- | --- | --- | | format.apply | `format.apply` | Yes | `conditional` | Yes | Yes | -| format.fontSize | `format.fontSize` | Yes | `conditional` | No | Yes | -| format.fontFamily | `format.fontFamily` | Yes | `conditional` | No | Yes | -| format.color | `format.color` | Yes | `conditional` | No | Yes | +| format.bold | `format.bold` | Yes | `conditional` | Yes | Yes | +| format.italic | `format.italic` | Yes | `conditional` | Yes | Yes | +| format.strike | `format.strike` | Yes | `conditional` | Yes | Yes | +| format.underline | `format.underline` | Yes | `conditional` | Yes | Yes | +| 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.letterSpacing | `format.letterSpacing` | Yes | `conditional` | Yes | Yes | +| format.vertAlign | `format.vertAlign` | Yes | `conditional` | Yes | Yes | +| format.position | `format.position` | Yes | `conditional` | Yes | Yes | +| format.dstrike | `format.dstrike` | Yes | `conditional` | No | Yes | +| format.smallCaps | `format.smallCaps` | Yes | `conditional` | No | Yes | +| format.caps | `format.caps` | Yes | `conditional` | No | Yes | +| format.shading | `format.shading` | Yes | `conditional` | No | Yes | +| format.border | `format.border` | Yes | `conditional` | No | Yes | +| format.outline | `format.outline` | Yes | `conditional` | No | Yes | +| format.shadow | `format.shadow` | Yes | `conditional` | No | Yes | +| format.emboss | `format.emboss` | Yes | `conditional` | No | Yes | +| format.imprint | `format.imprint` | Yes | `conditional` | No | Yes | +| format.charScale | `format.charScale` | Yes | `conditional` | No | Yes | +| format.kerning | `format.kerning` | Yes | `conditional` | No | Yes | +| format.vanish | `format.vanish` | Yes | `conditional` | No | Yes | +| format.webHidden | `format.webHidden` | Yes | `conditional` | No | Yes | +| format.specVanish | `format.specVanish` | Yes | `conditional` | No | Yes | +| format.rtl | `format.rtl` | Yes | `conditional` | No | Yes | +| format.cs | `format.cs` | Yes | `conditional` | No | Yes | +| format.bCs | `format.bCs` | Yes | `conditional` | No | Yes | +| format.iCs | `format.iCs` | Yes | `conditional` | No | Yes | +| format.eastAsianLayout | `format.eastAsianLayout` | Yes | `conditional` | No | Yes | +| format.em | `format.em` | Yes | `conditional` | No | Yes | +| format.fitText | `format.fitText` | Yes | `conditional` | No | Yes | +| format.snapToGrid | `format.snapToGrid` | Yes | `conditional` | No | Yes | +| format.lang | `format.lang` | Yes | `conditional` | No | Yes | +| format.oMath | `format.oMath` | Yes | `conditional` | No | Yes | +| format.rStyle | `format.rStyle` | Yes | `conditional` | No | Yes | +| format.rFonts | `format.rFonts` | Yes | `conditional` | No | Yes | +| format.fontSizeCs | `format.fontSizeCs` | Yes | `conditional` | No | Yes | +| format.ligatures | `format.ligatures` | Yes | `conditional` | No | Yes | +| format.numForm | `format.numForm` | Yes | `conditional` | No | Yes | +| 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 | @@ -25,8 +64,5 @@ Canonical formatting mutation with directive semantics ('on', 'off', 'clear'). | Alias method | Canonical operation | Behavior | | --- | --- | --- | -| `editor.doc.format.bold(...)` | format.apply | Convenience alias for `format.apply` with `inline.bold: 'on'`. | -| `editor.doc.format.italic(...)` | format.apply | Convenience alias for `format.apply` with `inline.italic: 'on'`. | -| `editor.doc.format.underline(...)` | format.apply | Convenience alias for `format.apply` with `inline.underline: 'on'`. | -| `editor.doc.format.strikethrough(...)` | format.apply | Convenience alias for `format.apply` with `inline.strike: 'on'`. | +| `editor.doc.format.strikethrough(...)` | format.strike | Convenience alias for `format.strike` with `value: true`. | diff --git a/apps/docs/document-api/reference/format/italic.mdx b/apps/docs/document-api/reference/format/italic.mdx new file mode 100644 index 0000000000..ba25757f12 --- /dev/null +++ b/apps/docs/document-api/reference/format/italic.mdx @@ -0,0 +1,228 @@ +--- +title: format.italic +sidebarTitle: format.italic +description: Reference for format.italic +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.italic` +- API member path: `editor.doc.format.italic(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `yes` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | boolean \\| null | no | One of: boolean, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": true +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/kerning.mdx b/apps/docs/document-api/reference/format/kerning.mdx new file mode 100644 index 0000000000..54eb6b0c92 --- /dev/null +++ b/apps/docs/document-api/reference/format/kerning.mdx @@ -0,0 +1,229 @@ +--- +title: format.kerning +sidebarTitle: format.kerning +description: Reference for format.kerning +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.kerning` +- API member path: `editor.doc.format.kerning(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | number \\| null | yes | One of: number, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": 12.5 +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target", + "value" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/lang.mdx b/apps/docs/document-api/reference/format/lang.mdx new file mode 100644 index 0000000000..8a8c16ed22 --- /dev/null +++ b/apps/docs/document-api/reference/format/lang.mdx @@ -0,0 +1,269 @@ +--- +title: format.lang +sidebarTitle: format.lang +description: Reference for format.lang +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.lang` +- API member path: `editor.doc.format.lang(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | object \\| null | yes | One of: object, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": { + "eastAsia": "example", + "val": "example" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "additionalProperties": false, + "minProperties": 1, + "properties": { + "bidi": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "eastAsia": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "val": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target", + "value" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/letter-spacing.mdx b/apps/docs/document-api/reference/format/letter-spacing.mdx new file mode 100644 index 0000000000..cfc4f313f6 --- /dev/null +++ b/apps/docs/document-api/reference/format/letter-spacing.mdx @@ -0,0 +1,229 @@ +--- +title: format.letterSpacing +sidebarTitle: format.letterSpacing +description: Reference for format.letterSpacing +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.letterSpacing` +- API member path: `editor.doc.format.letterSpacing(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `yes` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | number \\| null | yes | One of: number, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": 12.5 +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target", + "value" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/ligatures.mdx b/apps/docs/document-api/reference/format/ligatures.mdx new file mode 100644 index 0000000000..c4bbdaf98e --- /dev/null +++ b/apps/docs/document-api/reference/format/ligatures.mdx @@ -0,0 +1,230 @@ +--- +title: format.ligatures +sidebarTitle: format.ligatures +description: Reference for format.ligatures +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.ligatures` +- API member path: `editor.doc.format.ligatures(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | string \\| null | yes | One of: string, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": "example" +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target", + "value" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/num-form.mdx b/apps/docs/document-api/reference/format/num-form.mdx new file mode 100644 index 0000000000..add73888be --- /dev/null +++ b/apps/docs/document-api/reference/format/num-form.mdx @@ -0,0 +1,230 @@ +--- +title: format.numForm +sidebarTitle: format.numForm +description: Reference for format.numForm +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.numForm` +- API member path: `editor.doc.format.numForm(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | string \\| null | yes | One of: string, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": "example" +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target", + "value" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/num-spacing.mdx b/apps/docs/document-api/reference/format/num-spacing.mdx new file mode 100644 index 0000000000..4ea265f5d3 --- /dev/null +++ b/apps/docs/document-api/reference/format/num-spacing.mdx @@ -0,0 +1,230 @@ +--- +title: format.numSpacing +sidebarTitle: format.numSpacing +description: Reference for format.numSpacing +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.numSpacing` +- API member path: `editor.doc.format.numSpacing(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | string \\| null | yes | One of: string, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": "example" +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target", + "value" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/o-math.mdx b/apps/docs/document-api/reference/format/o-math.mdx new file mode 100644 index 0000000000..75a6f41c7d --- /dev/null +++ b/apps/docs/document-api/reference/format/o-math.mdx @@ -0,0 +1,228 @@ +--- +title: format.oMath +sidebarTitle: format.oMath +description: Reference for format.oMath +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.oMath` +- API member path: `editor.doc.format.oMath(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | boolean \\| null | no | One of: boolean, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": true +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/outline.mdx b/apps/docs/document-api/reference/format/outline.mdx new file mode 100644 index 0000000000..c907ff6ce2 --- /dev/null +++ b/apps/docs/document-api/reference/format/outline.mdx @@ -0,0 +1,228 @@ +--- +title: format.outline +sidebarTitle: format.outline +description: Reference for format.outline +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.outline` +- API member path: `editor.doc.format.outline(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | boolean \\| null | no | One of: boolean, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": true +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/position.mdx b/apps/docs/document-api/reference/format/position.mdx new file mode 100644 index 0000000000..a0353bf8c5 --- /dev/null +++ b/apps/docs/document-api/reference/format/position.mdx @@ -0,0 +1,229 @@ +--- +title: format.position +sidebarTitle: format.position +description: Reference for format.position +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.position` +- API member path: `editor.doc.format.position(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `yes` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | number \\| null | yes | One of: number, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": 12.5 +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target", + "value" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/r-fonts.mdx b/apps/docs/document-api/reference/format/r-fonts.mdx new file mode 100644 index 0000000000..cc2c494981 --- /dev/null +++ b/apps/docs/document-api/reference/format/r-fonts.mdx @@ -0,0 +1,335 @@ +--- +title: format.rFonts +sidebarTitle: format.rFonts +description: Reference for format.rFonts +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.rFonts` +- API member path: `editor.doc.format.rFonts(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | object \\| null | yes | One of: object, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": { + "ascii": "example", + "hAnsi": "example" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "additionalProperties": false, + "minProperties": 1, + "properties": { + "ascii": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "asciiTheme": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "cs": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "csTheme": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "eastAsia": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "eastAsiaTheme": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hAnsi": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hAnsiTheme": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hint": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target", + "value" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/r-style.mdx b/apps/docs/document-api/reference/format/r-style.mdx new file mode 100644 index 0000000000..4b49774f77 --- /dev/null +++ b/apps/docs/document-api/reference/format/r-style.mdx @@ -0,0 +1,230 @@ +--- +title: format.rStyle +sidebarTitle: format.rStyle +description: Reference for format.rStyle +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.rStyle` +- API member path: `editor.doc.format.rStyle(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | string \\| null | yes | One of: string, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": "example" +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target", + "value" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/rtl.mdx b/apps/docs/document-api/reference/format/rtl.mdx new file mode 100644 index 0000000000..9b0b331b6b --- /dev/null +++ b/apps/docs/document-api/reference/format/rtl.mdx @@ -0,0 +1,228 @@ +--- +title: format.rtl +sidebarTitle: format.rtl +description: Reference for format.rtl +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.rtl` +- API member path: `editor.doc.format.rtl(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | boolean \\| null | no | One of: boolean, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": true +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/shading.mdx b/apps/docs/document-api/reference/format/shading.mdx new file mode 100644 index 0000000000..57b2b5f11c --- /dev/null +++ b/apps/docs/document-api/reference/format/shading.mdx @@ -0,0 +1,269 @@ +--- +title: format.shading +sidebarTitle: format.shading +description: Reference for format.shading +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.shading` +- API member path: `editor.doc.format.shading(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | object \\| null | yes | One of: object, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": { + "color": "example", + "fill": "example" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "additionalProperties": false, + "minProperties": 1, + "properties": { + "color": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "fill": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "val": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target", + "value" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/shadow.mdx b/apps/docs/document-api/reference/format/shadow.mdx new file mode 100644 index 0000000000..8b6e3cb286 --- /dev/null +++ b/apps/docs/document-api/reference/format/shadow.mdx @@ -0,0 +1,228 @@ +--- +title: format.shadow +sidebarTitle: format.shadow +description: Reference for format.shadow +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.shadow` +- API member path: `editor.doc.format.shadow(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | boolean \\| null | no | One of: boolean, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": true +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/small-caps.mdx b/apps/docs/document-api/reference/format/small-caps.mdx new file mode 100644 index 0000000000..d20069d242 --- /dev/null +++ b/apps/docs/document-api/reference/format/small-caps.mdx @@ -0,0 +1,228 @@ +--- +title: format.smallCaps +sidebarTitle: format.smallCaps +description: Reference for format.smallCaps +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.smallCaps` +- API member path: `editor.doc.format.smallCaps(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | boolean \\| null | no | One of: boolean, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": true +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/snap-to-grid.mdx b/apps/docs/document-api/reference/format/snap-to-grid.mdx new file mode 100644 index 0000000000..53c2f446a3 --- /dev/null +++ b/apps/docs/document-api/reference/format/snap-to-grid.mdx @@ -0,0 +1,228 @@ +--- +title: format.snapToGrid +sidebarTitle: format.snapToGrid +description: Reference for format.snapToGrid +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.snapToGrid` +- API member path: `editor.doc.format.snapToGrid(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | boolean \\| null | no | One of: boolean, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": true +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/spec-vanish.mdx b/apps/docs/document-api/reference/format/spec-vanish.mdx new file mode 100644 index 0000000000..22b57c8866 --- /dev/null +++ b/apps/docs/document-api/reference/format/spec-vanish.mdx @@ -0,0 +1,228 @@ +--- +title: format.specVanish +sidebarTitle: format.specVanish +description: Reference for format.specVanish +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.specVanish` +- API member path: `editor.doc.format.specVanish(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | boolean \\| null | no | One of: boolean, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": true +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/strike.mdx b/apps/docs/document-api/reference/format/strike.mdx new file mode 100644 index 0000000000..5a4b3fe64f --- /dev/null +++ b/apps/docs/document-api/reference/format/strike.mdx @@ -0,0 +1,228 @@ +--- +title: format.strike +sidebarTitle: format.strike +description: Reference for format.strike +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.strike` +- API member path: `editor.doc.format.strike(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `yes` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | boolean \\| null | no | One of: boolean, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": true +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/stylistic-sets.mdx b/apps/docs/document-api/reference/format/stylistic-sets.mdx new file mode 100644 index 0000000000..cd722c994d --- /dev/null +++ b/apps/docs/document-api/reference/format/stylistic-sets.mdx @@ -0,0 +1,250 @@ +--- +title: format.stylisticSets +sidebarTitle: format.stylisticSets +description: Reference for format.stylisticSets +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.stylisticSets` +- API member path: `editor.doc.format.stylisticSets(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | object[] \\| null | yes | One of: object[], null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": [ + { + "id": 12.5, + "val": true + } + ] +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "items": { + "additionalProperties": false, + "properties": { + "id": { + "type": "number" + }, + "val": { + "type": "boolean" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target", + "value" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/underline.mdx b/apps/docs/document-api/reference/format/underline.mdx new file mode 100644 index 0000000000..bbfdab3b9e --- /dev/null +++ b/apps/docs/document-api/reference/format/underline.mdx @@ -0,0 +1,268 @@ +--- +title: format.underline +sidebarTitle: format.underline +description: Reference for format.underline +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.underline` +- API member path: `editor.doc.format.underline(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `yes` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | boolean \\| null \\| object | no | One of: boolean, null, object | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": true +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + }, + { + "additionalProperties": false, + "minProperties": 1, + "properties": { + "color": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "style": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + }, + "themeColor": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/vanish.mdx b/apps/docs/document-api/reference/format/vanish.mdx new file mode 100644 index 0000000000..0a1308fc56 --- /dev/null +++ b/apps/docs/document-api/reference/format/vanish.mdx @@ -0,0 +1,228 @@ +--- +title: format.vanish +sidebarTitle: format.vanish +description: Reference for format.vanish +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.vanish` +- API member path: `editor.doc.format.vanish(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | boolean \\| null | no | One of: boolean, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": true +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/vert-align.mdx b/apps/docs/document-api/reference/format/vert-align.mdx new file mode 100644 index 0000000000..8168c669c4 --- /dev/null +++ b/apps/docs/document-api/reference/format/vert-align.mdx @@ -0,0 +1,233 @@ +--- +title: format.vertAlign +sidebarTitle: format.vertAlign +description: Reference for format.vertAlign +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.vertAlign` +- API member path: `editor.doc.format.vertAlign(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `yes` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | enum \\| null | yes | One of: enum, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": "superscript" +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "enum": [ + "superscript", + "subscript", + "baseline" + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target", + "value" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/format/web-hidden.mdx b/apps/docs/document-api/reference/format/web-hidden.mdx new file mode 100644 index 0000000000..b11689b4fd --- /dev/null +++ b/apps/docs/document-api/reference/format/web-hidden.mdx @@ -0,0 +1,228 @@ +--- +title: format.webHidden +sidebarTitle: format.webHidden +description: Reference for format.webHidden +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +- Operation ID: `format.webHidden` +- API member path: `editor.doc.format.webHidden(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `target` | TextAddress | yes | TextAddress | +| `value` | boolean \\| null | no | One of: boolean, null | + +### Example request + +```json +{ + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "value": true +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{ + "inserted": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ], + "resolution": { + "range": { + "from": 0, + "to": 10 + }, + "requestedTarget": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "target": { + "blockId": "block-abc123", + "kind": "text", + "range": { + "end": 10, + "start": 0 + } + }, + "text": "Hello, world." + }, + "success": true, + "updated": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/$defs/TextAddress" + }, + "value": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "$ref": "#/$defs/TextMutationSuccess" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "$ref": "#/$defs/TextMutationSuccess" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "$ref": "#/$defs/TextMutationResolution" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/index.mdx b/apps/docs/document-api/reference/index.mdx index ae1ef0a061..cb868d2576 100644 --- a/apps/docs/document-api/reference/index.mdx +++ b/apps/docs/document-api/reference/index.mdx @@ -25,7 +25,7 @@ Document API is currently alpha and subject to breaking changes. | Capabilities | 1 | 0 | 1 | [Open](/document-api/reference/capabilities/index) | | Create | 4 | 0 | 4 | [Open](/document-api/reference/create/index) | | Sections | 18 | 0 | 18 | [Open](/document-api/reference/sections/index) | -| Format | 5 | 4 | 9 | [Open](/document-api/reference/format/index) | +| Format | 44 | 1 | 45 | [Open](/document-api/reference/format/index) | | Styles | 1 | 0 | 1 | [Open](/document-api/reference/styles/index) | | Lists | 8 | 0 | 8 | [Open](/document-api/reference/lists/index) | | Comments | 5 | 0 | 5 | [Open](/document-api/reference/comments/index) | @@ -99,15 +99,51 @@ The tables below are grouped by namespace. | Operation | API member path | Description | | --- | --- | --- | -| format.apply | editor.doc.format.apply(...) | Apply explicit inline style changes (bold, italic, underline, strike) to the target range using directive semantics ('on', 'off', 'clear'). | -| format.fontSize | editor.doc.format.fontSize(...) | Set or unset the font size on the target text range. Pass null to remove. | -| format.fontFamily | editor.doc.format.fontFamily(...) | Set or unset the font family on the target text range. Pass null to remove. | -| format.color | editor.doc.format.color(...) | Set or unset the text color on the target text range. Pass null to remove. | +| format.apply | editor.doc.format.apply(...) | Apply inline run-property patch changes to the target range with explicit set/clear semantics. | +| format.bold | editor.doc.format.bold(...) | Set or clear the `bold` inline run property on the target text range. | +| format.italic | editor.doc.format.italic(...) | Set or clear the `italic` inline run property on the target text range. | +| format.strike | editor.doc.format.strike(...) | Set or clear the `strike` inline run property on the target text range. | +| format.underline | editor.doc.format.underline(...) | Set or clear the `underline` inline run property on the target text range. | +| 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.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. | +| format.dstrike | editor.doc.format.dstrike(...) | Set or clear the `dstrike` inline run property on the target text range. | +| format.smallCaps | editor.doc.format.smallCaps(...) | Set or clear the `smallCaps` inline run property on the target text range. | +| format.caps | editor.doc.format.caps(...) | Set or clear the `caps` inline run property on the target text range. | +| format.shading | editor.doc.format.shading(...) | Set or clear the `shading` inline run property on the target text range. | +| format.border | editor.doc.format.border(...) | Set or clear the `border` inline run property on the target text range. | +| format.outline | editor.doc.format.outline(...) | Set or clear the `outline` inline run property on the target text range. | +| format.shadow | editor.doc.format.shadow(...) | Set or clear the `shadow` inline run property on the target text range. | +| format.emboss | editor.doc.format.emboss(...) | Set or clear the `emboss` inline run property on the target text range. | +| format.imprint | editor.doc.format.imprint(...) | Set or clear the `imprint` inline run property on the target text range. | +| format.charScale | editor.doc.format.charScale(...) | Set or clear the `charScale` inline run property on the target text range. | +| format.kerning | editor.doc.format.kerning(...) | Set or clear the `kerning` inline run property on the target text range. | +| format.vanish | editor.doc.format.vanish(...) | Set or clear the `vanish` inline run property on the target text range. | +| format.webHidden | editor.doc.format.webHidden(...) | Set or clear the `webHidden` inline run property on the target text range. | +| format.specVanish | editor.doc.format.specVanish(...) | Set or clear the `specVanish` inline run property on the target text range. | +| format.rtl | editor.doc.format.rtl(...) | Set or clear the `rtl` inline run property on the target text range. | +| format.cs | editor.doc.format.cs(...) | Set or clear the `cs` inline run property on the target text range. | +| format.bCs | editor.doc.format.bCs(...) | Set or clear the `bCs` inline run property on the target text range. | +| format.iCs | editor.doc.format.iCs(...) | Set or clear the `iCs` inline run property on the target text range. | +| format.eastAsianLayout | editor.doc.format.eastAsianLayout(...) | Set or clear the `eastAsianLayout` inline run property on the target text range. | +| format.em | editor.doc.format.em(...) | Set or clear the `em` inline run property on the target text range. | +| format.fitText | editor.doc.format.fitText(...) | Set or clear the `fitText` inline run property on the target text range. | +| format.snapToGrid | editor.doc.format.snapToGrid(...) | Set or clear the `snapToGrid` inline run property on the target text range. | +| format.lang | editor.doc.format.lang(...) | Set or clear the `lang` inline run property on the target text range. | +| format.oMath | editor.doc.format.oMath(...) | Set or clear the `oMath` inline run property on the target text range. | +| format.rStyle | editor.doc.format.rStyle(...) | Set or clear the `rStyle` inline run property on the target text range. | +| format.rFonts | editor.doc.format.rFonts(...) | Set or clear the `rFonts` inline run property on the target text range. | +| format.fontSizeCs | editor.doc.format.fontSizeCs(...) | Set or clear the `fontSizeCs` inline run property on the target text range. | +| format.ligatures | editor.doc.format.ligatures(...) | Set or clear the `ligatures` inline run property on the target text range. | +| format.numForm | editor.doc.format.numForm(...) | Set or clear the `numForm` inline run property on the target text range. | +| 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.bold | editor.doc.format.bold(...) | Convenience alias for `format.apply` with `inline.bold: 'on'`. | -| format.italic | editor.doc.format.italic(...) | Convenience alias for `format.apply` with `inline.italic: 'on'`. | -| format.underline | editor.doc.format.underline(...) | Convenience alias for `format.apply` with `inline.underline: 'on'`. | -| format.strikethrough | editor.doc.format.strikethrough(...) | Convenience alias for `format.apply` with `inline.strike: 'on'`. | +| format.strikethrough | editor.doc.format.strikethrough(...) | Convenience alias for `format.strike` with `value: true`. | #### Styles diff --git a/apps/docs/document-engine/sdks.mdx b/apps/docs/document-engine/sdks.mdx index 2cbacb9d64..5110b580bc 100644 --- a/apps/docs/document-engine/sdks.mdx +++ b/apps/docs/document-engine/sdks.mdx @@ -209,10 +209,49 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p | Operation | CLI command | Description | | --- | --- | --- | -| `doc.format.apply` | `format apply` | Apply explicit inline style changes (bold, italic, underline, strike) to the target range using directive semantics ('on', 'off', 'clear'). | -| `doc.format.fontSize` | `format font-size` | Set or unset the font size on the target text range. Pass null to remove. | -| `doc.format.fontFamily` | `format font-family` | Set or unset the font family on the target text range. Pass null to remove. | -| `doc.format.color` | `format color` | Set or unset the text color on the target text range. Pass null to remove. | +| `doc.format.apply` | `format apply` | Apply inline run-property patch changes to the target range with explicit set/clear semantics. | +| `doc.format.bold` | `format bold` | Set or clear the `bold` inline run property on the target text range. | +| `doc.format.italic` | `format italic` | Set or clear the `italic` inline run property on the target text range. | +| `doc.format.strike` | `format strike` | Set or clear the `strike` inline run property on the target text range. | +| `doc.format.underline` | `format underline` | Set or clear the `underline` inline run property on the target text range. | +| `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.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. | +| `doc.format.dstrike` | `format dstrike` | Set or clear the `dstrike` inline run property on the target text range. | +| `doc.format.smallCaps` | `format small-caps` | Set or clear the `smallCaps` inline run property on the target text range. | +| `doc.format.caps` | `format caps` | Set or clear the `caps` inline run property on the target text range. | +| `doc.format.shading` | `format shading` | Set or clear the `shading` inline run property on the target text range. | +| `doc.format.border` | `format border` | Set or clear the `border` inline run property on the target text range. | +| `doc.format.outline` | `format outline` | Set or clear the `outline` inline run property on the target text range. | +| `doc.format.shadow` | `format shadow` | Set or clear the `shadow` inline run property on the target text range. | +| `doc.format.emboss` | `format emboss` | Set or clear the `emboss` inline run property on the target text range. | +| `doc.format.imprint` | `format imprint` | Set or clear the `imprint` inline run property on the target text range. | +| `doc.format.charScale` | `format char-scale` | Set or clear the `charScale` inline run property on the target text range. | +| `doc.format.kerning` | `format kerning` | Set or clear the `kerning` inline run property on the target text range. | +| `doc.format.vanish` | `format vanish` | Set or clear the `vanish` inline run property on the target text range. | +| `doc.format.webHidden` | `format web-hidden` | Set or clear the `webHidden` inline run property on the target text range. | +| `doc.format.specVanish` | `format spec-vanish` | Set or clear the `specVanish` inline run property on the target text range. | +| `doc.format.rtl` | `format rtl` | Set or clear the `rtl` inline run property on the target text range. | +| `doc.format.cs` | `format cs` | Set or clear the `cs` inline run property on the target text range. | +| `doc.format.bCs` | `format b-cs` | Set or clear the `bCs` inline run property on the target text range. | +| `doc.format.iCs` | `format i-cs` | Set or clear the `iCs` inline run property on the target text range. | +| `doc.format.eastAsianLayout` | `format east-asian-layout` | Set or clear the `eastAsianLayout` inline run property on the target text range. | +| `doc.format.em` | `format em` | Set or clear the `em` inline run property on the target text range. | +| `doc.format.fitText` | `format fit-text` | Set or clear the `fitText` inline run property on the target text range. | +| `doc.format.snapToGrid` | `format snap-to-grid` | Set or clear the `snapToGrid` inline run property on the target text range. | +| `doc.format.lang` | `format lang` | Set or clear the `lang` inline run property on the target text range. | +| `doc.format.oMath` | `format o-math` | Set or clear the `oMath` inline run property on the target text range. | +| `doc.format.rStyle` | `format r-style` | Set or clear the `rStyle` inline run property on the target text range. | +| `doc.format.rFonts` | `format r-fonts` | Set or clear the `rFonts` inline run property on the target text range. | +| `doc.format.fontSizeCs` | `format font-size-cs` | Set or clear the `fontSizeCs` inline run property on the target text range. | +| `doc.format.ligatures` | `format ligatures` | Set or clear the `ligatures` inline run property on the target text range. | +| `doc.format.numForm` | `format num-form` | Set or clear the `numForm` inline run property on the target text range. | +| `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/scripts/check-contract-parity.ts b/packages/document-api/scripts/check-contract-parity.ts index 2a06f0174e..6b6659c906 100644 --- a/packages/document-api/scripts/check-contract-parity.ts +++ b/packages/document-api/scripts/check-contract-parity.ts @@ -71,7 +71,11 @@ function createNoopAdapters(): DocumentApiAdapters { lists: { enabled: false }, dryRun: { enabled: false }, }, - format: { properties: {} }, + format: { + supportedInlineProperties: {} as ReturnType< + DocumentApiAdapters['capabilities']['get'] + >['format']['supportedInlineProperties'], + }, operations: {} as ReturnType['operations'], planEngine: { supportedStepOps: [], diff --git a/packages/document-api/src/capabilities/capabilities.ts b/packages/document-api/src/capabilities/capabilities.ts index 21ecac5f98..50b8be5196 100644 --- a/packages/document-api/src/capabilities/capabilities.ts +++ b/packages/document-api/src/capabilities/capabilities.ts @@ -1,4 +1,5 @@ import type { OperationId } from '../contract/types.js'; +import type { InlinePropertyStorage, InlinePropertyType, InlineRunPatchKey } from '../format/inline-run-patch.js'; export const CAPABILITY_REASON_CODES = [ 'COMMAND_UNAVAILABLE', @@ -54,16 +55,22 @@ export interface PlanEngineCapabilities { * `operations` contains per-operation availability details keyed by {@link OperationId}. * `planEngine` describes plan engine capabilities (step ops, style strategies, limits). */ -/** Per-property capability describing the interaction model and accepted directives. */ -export interface FormatPropertyCapability { - kind: 'toggle' | 'value' | 'composite'; - directives: readonly string[]; +/** Per-inline-property runtime capability for `format.apply`. */ +export interface InlinePropertyCapability { + /** Whether this specific property is currently executable. */ + available: boolean; + /** Whether this property supports tracked mode. */ + tracked: boolean; + /** API value shape for this property. */ + type: InlinePropertyType; + /** Runtime storage path used by the editor (mark or runAttribute). */ + storage: InlinePropertyStorage; } -/** Format capability snapshot — advertises per-property capability objects. */ +/** Format capability snapshot — advertises per-property support for `format.apply`. */ export interface FormatCapabilities { - /** Per-property capability objects keyed by mark name. */ - properties: Partial>; + /** Capability entry per canonical inline patch key. */ + supportedInlineProperties: Record; } export interface DocumentApiCapabilities { diff --git a/packages/document-api/src/contract/operation-definitions.ts b/packages/document-api/src/contract/operation-definitions.ts index 47c8e35c16..2468d55841 100644 --- a/packages/document-api/src/contract/operation-definitions.ts +++ b/packages/document-api/src/contract/operation-definitions.ts @@ -26,6 +26,7 @@ import type { ReceiptFailureCode } from '../types/receipt.js'; import type { CommandStaticMetadata, OperationIdempotency, PreApplyThrowCode } from './metadata-types.js'; +import { INLINE_PROPERTY_REGISTRY, type InlineRunPatchKey } from '../format/inline-run-patch.js'; // --------------------------------------------------------------------------- // Reference group key @@ -162,6 +163,40 @@ const T_SECTION_MUTATION = [ ] as const; const T_SECTION_SETTINGS_MUTATION = ['INVALID_INPUT', 'CAPABILITY_UNAVAILABLE', 'INTERNAL_ERROR'] as const; +type FormatInlineAliasOperationId = `format.${InlineRunPatchKey}`; + +function camelToKebab(value: string): string { + return value.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`); +} + +function formatInlineAliasDescription(key: InlineRunPatchKey): string { + return `Set or clear the \`${key}\` inline run property on the target text range.`; +} + +const FORMAT_INLINE_ALIAS_OPERATION_DEFINITIONS: Record = + Object.fromEntries( + INLINE_PROPERTY_REGISTRY.map((entry) => { + const operationId = `format.${entry.key}` as FormatInlineAliasOperationId; + const definition: OperationDefinitionEntry = { + memberPath: operationId, + description: formatInlineAliasDescription(entry.key), + expectedResult: + 'Returns a TextMutationReceipt confirming the inline run property patch was applied to the target range.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: entry.tracked, + possibleFailureCodes: ['INVALID_TARGET'], + throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET', 'INVALID_INPUT'], + }), + referenceDocPath: `format/${camelToKebab(entry.key)}.mdx`, + referenceGroup: 'format', + }; + return [operationId, definition]; + }), + ) as Record; + // --------------------------------------------------------------------------- // Canonical definitions // --------------------------------------------------------------------------- @@ -299,8 +334,7 @@ export const OPERATION_DEFINITIONS = { 'format.apply': { memberPath: 'format.apply', - description: - "Apply explicit inline style changes (bold, italic, underline, strike) to the target range using directive semantics ('on', 'off', 'clear').", + description: 'Apply inline run-property patch changes to the target range with explicit set/clear semantics.', expectedResult: 'Returns a TextMutationReceipt confirming inline styles were applied to the target range.', requiresDocumentContext: true, metadata: mutationOperation({ @@ -313,54 +347,7 @@ export const OPERATION_DEFINITIONS = { referenceDocPath: 'format/apply.mdx', referenceGroup: 'format', }, - 'format.fontSize': { - memberPath: 'format.fontSize', - description: 'Set or unset the font size on the target text range. Pass null to remove.', - expectedResult: - 'Returns a TextMutationReceipt; receipt reports NO_OP if the target already has the requested font size.', - requiresDocumentContext: true, - metadata: mutationOperation({ - idempotency: 'conditional', - supportsDryRun: true, - supportsTrackedMode: false, - possibleFailureCodes: ['INVALID_TARGET', 'NO_OP'], - throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET', 'INVALID_INPUT'], - }), - referenceDocPath: 'format/font-size.mdx', - referenceGroup: 'format', - }, - 'format.fontFamily': { - memberPath: 'format.fontFamily', - description: 'Set or unset the font family on the target text range. Pass null to remove.', - expectedResult: - 'Returns a TextMutationReceipt; receipt reports NO_OP if the target already has the requested font family.', - requiresDocumentContext: true, - metadata: mutationOperation({ - idempotency: 'conditional', - supportsDryRun: true, - supportsTrackedMode: false, - possibleFailureCodes: ['INVALID_TARGET', 'NO_OP'], - throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET', 'INVALID_INPUT'], - }), - referenceDocPath: 'format/font-family.mdx', - referenceGroup: 'format', - }, - 'format.color': { - memberPath: 'format.color', - description: 'Set or unset the text color on the target text range. Pass null to remove.', - expectedResult: - 'Returns a TextMutationReceipt; receipt reports NO_OP if the target already has the requested color.', - requiresDocumentContext: true, - metadata: mutationOperation({ - idempotency: 'conditional', - supportsDryRun: true, - supportsTrackedMode: false, - possibleFailureCodes: ['INVALID_TARGET', 'NO_OP'], - throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET', 'INVALID_INPUT'], - }), - referenceDocPath: 'format/color.mdx', - 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.', diff --git a/packages/document-api/src/contract/operation-registry.ts b/packages/document-api/src/contract/operation-registry.ts index 5a84c2f174..2156b8b631 100644 --- a/packages/document-api/src/contract/operation-registry.ts +++ b/packages/document-api/src/contract/operation-registry.ts @@ -27,13 +27,8 @@ 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 { - StyleApplyInput, - FormatFontSizeInput, - FormatFontFamilyInput, - FormatColorInput, - FormatAlignInput, -} from '../format/format.js'; +import type { FormatInlineAliasInput, StyleApplyInput, FormatAlignInput } from '../format/format.js'; +import type { InlineRunPatchKey } from '../format/inline-run-patch.js'; import type { StylesApplyInput, StylesApplyOptions, StylesApplyReceipt } from '../styles/styles.js'; import type { CommentsCreateInput, @@ -137,7 +132,15 @@ import type { TablesGetPropertiesOutput, } from '../types/table-operations.types.js'; -export interface OperationRegistry { +type FormatInlineAliasOperationRegistry = { + [K in InlineRunPatchKey as `format.${K}`]: { + input: FormatInlineAliasInput; + options: MutationOptions; + output: TextMutationReceipt; + }; +}; + +export interface OperationRegistry extends FormatInlineAliasOperationRegistry { // --- Singleton reads --- find: { input: Selector | Query; options: FindOptions; output: FindOutput }; getNode: { input: NodeAddress; options: never; output: NodeInfo }; @@ -155,9 +158,6 @@ export interface OperationRegistry { // --- format.* --- 'format.apply': { input: StyleApplyInput; options: MutationOptions; output: TextMutationReceipt }; - 'format.fontSize': { input: FormatFontSizeInput; options: MutationOptions; output: TextMutationReceipt }; - 'format.fontFamily': { input: FormatFontFamilyInput; options: MutationOptions; output: TextMutationReceipt }; - 'format.color': { input: FormatColorInput; options: MutationOptions; output: TextMutationReceipt }; 'format.align': { input: FormatAlignInput; options: MutationOptions; output: TextMutationReceipt }; // --- styles.* --- diff --git a/packages/document-api/src/contract/reference-aliases.ts b/packages/document-api/src/contract/reference-aliases.ts index c4354b65c1..e3eeef90e9 100644 --- a/packages/document-api/src/contract/reference-aliases.ts +++ b/packages/document-api/src/contract/reference-aliases.ts @@ -19,28 +19,10 @@ export interface ReferenceAliasDefinition { } export const REFERENCE_OPERATION_ALIASES: readonly ReferenceAliasDefinition[] = [ - { - memberPath: 'format.bold', - canonicalOperationId: 'format.apply', - referenceGroup: 'format', - description: "Convenience alias for `format.apply` with `inline.bold: 'on'`.", - }, - { - memberPath: 'format.italic', - canonicalOperationId: 'format.apply', - referenceGroup: 'format', - description: "Convenience alias for `format.apply` with `inline.italic: 'on'`.", - }, - { - memberPath: 'format.underline', - canonicalOperationId: 'format.apply', - referenceGroup: 'format', - description: "Convenience alias for `format.apply` with `inline.underline: 'on'`.", - }, { memberPath: 'format.strikethrough', - canonicalOperationId: 'format.apply', + canonicalOperationId: 'format.strike', referenceGroup: 'format', - description: "Convenience alias for `format.apply` with `inline.strike: 'on'`.", + description: 'Convenience alias for `format.strike` with `value: true`.', }, ] as const; diff --git a/packages/document-api/src/contract/schemas.ts b/packages/document-api/src/contract/schemas.ts index b5d80be4a4..cf421dd358 100644 --- a/packages/document-api/src/contract/schemas.ts +++ b/packages/document-api/src/contract/schemas.ts @@ -1,8 +1,9 @@ import { COMMAND_CATALOG } from './command-catalog.js'; import { CONTRACT_VERSION, JSON_SCHEMA_DIALECT, OPERATION_IDS, type OperationId } from './types.js'; import { NODE_TYPES, BLOCK_NODE_TYPES, DELETABLE_BLOCK_NODE_TYPES, INLINE_NODE_TYPES } from '../types/base.js'; -import { MARK_KEYS, INLINE_DIRECTIVES } from '../types/style-policy.types.js'; import { ALIGNMENTS } from '../format/format.js'; +import { INLINE_PROPERTY_REGISTRY, buildInlineRunPatchSchema } from '../format/inline-run-patch.js'; +import { INLINE_DIRECTIVES } from '../types/style-policy.types.js'; type JsonSchema = Record; @@ -365,7 +366,7 @@ void matchRunSchema; /** * Builds a DiscoveryResult schema wrapping the given item schema. - * When `metaSchema` is provided, the result includes a required `meta` field. + * When `metaSchema` is provided, a required `meta` field is added to the envelope. */ function discoveryResultSchema(itemSchema: JsonSchema, metaSchema?: JsonSchema): JsonSchema { const properties: Record = { @@ -1103,21 +1104,29 @@ const operationCapabilitiesSchema = objectSchema( OPERATION_IDS, ); -const formatPropertyCapabilitySchema = objectSchema( +const inlinePropertyCapabilitySchema = objectSchema( { - kind: { type: 'string' }, - directives: arraySchema({ type: 'string' }), + available: { type: 'boolean' }, + tracked: { type: 'boolean' }, + type: { enum: ['boolean', 'string', 'number', 'object', 'array'] }, + storage: { enum: ['mark', 'runAttribute'] }, }, - ['kind', 'directives'], + ['available', 'tracked', 'type', 'storage'], +); + +const inlinePropertyCapabilitiesByKeySchema = objectSchema( + Object.fromEntries(INLINE_PROPERTY_REGISTRY.map((entry) => [entry.key, inlinePropertyCapabilitySchema])) as Record< + string, + JsonSchema + >, + INLINE_PROPERTY_REGISTRY.map((entry) => entry.key), ); const formatCapabilitiesSchema = objectSchema( { - properties: objectSchema( - Object.fromEntries(MARK_KEYS.map((key) => [key, formatPropertyCapabilitySchema])) as Record, - ), + supportedInlineProperties: inlinePropertyCapabilitiesByKeySchema, }, - ['properties'], + ['supportedInlineProperties'], ); const planEngineCapabilitiesSchema = objectSchema( @@ -1282,6 +1291,35 @@ const createTableResultSchema: JsonSchema = { oneOf: [createTableSuccessSchema, tableMutationFailureSchema], }; +type FormatInlineAliasOperationId = `format.${(typeof INLINE_PROPERTY_REGISTRY)[number]['key']}`; + +function supportsImplicitTrueValue(operationId: FormatInlineAliasOperationId): boolean { + const key = operationId.slice('format.'.length); + const entry = INLINE_PROPERTY_REGISTRY.find((candidate) => candidate.key === key); + if (!entry) return false; + return entry.type === 'boolean' || key === 'underline'; +} + +const formatInlineAliasOperationSchemas: Record = Object.fromEntries( + INLINE_PROPERTY_REGISTRY.map((entry) => { + const operationId = `format.${entry.key}` as FormatInlineAliasOperationId; + const requiredFields = supportsImplicitTrueValue(operationId) ? ['target'] : ['target', 'value']; + const schema: OperationSchemaSet = { + input: objectSchema( + { + target: textAddressSchema, + value: entry.schema, + }, + requiredFields, + ), + output: textMutationResultSchemaFor(operationId), + success: textMutationSuccessSchema, + failure: textMutationFailureSchemaFor(operationId), + }; + return [operationId, schema]; + }), +) as Record; + const operationSchemas: Record = { find: { input: findInputSchema, @@ -1342,19 +1380,7 @@ const operationSchemas: Record = { input: objectSchema( { target: textAddressSchema, - inline: (() => { - const directiveSchema: JsonSchema = { enum: [...INLINE_DIRECTIVES] }; - const markProperties = Object.fromEntries(MARK_KEYS.map((key) => [key, directiveSchema])) as Record< - string, - JsonSchema - >; - return { - type: 'object', - properties: markProperties, - additionalProperties: false, - minProperties: 1, - } as JsonSchema; - })(), + inline: buildInlineRunPatchSchema(), }, ['target', 'inline'], ), @@ -1362,42 +1388,7 @@ const operationSchemas: Record = { success: textMutationSuccessSchema, failure: textMutationFailureSchemaFor('format.apply'), }, - 'format.fontSize': { - input: objectSchema( - { - target: textAddressSchema, - value: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'number' }, { type: 'null' }] }, - }, - ['target', 'value'], - ), - output: textMutationResultSchemaFor('format.fontSize'), - success: textMutationSuccessSchema, - failure: textMutationFailureSchemaFor('format.fontSize'), - }, - 'format.fontFamily': { - input: objectSchema( - { - target: textAddressSchema, - value: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, - }, - ['target', 'value'], - ), - output: textMutationResultSchemaFor('format.fontFamily'), - success: textMutationSuccessSchema, - failure: textMutationFailureSchemaFor('format.fontFamily'), - }, - 'format.color': { - input: objectSchema( - { - target: textAddressSchema, - value: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, - }, - ['target', 'value'], - ), - output: textMutationResultSchemaFor('format.color'), - success: textMutationSuccessSchema, - failure: textMutationFailureSchemaFor('format.color'), - }, + ...formatInlineAliasOperationSchemas, 'format.align': { input: objectSchema( { diff --git a/packages/document-api/src/format/format.test.ts b/packages/document-api/src/format/format.test.ts index 7b42448bc1..1567b49c04 100644 --- a/packages/document-api/src/format/format.test.ts +++ b/packages/document-api/src/format/format.test.ts @@ -1,6 +1,6 @@ -import { describe, expect, it, vi } from 'vitest'; -import type { FormatAdapter, StyleApplyInput } from './format.js'; -import { executeStyleApply, executeFontSize, executeFontFamily, executeColor, executeAlign } from './format.js'; +import { describe, expect, it, vi, assertType } from 'vitest'; +import type { FormatAdapter, FormatInlineAliasInput, StyleApplyInput } from './format.js'; +import { executeStyleApply, executeAlign, executeInlineAlias } from './format.js'; import { DocumentApiValidationError } from '../errors.js'; import type { TextMutationReceipt } from '../types/index.js'; @@ -22,18 +22,11 @@ function makeReceipt(): TextMutationReceipt { function makeAdapter(): FormatAdapter & Record> { return { apply: vi.fn(() => makeReceipt()), - fontSize: vi.fn(() => makeReceipt()), - fontFamily: vi.fn(() => makeReceipt()), - color: vi.fn(() => makeReceipt()), align: vi.fn(() => makeReceipt()), }; } describe('executeStyleApply validation', () => { - // ------------------------------------------------------------------------- - // Input shape guards - // ------------------------------------------------------------------------- - it('rejects non-object input', () => { const adapter = makeAdapter(); expect(() => executeStyleApply(adapter, null as any)).toThrow(DocumentApiValidationError); @@ -41,131 +34,37 @@ describe('executeStyleApply validation', () => { expect(() => executeStyleApply(adapter, 'bad' as any)).toThrow('non-null object'); }); - // ------------------------------------------------------------------------- - // Unknown field rejection - // ------------------------------------------------------------------------- - it('rejects unknown top-level fields', () => { const adapter = makeAdapter(); - const input = { target: TARGET, inline: { bold: 'on' }, extra: 1 }; + const input = { target: TARGET, inline: { bold: true }, extra: 1 }; expect(() => executeStyleApply(adapter, input as any)).toThrow('extra'); }); - // ------------------------------------------------------------------------- - // Target validation - // ------------------------------------------------------------------------- - it('rejects missing target', () => { const adapter = makeAdapter(); - const input = { inline: { bold: 'on' } }; + const input = { inline: { bold: true } }; expect(() => executeStyleApply(adapter, input as any)).toThrow('requires a target'); }); - it('rejects invalid target (string)', () => { - const adapter = makeAdapter(); - const input = { target: 'not-an-address', inline: { bold: 'on' } }; - expect(() => executeStyleApply(adapter, input as any)).toThrow('text address'); - }); - - it('rejects invalid target (number)', () => { - const adapter = makeAdapter(); - const input = { target: 42, inline: { bold: 'on' } }; - expect(() => executeStyleApply(adapter, input as any)).toThrow('text address'); - }); - - it('rejects invalid target (null)', () => { - const adapter = makeAdapter(); - const input = { target: null, inline: { bold: 'on' } }; - expect(() => executeStyleApply(adapter, input as any)).toThrow('text address'); - }); - - it('rejects target missing kind', () => { - const adapter = makeAdapter(); - const input = { target: { blockId: 'p1', range: { start: 0, end: 5 } }, inline: { bold: 'on' } }; - expect(() => executeStyleApply(adapter, input as any)).toThrow('text address'); - }); - - it('rejects target with wrong kind', () => { - const adapter = makeAdapter(); - const input = { target: { kind: 'block', blockId: 'p1', range: { start: 0, end: 5 } }, inline: { bold: 'on' } }; - expect(() => executeStyleApply(adapter, input as any)).toThrow('text address'); - }); - - it('rejects target missing blockId', () => { - const adapter = makeAdapter(); - const input = { target: { kind: 'text', range: { start: 0, end: 5 } }, inline: { bold: 'on' } }; - expect(() => executeStyleApply(adapter, input as any)).toThrow('text address'); - }); - - it('rejects target with non-string blockId', () => { + it('rejects invalid target', () => { const adapter = makeAdapter(); - const input = { target: { kind: 'text', blockId: 123, range: { start: 0, end: 5 } }, inline: { bold: 'on' } }; - expect(() => executeStyleApply(adapter, input as any)).toThrow('text address'); - }); - - it('rejects target missing range', () => { - const adapter = makeAdapter(); - const input = { target: { kind: 'text', blockId: 'p1' }, inline: { bold: 'on' } }; - expect(() => executeStyleApply(adapter, input as any)).toThrow('text address'); - }); - - it('rejects target with non-object range', () => { - const adapter = makeAdapter(); - const input = { target: { kind: 'text', blockId: 'p1', range: 'bad' }, inline: { bold: 'on' } }; - expect(() => executeStyleApply(adapter, input as any)).toThrow('text address'); - }); - - it('rejects target with non-integer start in range', () => { - const adapter = makeAdapter(); - const input = { target: { kind: 'text', blockId: 'p1', range: { start: 1.5, end: 5 } }, inline: { bold: 'on' } }; - expect(() => executeStyleApply(adapter, input as any)).toThrow('text address'); - }); - - it('rejects target with non-integer end in range', () => { - const adapter = makeAdapter(); - const input = { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5.5 } }, inline: { bold: 'on' } }; - expect(() => executeStyleApply(adapter, input as any)).toThrow('text address'); - }); - - it('rejects target with start > end in range', () => { - const adapter = makeAdapter(); - const input = { target: { kind: 'text', blockId: 'p1', range: { start: 10, end: 5 } }, inline: { bold: 'on' } }; + const input = { target: 'not-an-address', inline: { bold: true } }; expect(() => executeStyleApply(adapter, input as any)).toThrow('text address'); }); it('accepts valid target', () => { const adapter = makeAdapter(); - const input: StyleApplyInput = { target: TARGET, inline: { bold: 'on' } }; + const input: StyleApplyInput = { target: TARGET, inline: { bold: true } }; const result = executeStyleApply(adapter, input); expect(result.success).toBe(true); }); - it('accepts zero-length range (start === end) in target', () => { - const adapter = makeAdapter(); - const input: StyleApplyInput = { - target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 0 } }, - inline: { bold: 'on' }, - }; - const result = executeStyleApply(adapter, input); - expect(result.success).toBe(true); - }); - - // ------------------------------------------------------------------------- - // Inline-style validation - // ------------------------------------------------------------------------- - it('rejects missing inline', () => { const adapter = makeAdapter(); const input = { target: TARGET }; expect(() => executeStyleApply(adapter, input as any)).toThrow('requires an inline object'); }); - it('rejects null inline', () => { - const adapter = makeAdapter(); - const input = { target: TARGET, inline: null }; - expect(() => executeStyleApply(adapter, input as any)).toThrow('requires an inline object'); - }); - it('rejects non-object inline', () => { const adapter = makeAdapter(); const input = { target: TARGET, inline: 'bold' }; @@ -180,106 +79,51 @@ describe('executeStyleApply validation', () => { it('rejects unknown inline keys', () => { const adapter = makeAdapter(); - const input = { target: TARGET, inline: { bold: 'on', superscript: 'on' } }; + const input = { target: TARGET, inline: { superscript: true } }; expect(() => executeStyleApply(adapter, input as any)).toThrow('Unknown inline style key "superscript"'); }); - it('rejects invalid directive string values', () => { + it('rejects invalid boolean payload type', () => { const adapter = makeAdapter(); const input = { target: TARGET, inline: { bold: 'yes' } }; - expect(() => executeStyleApply(adapter, input as any)).toThrow("expected 'on'|'off'|'clear'"); + expect(() => executeStyleApply(adapter, input as any)).toThrow('inline.bold must be boolean or null'); }); - it('rejects numeric inline values', () => { + it('rejects empty object patch values', () => { const adapter = makeAdapter(); - const input = { target: TARGET, inline: { bold: 1 } }; - expect(() => executeStyleApply(adapter, input as any)).toThrow("expected 'on'|'off'|'clear'"); + const input = { target: TARGET, inline: { shading: {} } }; + expect(() => executeStyleApply(adapter, input as any)).toThrow('inline.shading object must not be empty'); }); - it('rejects boolean inline values (must be string directive)', () => { - const adapter = makeAdapter(); - const input = { target: TARGET, inline: { bold: true } }; - expect(() => executeStyleApply(adapter, input as any)).toThrow("expected 'on'|'off'|'clear'"); - }); - - // ------------------------------------------------------------------------- - // Happy paths — single inline style - // ------------------------------------------------------------------------- - - it('delegates single mark to adapter.apply', () => { + it('accepts boolean tri-state payloads', () => { const adapter = makeAdapter(); - const input: StyleApplyInput = { target: TARGET, inline: { bold: 'on' } }; + const input: StyleApplyInput = { target: TARGET, inline: { bold: null, italic: false } }; const result = executeStyleApply(adapter, input); expect(result.success).toBe(true); - expect(adapter.apply).toHaveBeenCalledWith(input, { changeMode: 'direct', dryRun: false }); + expect(adapter.apply).toHaveBeenCalledWith(input, expect.objectContaining({ changeMode: 'direct' })); }); - it('passes through tracked changeMode option', () => { - const adapter = makeAdapter(); - const input: StyleApplyInput = { target: TARGET, inline: { italic: 'off' } }; - executeStyleApply(adapter, input, { changeMode: 'tracked' }); - expect(adapter.apply).toHaveBeenCalledWith(input, { changeMode: 'tracked', dryRun: false }); - }); - - it('passes through dryRun option', () => { - const adapter = makeAdapter(); - const input: StyleApplyInput = { target: TARGET, inline: { underline: 'on' } }; - executeStyleApply(adapter, input, { dryRun: true }); - expect(adapter.apply).toHaveBeenCalledWith(input, { changeMode: 'direct', dryRun: true }); - }); - - // ------------------------------------------------------------------------- - // Happy paths — multi-mark (directive patch semantics) - // ------------------------------------------------------------------------- - - it('accepts multiple inline in one call', () => { - const adapter = makeAdapter(); - const input: StyleApplyInput = { target: TARGET, inline: { bold: 'on', italic: 'on' } }; - const result = executeStyleApply(adapter, input); - expect(result.success).toBe(true); - expect(adapter.apply).toHaveBeenCalledWith(input, expect.objectContaining({})); - }); - - it('accepts mixed on/off in one call', () => { - const adapter = makeAdapter(); - const input: StyleApplyInput = { target: TARGET, inline: { bold: 'on', italic: 'off' } }; - const result = executeStyleApply(adapter, input); - expect(result.success).toBe(true); - expect(adapter.apply).toHaveBeenCalledWith(input, expect.objectContaining({})); - }); - - it('accepts all four inline in one call', () => { + it('accepts numeric and object inline properties in one call', () => { const adapter = makeAdapter(); const input: StyleApplyInput = { target: TARGET, - inline: { bold: 'on', italic: 'off', underline: 'clear', strike: 'off' }, + inline: { + fontSize: 12, + underline: { style: 'single', color: 'FF0000' }, + }, }; const result = executeStyleApply(adapter, input); expect(result.success).toBe(true); - expect(adapter.apply).toHaveBeenCalledWith(input, expect.objectContaining({})); }); - it('accepts explicit OFF directive', () => { + it('passes through tracked and dryRun options', () => { const adapter = makeAdapter(); - const input: StyleApplyInput = { target: TARGET, inline: { bold: 'off' } }; - const result = executeStyleApply(adapter, input); - expect(result.success).toBe(true); - expect(adapter.apply).toHaveBeenCalledWith(input, expect.objectContaining({})); - }); - - it('accepts clear directive', () => { - const adapter = makeAdapter(); - const input: StyleApplyInput = { target: TARGET, inline: { bold: 'clear' } }; - const result = executeStyleApply(adapter, input); - expect(result.success).toBe(true); - expect(adapter.apply).toHaveBeenCalledWith(input, expect.objectContaining({})); + const input: StyleApplyInput = { target: TARGET, inline: { color: '00AA00' } }; + executeStyleApply(adapter, input, { changeMode: 'tracked', dryRun: true }); + expect(adapter.apply).toHaveBeenCalledWith(input, { changeMode: 'tracked', dryRun: true }); }); }); -// --------------------------------------------------------------------------- -// Shared target validation helper for value-based format operations -// --------------------------------------------------------------------------- - function targetValidationSuite( name: string, exec: (adapter: ReturnType, input: unknown, options?: unknown) => unknown, @@ -290,159 +134,134 @@ function targetValidationSuite( }); it('rejects missing target', () => { - expect(() => exec(makeAdapter(), { value: '12pt' })).toThrow('requires a target'); + expect(() => exec(makeAdapter(), { alignment: 'left' })).toThrow('requires a target'); }); it('rejects invalid target', () => { - expect(() => exec(makeAdapter(), { target: 'bad', value: '12pt' })).toThrow('text address'); + expect(() => exec(makeAdapter(), { target: 'bad', alignment: 'left' })).toThrow('text address'); }); }); } -// --------------------------------------------------------------------------- -// executeFontSize validation -// --------------------------------------------------------------------------- - -describe('executeFontSize validation', () => { - targetValidationSuite('format.fontSize', (a, i) => executeFontSize(a, i as any)); - - it('rejects missing value', () => { - expect(() => executeFontSize(makeAdapter(), { target: TARGET } as any)).toThrow('requires a value'); - }); +describe('executeAlign validation', () => { + targetValidationSuite('format.align', (a, i) => executeAlign(a, i as any)); - it('rejects empty string value', () => { - expect(() => executeFontSize(makeAdapter(), { target: TARGET, value: '' })).toThrow('empty string'); + it('rejects missing alignment', () => { + expect(() => executeAlign(makeAdapter(), { target: TARGET } as any)).toThrow('requires an alignment'); }); - it('rejects boolean value', () => { - expect(() => executeFontSize(makeAdapter(), { target: TARGET, value: true } as any)).toThrow( - 'string, number, or null', + it('rejects invalid alignment value', () => { + expect(() => executeAlign(makeAdapter(), { target: TARGET, alignment: 'middle' } as any)).toThrow( + 'left, center, right, justify', ); }); it('rejects unknown fields', () => { - expect(() => executeFontSize(makeAdapter(), { target: TARGET, value: 12, extra: 1 } as any)).toThrow('extra'); - }); - - it('accepts null value (unset)', () => { - const adapter = makeAdapter(); - executeFontSize(adapter, { target: TARGET, value: null }); - expect(adapter.fontSize).toHaveBeenCalled(); + expect(() => executeAlign(makeAdapter(), { target: TARGET, alignment: 'left', extra: 1 } as any)).toThrow('extra'); }); - it('accepts string value', () => { + it('accepts null alignment (unset)', () => { const adapter = makeAdapter(); - executeFontSize(adapter, { target: TARGET, value: '14pt' }); - expect(adapter.fontSize).toHaveBeenCalledWith({ target: TARGET, value: '14pt' }, expect.any(Object)); + executeAlign(adapter, { target: TARGET, alignment: null }); + expect(adapter.align).toHaveBeenCalled(); }); - it('accepts numeric value', () => { + it.each(['left', 'center', 'right', 'justify'] as const)('accepts alignment "%s"', (alignment) => { const adapter = makeAdapter(); - executeFontSize(adapter, { target: TARGET, value: 16 }); - expect(adapter.fontSize).toHaveBeenCalled(); + executeAlign(adapter, { target: TARGET, alignment }); + expect(adapter.align).toHaveBeenCalled(); }); }); // --------------------------------------------------------------------------- -// executeFontFamily validation +// executeInlineAlias — runtime + type contract // --------------------------------------------------------------------------- -describe('executeFontFamily validation', () => { - targetValidationSuite('format.fontFamily', (a, i) => executeFontFamily(a, i as any)); - - it('rejects missing value', () => { - expect(() => executeFontFamily(makeAdapter(), { target: TARGET } as any)).toThrow('requires a value'); - }); - - it('rejects empty string value', () => { - expect(() => executeFontFamily(makeAdapter(), { target: TARGET, value: '' })).toThrow('empty string'); - }); - - it('rejects non-string value', () => { - expect(() => executeFontFamily(makeAdapter(), { target: TARGET, value: 42 } as any)).toThrow('string or null'); - }); - - it('accepts null value (unset)', () => { +describe('executeInlineAlias', () => { + it('format.bold accepts omitted value (defaults to true)', () => { const adapter = makeAdapter(); - executeFontFamily(adapter, { target: TARGET, value: null }); - expect(adapter.fontFamily).toHaveBeenCalled(); + executeInlineAlias(adapter, 'bold', { target: TARGET }); + expect(adapter.apply).toHaveBeenCalledWith( + { target: TARGET, inline: { bold: true } }, + expect.objectContaining({ changeMode: 'direct' }), + ); }); - it('accepts valid string value', () => { + it('format.underline accepts omitted value (defaults to true)', () => { const adapter = makeAdapter(); - executeFontFamily(adapter, { target: TARGET, value: 'Arial' }); - expect(adapter.fontFamily).toHaveBeenCalled(); - }); -}); - -// --------------------------------------------------------------------------- -// executeColor validation -// --------------------------------------------------------------------------- - -describe('executeColor validation', () => { - targetValidationSuite('format.color', (a, i) => executeColor(a, i as any)); - - it('rejects missing value', () => { - expect(() => executeColor(makeAdapter(), { target: TARGET } as any)).toThrow('requires a value'); + executeInlineAlias(adapter, 'underline', { target: TARGET }); + expect(adapter.apply).toHaveBeenCalledWith( + { target: TARGET, inline: { underline: true } }, + expect.objectContaining({ changeMode: 'direct' }), + ); }); - it('rejects empty string value', () => { - expect(() => executeColor(makeAdapter(), { target: TARGET, value: '' })).toThrow('empty string'); + it('format.color requires value — throws when omitted', () => { + const adapter = makeAdapter(); + expect(() => executeInlineAlias(adapter, 'color', { target: TARGET } as any)).toThrow( + 'format.color requires a value field', + ); }); - it('rejects non-string value', () => { - expect(() => executeColor(makeAdapter(), { target: TARGET, value: 123 } as any)).toThrow('string or null'); + it('format.rFonts requires value — throws when omitted', () => { + const adapter = makeAdapter(); + expect(() => executeInlineAlias(adapter, 'rFonts', { target: TARGET } as any)).toThrow( + 'format.rFonts requires a value field', + ); }); - it('accepts null value (unset)', () => { + it('format.fontSize requires value — throws when omitted', () => { const adapter = makeAdapter(); - executeColor(adapter, { target: TARGET, value: null }); - expect(adapter.color).toHaveBeenCalled(); + expect(() => executeInlineAlias(adapter, 'fontSize', { target: TARGET } as any)).toThrow( + 'format.fontSize requires a value field', + ); }); - it('accepts hex color string', () => { + it('format.color accepts explicit value', () => { const adapter = makeAdapter(); - executeColor(adapter, { target: TARGET, value: '#ff0000' }); - expect(adapter.color).toHaveBeenCalled(); + executeInlineAlias(adapter, 'color', { target: TARGET, value: 'FF0000' }); + expect(adapter.apply).toHaveBeenCalledWith( + { target: TARGET, inline: { color: 'FF0000' } }, + expect.objectContaining({ changeMode: 'direct' }), + ); }); }); // --------------------------------------------------------------------------- -// executeAlign validation +// FormatInlineAliasInput — compile-time type shape assertions // --------------------------------------------------------------------------- -describe('executeAlign validation', () => { - targetValidationSuite('format.align', (a, i) => executeAlign(a, i as any)); - - it('rejects missing alignment', () => { - expect(() => executeAlign(makeAdapter(), { target: TARGET } as any)).toThrow('requires an alignment'); - }); - - it('rejects invalid alignment value', () => { - expect(() => executeAlign(makeAdapter(), { target: TARGET, alignment: 'middle' } as any)).toThrow( - 'left, center, right, justify', - ); - }); - - it('rejects empty string alignment', () => { - expect(() => executeAlign(makeAdapter(), { target: TARGET, alignment: '' } as any)).toThrow( - 'left, center, right, justify', - ); - }); - - it('rejects unknown fields', () => { - expect(() => executeAlign(makeAdapter(), { target: TARGET, alignment: 'left', extra: 1 } as any)).toThrow('extra'); - }); - - it('accepts null alignment (unset)', () => { - const adapter = makeAdapter(); - executeAlign(adapter, { target: TARGET, alignment: null }); - expect(adapter.align).toHaveBeenCalled(); - }); - - it.each(['left', 'center', 'right', 'justify'] as const)('accepts alignment "%s"', (alignment) => { - const adapter = makeAdapter(); - executeAlign(adapter, { target: TARGET, alignment }); - expect(adapter.align).toHaveBeenCalled(); +describe('FormatInlineAliasInput type contract', () => { + it('boolean keys allow omitted value', () => { + // These should all compile — value is optional for boolean keys. + assertType>({ target: TARGET }); + assertType>({ target: TARGET, value: true }); + assertType>({ target: TARGET }); + assertType>({ target: TARGET }); + assertType>({ target: TARGET }); + assertType>({ target: TARGET }); + }); + + it('underline allows omitted value', () => { + assertType>({ target: TARGET }); + assertType>({ target: TARGET, value: true }); + assertType>({ target: TARGET, value: { style: 'single' } }); + }); + + it('non-boolean keys require value', () => { + // color requires value + assertType>({ target: TARGET, value: 'FF0000' }); + // @ts-expect-error — value is required for color + assertType>({ target: TARGET }); + + // fontSize requires value + assertType>({ target: TARGET, value: 12 }); + // @ts-expect-error — value is required for fontSize + assertType>({ target: TARGET }); + + // rFonts requires value + assertType>({ target: TARGET, value: { ascii: 'Arial' } }); + // @ts-expect-error — value is required for rFonts + assertType>({ target: TARGET }); }); }); diff --git a/packages/document-api/src/format/format.ts b/packages/document-api/src/format/format.ts index e3116168dc..a4a37155df 100644 --- a/packages/document-api/src/format/format.ts +++ b/packages/document-api/src/format/format.ts @@ -1,8 +1,9 @@ import { normalizeMutationOptions, type MutationOptions } from '../write/write.js'; -import type { TextAddress, TextMutationReceipt, SetMarks } from '../types/index.js'; -import { MARK_KEY_SET, INLINE_DIRECTIVE_SET } from '../types/style-policy.types.js'; +import type { TextAddress, TextMutationReceipt } from '../types/index.js'; import { DocumentApiValidationError } from '../errors.js'; import { isRecord, isTextAddress, assertNoUnknownFields } from '../validation-primitives.js'; +import type { InlineRunPatch, InlineRunPatchKey } from './inline-run-patch.js'; +import { INLINE_PROPERTY_BY_KEY, validateInlineRunPatch } from './inline-run-patch.js'; // --------------------------------------------------------------------------- // Alignment enum @@ -14,73 +15,60 @@ export type Alignment = (typeof ALIGNMENTS)[number]; const ALIGNMENT_SET: ReadonlySet = new Set(ALIGNMENTS); // --------------------------------------------------------------------------- -// Input types — boolean toggle marks (existing) +// Input types // --------------------------------------------------------------------------- -/** - * Input payload for `format.bold`. - */ -export interface FormatBoldInput { - target: TextAddress; -} +/** Input payload for `format.bold`. */ +export type FormatBoldInput = FormatInlineAliasInput<'bold'>; -/** - * Input payload for `format.italic`. - */ -export interface FormatItalicInput { +/** Input payload for `format.italic`. */ +export type FormatItalicInput = FormatInlineAliasInput<'italic'>; + +/** Input payload for `format.underline`. */ +export type FormatUnderlineInput = FormatInlineAliasInput<'underline'>; + +/** Input payload for `format.strikethrough`. */ +export interface FormatStrikethroughInput { target: TextAddress; } /** - * Input payload for `format.underline`. + * Keys where `value` may be omitted — booleans (defaults to `true`) and + * `underline` (defaults to `true` for simple on/off). */ -export interface FormatUnderlineInput { - target: TextAddress; -} +type ImplicitTrueKey = + | { + [K in InlineRunPatchKey]: InlineRunPatch[K] extends boolean | null | undefined ? K : never; + }[InlineRunPatchKey] + | 'underline'; /** - * Input payload for `format.strikethrough`. + * Input payload for direct per-property aliases (`format.`). + * + * `value` is optional only for boolean-like keys (including `underline`), where + * omission defaults to `true` for ergonomic "turn on" calls. + * For all other keys the caller must supply a value. */ -export interface FormatStrikethroughInput { - target: TextAddress; -} +export type FormatInlineAliasInput = K extends ImplicitTrueKey + ? { target: TextAddress; value?: InlineRunPatch[K] } + : { target: TextAddress; value: InlineRunPatch[K] }; /** * Input payload for `format.apply`. * - * `inline` uses tri-state directive semantics: `'on'` sets, `'off'` overrides to OFF, `'clear'` removes direct formatting. + * `inline` uses explicit patch semantics: + * - omitted key: unchanged + * - concrete value: set + * - `null`: clear */ export interface StyleApplyInput { target: TextAddress; - /** Tri-state inline directive patch — at least one known key required. */ - inline: SetMarks; + inline: InlineRunPatch; } /** Options for `format.apply` — same shape as all other mutations. */ export type StyleApplyOptions = MutationOptions; -// --------------------------------------------------------------------------- -// Input types — value-based format operations (new) -// --------------------------------------------------------------------------- - -/** Input payload for `format.fontSize`. Pass `null` to unset. */ -export interface FormatFontSizeInput { - target: TextAddress; - value: string | number | null; -} - -/** Input payload for `format.fontFamily`. Pass `null` to unset. */ -export interface FormatFontFamilyInput { - target: TextAddress; - value: string | null; -} - -/** Input payload for `format.color`. Pass `null` to unset. */ -export interface FormatColorInput { - target: TextAddress; - value: string | null; -} - /** Input payload for `format.align`. Pass `null` to unset (reset to default). */ export interface FormatAlignInput { target: TextAddress; @@ -91,18 +79,9 @@ export interface FormatAlignInput { // Adapter interface // --------------------------------------------------------------------------- -/** - * Engine-specific adapter for format operations. - * - * `apply()` handles inline toggle marks via tri-state directives. - * Value-based methods handle fontSize, fontFamily, color, and paragraph alignment. - */ +/** Engine-specific adapter for format operations. */ export interface FormatAdapter { - /** Apply explicit inline-style changes using tri-state directive semantics. */ apply(input: StyleApplyInput, options?: MutationOptions): TextMutationReceipt; - fontSize(input: FormatFontSizeInput, options?: MutationOptions): TextMutationReceipt; - fontFamily(input: FormatFontFamilyInput, options?: MutationOptions): TextMutationReceipt; - color(input: FormatColorInput, options?: MutationOptions): TextMutationReceipt; align(input: FormatAlignInput, options?: MutationOptions): TextMutationReceipt; } @@ -110,19 +89,15 @@ export interface FormatAdapter { // Public API surface // --------------------------------------------------------------------------- -/** - * Public helper surface exposed on `DocumentApi.format`. - * Per-mark helpers route through `executeStyleApply` internally. - */ -export interface FormatApi { - bold(input: FormatBoldInput, options?: MutationOptions): TextMutationReceipt; - italic(input: FormatItalicInput, options?: MutationOptions): TextMutationReceipt; - underline(input: FormatUnderlineInput, options?: MutationOptions): TextMutationReceipt; +/** Direct alias methods (`format.`) that route to `format.apply`. */ +export type FormatInlineAliasApi = { + [K in InlineRunPatchKey]: (input: FormatInlineAliasInput, options?: MutationOptions) => TextMutationReceipt; +}; + +/** Public helper surface exposed on `DocumentApi.format`. */ +export interface FormatApi extends FormatInlineAliasApi { strikethrough(input: FormatStrikethroughInput, options?: MutationOptions): TextMutationReceipt; apply(input: StyleApplyInput, options?: MutationOptions): TextMutationReceipt; - fontSize(input: FormatFontSizeInput, options?: MutationOptions): TextMutationReceipt; - fontFamily(input: FormatFontFamilyInput, options?: MutationOptions): TextMutationReceipt; - color(input: FormatColorInput, options?: MutationOptions): TextMutationReceipt; align(input: FormatAlignInput, options?: MutationOptions): TextMutationReceipt; } @@ -132,18 +107,6 @@ export interface FormatApi { const STYLE_APPLY_INPUT_ALLOWED_KEYS = new Set(['target', 'inline']); -/** - * Validates a `format.apply` input and throws on violations. - * - * Validation order: - * 0. Input shape guard - * 1. Unknown field rejection - * 2. Locator validation (same rules as format operations) - * 3. `inline` presence and type - * 4. At least one known inline key - * 5. No unknown inline keys - * 6. All inline values are valid directives ('on' | 'off' | 'clear') - */ function validateStyleApplyInput(input: unknown): asserts input is StyleApplyInput { if (!isRecord(input)) { throw new DocumentApiValidationError('INVALID_INPUT', 'format.apply input must be a non-null object.'); @@ -151,70 +114,28 @@ function validateStyleApplyInput(input: unknown): asserts input is StyleApplyInp assertNoUnknownFields(input, STYLE_APPLY_INPUT_ALLOWED_KEYS, 'format.apply'); - // --- Locator validation --- - const { target, inline } = input; - - if (target === undefined) { + if (input.target === undefined) { throw new DocumentApiValidationError('INVALID_TARGET', 'format.apply requires a target.'); } - if (!isTextAddress(target)) { + if (!isTextAddress(input.target)) { throw new DocumentApiValidationError('INVALID_TARGET', 'target must be a text address object.', { field: 'target', - value: target, + value: input.target, }); } - // --- Inline-style validation --- - if (inline === undefined || inline === null) { + if (input.inline === undefined || input.inline === null) { throw new DocumentApiValidationError('INVALID_INPUT', 'format.apply requires an inline object.'); } - if (!isRecord(inline)) { - throw new DocumentApiValidationError('INVALID_INPUT', 'inline must be a non-null object.', { - field: 'inline', - value: inline, - }); - } - - const inlineKeys = Object.keys(inline); - - if (inlineKeys.length === 0) { - throw new DocumentApiValidationError('INVALID_INPUT', 'inline must include at least one known key.'); - } - - for (const key of inlineKeys) { - if (!MARK_KEY_SET.has(key)) { - throw new DocumentApiValidationError( - 'INVALID_INPUT', - `Unknown inline style key "${key}". Known keys: bold, italic, underline, strike.`, - { - field: 'inline', - key, - }, - ); - } - const value = inline[key]; - if (typeof value !== 'string' || !INLINE_DIRECTIVE_SET.has(value)) { - throw new DocumentApiValidationError( - 'INVALID_INPUT', - `inline.${key}: expected 'on'|'off'|'clear', got ${typeof value === 'string' ? `'${value}'` : typeof value} ${JSON.stringify(value)}.`, - { - field: 'inline', - key, - value, - }, - ); - } - } + validateInlineRunPatch(input.inline); } /** * Executes `format.apply` using the provided adapter. * - * Validates input (locator + inline), then delegates to the adapter's `apply()` method. - * Inline styles use tri-state directive semantics: `'on'` sets, `'off'` overrides, `'clear'` removes direct formatting. - * All inline changes within one call are applied in a single ProseMirror transaction. + * Validates the target and inline patch payload, then delegates to adapter `apply`. */ export function executeStyleApply( adapter: FormatAdapter, @@ -226,132 +147,77 @@ export function executeStyleApply( } // --------------------------------------------------------------------------- -// Shared validation: target field -// --------------------------------------------------------------------------- - -function validateTarget(input: unknown, operation: string): asserts input is { target: TextAddress } { - if (!isRecord(input)) { - throw new DocumentApiValidationError('INVALID_INPUT', `${operation} input must be a non-null object.`); - } - if (input.target === undefined) { - throw new DocumentApiValidationError('INVALID_TARGET', `${operation} requires a target.`); - } - if (!isTextAddress(input.target)) { - throw new DocumentApiValidationError('INVALID_TARGET', 'target must be a text address object.', { - field: 'target', - value: input.target, - }); - } -} - -// --------------------------------------------------------------------------- -// format.fontSize — validation and execution +// format. aliases — normalize to format.apply payloads // --------------------------------------------------------------------------- -const FONT_SIZE_ALLOWED_KEYS = new Set(['target', 'value']); +const INLINE_ALIAS_INPUT_ALLOWED_KEYS = new Set(['target', 'value']); -function validateFontSizeInput(input: unknown): asserts input is FormatFontSizeInput { - validateTarget(input, 'format.fontSize'); - assertNoUnknownFields(input as Record, FONT_SIZE_ALLOWED_KEYS, 'format.fontSize'); +function acceptsImplicitTrue(key: InlineRunPatchKey): boolean { + return INLINE_PROPERTY_BY_KEY[key].type === 'boolean' || key === 'underline'; +} - const { value } = input as Record; - if (value === undefined) { - throw new DocumentApiValidationError('INVALID_INPUT', 'format.fontSize requires a value field.'); - } - if (value !== null && typeof value !== 'string' && typeof value !== 'number') { - throw new DocumentApiValidationError('INVALID_INPUT', `format.fontSize value must be a string, number, or null.`, { - field: 'value', - value, - }); - } - if (typeof value === 'string' && value.length === 0) { - throw new DocumentApiValidationError('INVALID_INPUT', 'format.fontSize value must not be an empty string.', { - field: 'value', - }); +function normalizeInlineAliasValue( + key: K, + value: InlineRunPatch[K] | undefined, +): InlineRunPatch[K] { + if (value !== undefined) return value; + if (acceptsImplicitTrue(key)) { + return true as InlineRunPatch[K]; } + throw new DocumentApiValidationError('INVALID_INPUT', `format.${key} requires a value field.`); } -export function executeFontSize( - adapter: FormatAdapter, - input: FormatFontSizeInput, - options?: MutationOptions, -): TextMutationReceipt { - validateFontSizeInput(input); - return adapter.fontSize(input, normalizeMutationOptions(options)); +function validateInlineAliasInput( + key: K, + input: unknown, +): asserts input is FormatInlineAliasInput { + const operation = `format.${key}`; + // Preserve historical input semantics for direct aliases: + // - null / primitive input behaves like "{}" and fails with missing target. + // - unknown top-level fields are reported before target validation. + const candidate = isRecord(input) ? input : {}; + assertNoUnknownFields(candidate, INLINE_ALIAS_INPUT_ALLOWED_KEYS, operation); + validateTarget(candidate, operation); } -// --------------------------------------------------------------------------- -// format.fontFamily — validation and execution -// --------------------------------------------------------------------------- - -const FONT_FAMILY_ALLOWED_KEYS = new Set(['target', 'value']); - -function validateFontFamilyInput(input: unknown): asserts input is FormatFontFamilyInput { - validateTarget(input, 'format.fontFamily'); - assertNoUnknownFields(input as Record, FONT_FAMILY_ALLOWED_KEYS, 'format.fontFamily'); - - const { value } = input as Record; - if (value === undefined) { - throw new DocumentApiValidationError('INVALID_INPUT', 'format.fontFamily requires a value field.'); - } - if (value !== null && typeof value !== 'string') { - throw new DocumentApiValidationError('INVALID_INPUT', 'format.fontFamily value must be a string or null.', { - field: 'value', - value, - }); - } - if (typeof value === 'string' && value.length === 0) { - throw new DocumentApiValidationError('INVALID_INPUT', 'format.fontFamily value must not be an empty string.', { - field: 'value', - }); - } -} - -export function executeFontFamily( +/** + * Executes a direct alias operation (`format.`) by translating it + * into a single-key `format.apply` payload. + */ +export function executeInlineAlias( adapter: FormatAdapter, - input: FormatFontFamilyInput, + key: K, + input: FormatInlineAliasInput, options?: MutationOptions, ): TextMutationReceipt { - validateFontFamilyInput(input); - return adapter.fontFamily(input, normalizeMutationOptions(options)); + validateInlineAliasInput(key, input); + // `input.value` is typed as required or optional depending on K; at runtime + // `normalizeInlineAliasValue` handles both branches uniformly. + const value = normalizeInlineAliasValue(key, (input as { value?: InlineRunPatch[K] }).value); + const inline = { [key]: value } as InlineRunPatch; + validateInlineRunPatch(inline); + return adapter.apply({ target: input.target, inline }, normalizeMutationOptions(options)); } // --------------------------------------------------------------------------- -// format.color — validation and execution +// Shared validation: target field // --------------------------------------------------------------------------- -const COLOR_ALLOWED_KEYS = new Set(['target', 'value']); - -function validateColorInput(input: unknown): asserts input is FormatColorInput { - validateTarget(input, 'format.color'); - assertNoUnknownFields(input as Record, COLOR_ALLOWED_KEYS, 'format.color'); - - const { value } = input as Record; - if (value === undefined) { - throw new DocumentApiValidationError('INVALID_INPUT', 'format.color requires a value field.'); +function validateTarget(input: unknown, operation: string): asserts input is { target: TextAddress } { + if (!isRecord(input)) { + throw new DocumentApiValidationError('INVALID_INPUT', `${operation} input must be a non-null object.`); } - if (value !== null && typeof value !== 'string') { - throw new DocumentApiValidationError('INVALID_INPUT', 'format.color value must be a string or null.', { - field: 'value', - value, - }); + if (input.target === undefined) { + throw new DocumentApiValidationError('INVALID_TARGET', `${operation} requires a target.`); } - if (typeof value === 'string' && value.length === 0) { - throw new DocumentApiValidationError('INVALID_INPUT', 'format.color value must not be an empty string.', { - field: 'value', + if (!isTextAddress(input.target)) { + throw new DocumentApiValidationError('INVALID_TARGET', 'target must be a text address object.', { + field: 'target', + value: input.target, }); } } -export function executeColor( - adapter: FormatAdapter, - input: FormatColorInput, - options?: MutationOptions, -): TextMutationReceipt { - validateColorInput(input); - return adapter.color(input, normalizeMutationOptions(options)); -} - // --------------------------------------------------------------------------- // format.align — validation and execution // --------------------------------------------------------------------------- diff --git a/packages/document-api/src/format/inline-run-patch.ts b/packages/document-api/src/format/inline-run-patch.ts new file mode 100644 index 0000000000..c95ed64f18 --- /dev/null +++ b/packages/document-api/src/format/inline-run-patch.ts @@ -0,0 +1,710 @@ +import { DocumentApiValidationError } from '../errors.js'; +import { isRecord } from '../validation-primitives.js'; + +export type InlinePropertyStorage = 'mark' | 'runAttribute'; +export type InlinePropertyType = 'boolean' | 'string' | 'number' | 'object' | 'array'; + +export interface UnderlinePatch { + style?: string | null; + color?: string | null; + themeColor?: string | null; +} + +export interface ShadingPatch { + fill?: string | null; + color?: string | null; + val?: string | null; +} + +export interface BorderPatch { + val?: string | null; + sz?: number | null; + color?: string | null; + space?: number | null; +} + +export interface FitTextPatch { + val?: number | null; + id?: string | null; +} + +export interface LangPatch { + val?: string | null; + eastAsia?: string | null; + bidi?: string | null; +} + +export interface RFontsPatch { + ascii?: string | null; + hAnsi?: string | null; + eastAsia?: string | null; + cs?: string | null; + asciiTheme?: string | null; + hAnsiTheme?: string | null; + eastAsiaTheme?: string | null; + csTheme?: string | null; + hint?: string | null; +} + +export interface EastAsianLayoutPatch { + id?: string | null; + combine?: boolean | null; + combineBrackets?: string | null; + vert?: boolean | null; + vertCompress?: boolean | null; +} + +export interface StylisticSetPatch { + id: number; + val?: boolean; +} + +export interface InlineRunPatch { + bold?: boolean | null; + italic?: boolean | null; + strike?: boolean | null; + dstrike?: boolean | null; + smallCaps?: boolean | null; + caps?: boolean | null; + underline?: true | false | UnderlinePatch | null; + highlight?: string | null; + shading?: ShadingPatch | null; + color?: string | null; + border?: BorderPatch | null; + outline?: boolean | null; + shadow?: boolean | null; + emboss?: boolean | null; + imprint?: boolean | null; + vertAlign?: 'superscript' | 'subscript' | 'baseline' | null; + position?: number | null; + rtl?: boolean | null; + cs?: boolean | null; + bCs?: boolean | null; + iCs?: boolean | null; + vanish?: boolean | null; + webHidden?: boolean | null; + specVanish?: boolean | null; + snapToGrid?: boolean | null; + oMath?: boolean | null; + fontSize?: number | null; + fontSizeCs?: number | null; + letterSpacing?: number | null; + charScale?: number | null; + kerning?: number | null; + fitText?: FitTextPatch | null; + lang?: LangPatch | null; + rStyle?: string | null; + rFonts?: RFontsPatch | null; + eastAsianLayout?: EastAsianLayoutPatch | null; + em?: string | null; + ligatures?: string | null; + numForm?: string | null; + numSpacing?: string | null; + stylisticSets?: StylisticSetPatch[] | null; + contextualAlternates?: boolean | null; +} + +export type InlineRunPatchKey = keyof InlineRunPatch; + +interface InlinePropertyCarrierMark { + storage: 'mark'; + markName: 'bold' | 'italic' | 'underline' | 'strike' | 'highlight' | 'textStyle'; + textStyleAttr?: string; +} + +interface InlinePropertyCarrierRunAttribute { + storage: 'runAttribute'; + nodeName: 'run'; + runPropertyKey: string; +} + +export type InlinePropertyCarrier = InlinePropertyCarrierMark | InlinePropertyCarrierRunAttribute; + +export interface InlinePropertyRegistryEntry { + key: InlineRunPatchKey; + type: InlinePropertyType; + ooxmlElement: string; + storage: InlinePropertyStorage; + tracked: boolean; + carrier: InlinePropertyCarrier; + schema: Record; +} + +const schemaBooleanOrNull = (): Record => ({ + oneOf: [{ type: 'boolean' }, { type: 'null' }], +}); + +const schemaStringOrNull = (): Record => ({ + oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }], +}); + +const schemaNumberOrNull = (): Record => ({ + oneOf: [{ type: 'number' }, { type: 'null' }], +}); + +const schemaObjectOrNull = (properties: Record): Record => ({ + oneOf: [ + { + type: 'object', + properties, + additionalProperties: false, + minProperties: 1, + }, + { type: 'null' }, + ], +}); + +const UNDERLINE_OBJECT_SCHEMA: Record = { + type: 'object', + properties: { + style: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, + color: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, + themeColor: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, + }, + additionalProperties: false, + minProperties: 1, +}; + +const STYLISTIC_SET_ITEM_SCHEMA: Record = { + type: 'object', + properties: { + id: { type: 'number' }, + val: { type: 'boolean' }, + }, + required: ['id'], + additionalProperties: false, +}; + +const schemaUnderlinePatch = (): Record => ({ + oneOf: [{ type: 'boolean' }, { type: 'null' }, UNDERLINE_OBJECT_SCHEMA], +}); + +const schemaStylisticSets = (): Record => ({ + oneOf: [ + { + type: 'array', + items: STYLISTIC_SET_ITEM_SCHEMA, + minItems: 1, + }, + { type: 'null' }, + ], +}); + +function markCarrier( + markName: InlinePropertyCarrierMark['markName'], + textStyleAttr?: string, +): InlinePropertyCarrierMark { + return { storage: 'mark', markName, textStyleAttr }; +} + +function runAttributeCarrier(runPropertyKey: string): InlinePropertyCarrierRunAttribute { + return { storage: 'runAttribute', nodeName: 'run', runPropertyKey }; +} + +const markBoolean = ( + key: InlineRunPatchKey, + ooxmlElement: string, + markName: InlinePropertyCarrierMark['markName'], +): InlinePropertyRegistryEntry => ({ + key, + type: 'boolean', + ooxmlElement, + storage: 'mark', + tracked: true, + carrier: markCarrier(markName), + schema: schemaBooleanOrNull(), +}); + +const markTextStyleValue = ( + key: InlineRunPatchKey, + type: InlinePropertyType, + ooxmlElement: string, + schema: Record, +): InlinePropertyRegistryEntry => ({ + key, + type, + ooxmlElement, + storage: 'mark', + tracked: true, + carrier: markCarrier('textStyle', key), + schema, +}); + +const runAttribute = ( + key: InlineRunPatchKey, + type: InlinePropertyType, + ooxmlElement: string, + schema: Record, + runPropertyKey?: string, +): InlinePropertyRegistryEntry => ({ + key, + type, + ooxmlElement, + storage: 'runAttribute', + tracked: false, + carrier: runAttributeCarrier(runPropertyKey ?? key), + schema, +}); + +export const INLINE_PROPERTY_REGISTRY = [ + markBoolean('bold', 'w:b', 'bold'), + markBoolean('italic', 'w:i', 'italic'), + markBoolean('strike', 'w:strike', 'strike'), + { + key: 'underline', + type: 'object', + ooxmlElement: 'w:u', + storage: 'mark', + tracked: true, + carrier: markCarrier('underline'), + schema: schemaUnderlinePatch(), + }, + { + key: 'highlight', + type: 'string', + ooxmlElement: 'w:highlight', + storage: 'mark', + tracked: true, + carrier: markCarrier('highlight'), + schema: schemaStringOrNull(), + }, + markTextStyleValue('color', 'string', 'w:color', schemaStringOrNull()), + markTextStyleValue('fontSize', 'number', 'w:sz', schemaNumberOrNull()), + markTextStyleValue('letterSpacing', 'number', 'w:spacing', schemaNumberOrNull()), + markTextStyleValue('vertAlign', 'string', 'w:vertAlign', { + oneOf: [{ enum: ['superscript', 'subscript', 'baseline'] }, { type: 'null' }], + }), + markTextStyleValue('position', 'number', 'w:position', schemaNumberOrNull()), + runAttribute('dstrike', 'boolean', 'w:dstrike', schemaBooleanOrNull()), + runAttribute('smallCaps', 'boolean', 'w:smallCaps', schemaBooleanOrNull()), + runAttribute('caps', 'boolean', 'w:caps', schemaBooleanOrNull()), + runAttribute( + 'shading', + 'object', + 'w:shd', + schemaObjectOrNull({ + fill: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, + color: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, + val: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, + }), + ), + runAttribute( + 'border', + 'object', + 'w:bdr', + schemaObjectOrNull({ + val: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, + sz: { oneOf: [{ type: 'number' }, { type: 'null' }] }, + color: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, + space: { oneOf: [{ type: 'number' }, { type: 'null' }] }, + }), + 'borders', + ), + runAttribute('outline', 'boolean', 'w:outline', schemaBooleanOrNull()), + runAttribute('shadow', 'boolean', 'w:shadow', schemaBooleanOrNull()), + runAttribute('emboss', 'boolean', 'w:emboss', schemaBooleanOrNull()), + runAttribute('imprint', 'boolean', 'w:imprint', schemaBooleanOrNull()), + runAttribute('charScale', 'number', 'w:w', schemaNumberOrNull(), 'w'), + runAttribute('kerning', 'number', 'w:kern', schemaNumberOrNull(), 'kern'), + runAttribute('vanish', 'boolean', 'w:vanish', schemaBooleanOrNull()), + runAttribute('webHidden', 'boolean', 'w:webHidden', schemaBooleanOrNull()), + runAttribute('specVanish', 'boolean', 'w:specVanish', schemaBooleanOrNull()), + runAttribute('rtl', 'boolean', 'w:rtl', schemaBooleanOrNull()), + runAttribute('cs', 'boolean', 'w:cs', schemaBooleanOrNull()), + runAttribute('bCs', 'boolean', 'w:bCs', schemaBooleanOrNull(), 'boldCs'), + runAttribute('iCs', 'boolean', 'w:iCs', schemaBooleanOrNull()), + runAttribute( + 'eastAsianLayout', + 'object', + 'w:eastAsianLayout', + schemaObjectOrNull({ + id: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, + combine: { oneOf: [{ type: 'boolean' }, { type: 'null' }] }, + combineBrackets: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, + vert: { oneOf: [{ type: 'boolean' }, { type: 'null' }] }, + vertCompress: { oneOf: [{ type: 'boolean' }, { type: 'null' }] }, + }), + ), + runAttribute('em', 'string', 'w:em', schemaStringOrNull()), + runAttribute( + 'fitText', + 'object', + 'w:fitText', + schemaObjectOrNull({ + val: { oneOf: [{ type: 'number' }, { type: 'null' }] }, + id: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, + }), + ), + runAttribute('snapToGrid', 'boolean', 'w:snapToGrid', schemaBooleanOrNull()), + runAttribute( + 'lang', + 'object', + 'w:lang', + schemaObjectOrNull({ + val: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, + eastAsia: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, + bidi: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, + }), + ), + runAttribute('oMath', 'boolean', 'w:oMath', schemaBooleanOrNull()), + runAttribute('rStyle', 'string', 'w:rStyle', schemaStringOrNull(), 'styleId'), + runAttribute( + 'rFonts', + 'object', + 'w:rFonts', + schemaObjectOrNull({ + ascii: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, + hAnsi: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, + eastAsia: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, + cs: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, + asciiTheme: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, + hAnsiTheme: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, + eastAsiaTheme: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, + csTheme: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, + hint: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] }, + }), + 'fontFamily', + ), + runAttribute('fontSizeCs', 'number', 'w:szCs', schemaNumberOrNull()), + runAttribute('ligatures', 'string', 'w14:ligatures', schemaStringOrNull()), + runAttribute('numForm', 'string', 'w14:numForm', schemaStringOrNull()), + runAttribute('numSpacing', 'string', 'w14:numSpacing', schemaStringOrNull()), + runAttribute('stylisticSets', 'array', 'w14:stylisticSets', schemaStylisticSets()), + runAttribute('contextualAlternates', 'boolean', 'w14:cntxtAlts', schemaBooleanOrNull()), +] as const satisfies readonly InlinePropertyRegistryEntry[]; + +export const INLINE_PROPERTY_KEY_SET: ReadonlySet = new Set(INLINE_PROPERTY_REGISTRY.map((entry) => entry.key)); + +export const INLINE_PROPERTY_BY_KEY: Readonly> = + Object.fromEntries(INLINE_PROPERTY_REGISTRY.map((entry) => [entry.key, entry])) as Record< + InlineRunPatchKey, + InlinePropertyRegistryEntry + >; + +export const INLINE_PROPERTY_KEYS_BY_STORAGE: Readonly> = { + mark: INLINE_PROPERTY_REGISTRY.filter((entry) => entry.storage === 'mark').map((entry) => entry.key), + runAttribute: INLINE_PROPERTY_REGISTRY.filter((entry) => entry.storage === 'runAttribute').map((entry) => entry.key), +}; + +const UNDERLINE_OBJECT_ALLOWED_KEYS = new Set(['style', 'color', 'themeColor']); +const SHADING_ALLOWED_KEYS = new Set(['fill', 'color', 'val']); +const BORDER_ALLOWED_KEYS = new Set(['val', 'sz', 'color', 'space']); +const FIT_TEXT_ALLOWED_KEYS = new Set(['val', 'id']); +const LANG_ALLOWED_KEYS = new Set(['val', 'eastAsia', 'bidi']); +const RFONTS_ALLOWED_KEYS = new Set([ + 'ascii', + 'hAnsi', + 'eastAsia', + 'cs', + 'asciiTheme', + 'hAnsiTheme', + 'eastAsiaTheme', + 'csTheme', + 'hint', +]); +const EAST_ASIAN_LAYOUT_ALLOWED_KEYS = new Set(['id', 'combine', 'combineBrackets', 'vert', 'vertCompress']); +const STYLISTIC_SET_ALLOWED_KEYS = new Set(['id', 'val']); +const VERT_ALIGN_VALUES = new Set(['superscript', 'subscript', 'baseline']); + +function isObjectPatch(value: unknown): value is Record { + return isRecord(value) && !Array.isArray(value); +} + +function assertNonEmptyObject(value: Record, propertyKey: string): void { + if (Object.keys(value).length === 0) { + throw new DocumentApiValidationError('INVALID_INPUT', `inline.${propertyKey} object must not be empty.`, { + field: `inline.${propertyKey}`, + }); + } +} + +function assertAllowedObjectKeys( + objectValue: Record, + allowedKeys: ReadonlySet, + propertyKey: string, +): void { + for (const key of Object.keys(objectValue)) { + if (!allowedKeys.has(key)) { + throw new DocumentApiValidationError('INVALID_INPUT', `Unknown inline.${propertyKey} key "${key}".`, { + field: `inline.${propertyKey}.${key}`, + }); + } + } +} + +function assertStringOrNull(value: unknown, field: string): void { + if (value === null || value === undefined) return; + if (typeof value !== 'string' || value.trim().length === 0) { + throw new DocumentApiValidationError('INVALID_INPUT', `${field} must be a non-empty string or null.`, { + field, + value, + }); + } +} + +function assertBooleanOrNull(value: unknown, field: string): void { + if (value === null || value === undefined) return; + if (typeof value !== 'boolean') { + throw new DocumentApiValidationError('INVALID_INPUT', `${field} must be boolean or null.`, { + field, + value, + }); + } +} + +function assertNumberOrNull(value: unknown, field: string): void { + if (value === null || value === undefined) return; + if (typeof value !== 'number' || !Number.isFinite(value)) { + throw new DocumentApiValidationError('INVALID_INPUT', `${field} must be a finite number or null.`, { + field, + value, + }); + } +} + +function assertVertAlignOrNull(value: unknown, field: string): void { + if (value === null || value === undefined) return; + if (typeof value !== 'string' || !VERT_ALIGN_VALUES.has(value)) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${field} must be one of superscript, subscript, baseline, or null.`, + { + field, + value, + }, + ); + } +} + +function validateUnderlinePatch(value: unknown): void { + if (value === null || typeof value === 'boolean') return; + if (!isObjectPatch(value)) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + 'inline.underline must be true/false/null or an object patch.', + { + field: 'inline.underline', + value, + }, + ); + } + assertNonEmptyObject(value, 'underline'); + assertAllowedObjectKeys(value, UNDERLINE_OBJECT_ALLOWED_KEYS, 'underline'); + assertStringOrNull(value.style, 'inline.underline.style'); + assertStringOrNull(value.color, 'inline.underline.color'); + assertStringOrNull(value.themeColor, 'inline.underline.themeColor'); +} + +function validateObjectPatch( + value: unknown, + propertyKey: string, + allowedKeys: ReadonlySet, + validators: Record void>, +): void { + if (value === null) return; + if (!isObjectPatch(value)) { + throw new DocumentApiValidationError('INVALID_INPUT', `inline.${propertyKey} must be an object or null.`, { + field: `inline.${propertyKey}`, + value, + }); + } + + assertNonEmptyObject(value, propertyKey); + assertAllowedObjectKeys(value, allowedKeys, propertyKey); + + for (const key of Object.keys(value)) { + const validator = validators[key]; + if (validator) validator(value[key], `inline.${propertyKey}.${key}`); + } +} + +function validateStylisticSets(value: unknown): void { + if (value === null) return; + if (!Array.isArray(value)) { + throw new DocumentApiValidationError('INVALID_INPUT', 'inline.stylisticSets must be an array or null.', { + field: 'inline.stylisticSets', + value, + }); + } + if (value.length === 0) { + throw new DocumentApiValidationError('INVALID_INPUT', 'inline.stylisticSets array must not be empty.', { + field: 'inline.stylisticSets', + }); + } + + value.forEach((item, index) => { + if (!isObjectPatch(item)) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `inline.stylisticSets[${index}] must be an object with id/val fields.`, + { + field: `inline.stylisticSets[${index}]`, + value: item, + }, + ); + } + assertAllowedObjectKeys(item, STYLISTIC_SET_ALLOWED_KEYS, `stylisticSets[${index}]`); + if (typeof item.id !== 'number' || !Number.isFinite(item.id)) { + throw new DocumentApiValidationError('INVALID_INPUT', `inline.stylisticSets[${index}].id must be a number.`, { + field: `inline.stylisticSets[${index}].id`, + value: item.id, + }); + } + if (item.val !== undefined && typeof item.val !== 'boolean') { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `inline.stylisticSets[${index}].val must be boolean when provided.`, + { + field: `inline.stylisticSets[${index}].val`, + value: item.val, + }, + ); + } + }); +} + +function validateInlineProperty(key: string, value: unknown): void { + switch (key as InlineRunPatchKey) { + case 'bold': + case 'italic': + case 'strike': + case 'dstrike': + case 'smallCaps': + case 'caps': + case 'outline': + case 'shadow': + case 'emboss': + case 'imprint': + case 'rtl': + case 'cs': + case 'bCs': + case 'iCs': + case 'vanish': + case 'webHidden': + case 'specVanish': + case 'snapToGrid': + case 'oMath': + case 'contextualAlternates': + assertBooleanOrNull(value, `inline.${key}`); + return; + case 'underline': + validateUnderlinePatch(value); + return; + case 'highlight': + case 'color': + case 'rStyle': + case 'em': + case 'ligatures': + case 'numForm': + case 'numSpacing': + assertStringOrNull(value, `inline.${key}`); + return; + case 'fontSize': + case 'fontSizeCs': + case 'letterSpacing': + case 'charScale': + case 'kerning': + case 'position': + assertNumberOrNull(value, `inline.${key}`); + return; + case 'vertAlign': + assertVertAlignOrNull(value, 'inline.vertAlign'); + return; + case 'shading': + validateObjectPatch(value, 'shading', SHADING_ALLOWED_KEYS, { + fill: assertStringOrNull, + color: assertStringOrNull, + val: assertStringOrNull, + }); + return; + case 'border': + validateObjectPatch(value, 'border', BORDER_ALLOWED_KEYS, { + val: assertStringOrNull, + color: assertStringOrNull, + sz: assertNumberOrNull, + space: assertNumberOrNull, + }); + return; + case 'fitText': + validateObjectPatch(value, 'fitText', FIT_TEXT_ALLOWED_KEYS, { + val: assertNumberOrNull, + id: assertStringOrNull, + }); + return; + case 'lang': + validateObjectPatch(value, 'lang', LANG_ALLOWED_KEYS, { + val: assertStringOrNull, + eastAsia: assertStringOrNull, + bidi: assertStringOrNull, + }); + return; + case 'rFonts': + validateObjectPatch(value, 'rFonts', RFONTS_ALLOWED_KEYS, { + ascii: assertStringOrNull, + hAnsi: assertStringOrNull, + eastAsia: assertStringOrNull, + cs: assertStringOrNull, + asciiTheme: assertStringOrNull, + hAnsiTheme: assertStringOrNull, + eastAsiaTheme: assertStringOrNull, + csTheme: assertStringOrNull, + hint: assertStringOrNull, + }); + return; + case 'eastAsianLayout': + validateObjectPatch(value, 'eastAsianLayout', EAST_ASIAN_LAYOUT_ALLOWED_KEYS, { + id: assertStringOrNull, + combine: assertBooleanOrNull, + combineBrackets: assertStringOrNull, + vert: assertBooleanOrNull, + vertCompress: assertBooleanOrNull, + }); + return; + case 'stylisticSets': + validateStylisticSets(value); + return; + default: + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `Unknown inline style key "${key}". Known keys are defined by INLINE_PROPERTY_REGISTRY.`, + { + field: 'inline', + key, + }, + ); + } +} + +export function validateInlineRunPatch(patch: unknown): asserts patch is InlineRunPatch { + if (!isObjectPatch(patch)) { + throw new DocumentApiValidationError('INVALID_INPUT', 'inline must be a non-null object.', { + field: 'inline', + value: patch, + }); + } + + const keys = Object.keys(patch); + if (keys.length === 0) { + throw new DocumentApiValidationError('INVALID_INPUT', 'inline must include at least one known key.'); + } + + for (const key of keys) { + if (!INLINE_PROPERTY_KEY_SET.has(key)) { + throw new DocumentApiValidationError('INVALID_INPUT', `Unknown inline style key "${key}".`, { + field: 'inline', + key, + }); + } + validateInlineProperty(key, patch[key]); + } +} + +export function buildInlineRunPatchSchema(): Record { + const properties = Object.fromEntries(INLINE_PROPERTY_REGISTRY.map((entry) => [entry.key, entry.schema])); + return { + type: 'object', + properties, + additionalProperties: false, + minProperties: 1, + }; +} diff --git a/packages/document-api/src/index.test.ts b/packages/document-api/src/index.test.ts index a32199e800..31007bdb98 100644 --- a/packages/document-api/src/index.test.ts +++ b/packages/document-api/src/index.test.ts @@ -120,9 +120,6 @@ function makeFormatReceipt() { function makeFormatAdapter(): FormatAdapter { return { apply: vi.fn(() => makeFormatReceipt()), - fontSize: vi.fn(() => makeFormatReceipt()), - fontFamily: vi.fn(() => makeFormatReceipt()), - color: vi.fn(() => makeFormatReceipt()), align: vi.fn(() => makeFormatReceipt()), }; } @@ -273,7 +270,7 @@ function makeCapabilitiesAdapter(overrides?: Partial): lists: { enabled: false }, dryRun: { enabled: false }, }, - format: { properties: {} }, + format: { supportedInlineProperties: {} as DocumentApiCapabilities['format']['supportedInlineProperties'] }, operations: {} as DocumentApiCapabilities['operations'], planEngine: { supportedStepOps: [], @@ -584,7 +581,7 @@ describe('createDocumentApi', () => { const target = { kind: 'text', blockId: 'p1', range: { start: 0, end: 2 } } as const; api.format.bold({ target }, { changeMode: 'tracked' }); expect(formatAdpt.apply).toHaveBeenCalledWith( - { target, inline: { bold: 'on' } }, + { target, inline: { bold: true } }, { changeMode: 'tracked', dryRun: false }, ); }); @@ -607,7 +604,7 @@ describe('createDocumentApi', () => { const target = { kind: 'text', blockId: 'p1', range: { start: 0, end: 2 } } as const; api.format.italic({ target }, { changeMode: 'direct' }); expect(formatAdpt.apply).toHaveBeenCalledWith( - { target, inline: { italic: 'on' } }, + { target, inline: { italic: true } }, { changeMode: 'direct', dryRun: false }, ); }); @@ -630,7 +627,7 @@ describe('createDocumentApi', () => { const target = { kind: 'text', blockId: 'p1', range: { start: 0, end: 2 } } as const; api.format.underline({ target }, { changeMode: 'direct' }); expect(formatAdpt.apply).toHaveBeenCalledWith( - { target, inline: { underline: 'on' } }, + { target, inline: { underline: true } }, { changeMode: 'direct', dryRun: false }, ); }); @@ -653,80 +650,11 @@ describe('createDocumentApi', () => { const target = { kind: 'text', blockId: 'p1', range: { start: 0, end: 2 } } as const; api.format.strikethrough({ target }, { changeMode: 'tracked' }); expect(formatAdpt.apply).toHaveBeenCalledWith( - { target, inline: { strike: 'on' } }, + { target, inline: { strike: true } }, { changeMode: 'tracked', dryRun: false }, ); }); - it('delegates format.fontSize to adapter.fontSize', () => { - const formatAdpt = makeFormatAdapter(); - const api = createDocumentApi({ - find: makeFindAdapter(QUERY_RESULT), - getNode: makeGetNodeAdapter(PARAGRAPH_INFO), - getText: makeGetTextAdapter(), - info: makeInfoAdapter(), - comments: makeCommentsAdapter(), - write: makeWriteAdapter(), - format: formatAdpt, - trackChanges: makeTrackChangesAdapter(), - create: makeCreateAdapter(), - lists: makeListsAdapter(), - }); - - const target = { kind: 'text', blockId: 'p1', range: { start: 0, end: 2 } } as const; - api.format.fontSize({ target, value: '14pt' }); - expect(formatAdpt.fontSize).toHaveBeenCalledWith( - { target, value: '14pt' }, - { changeMode: 'direct', dryRun: false }, - ); - }); - - it('delegates format.fontFamily to adapter.fontFamily', () => { - const formatAdpt = makeFormatAdapter(); - const api = createDocumentApi({ - find: makeFindAdapter(QUERY_RESULT), - getNode: makeGetNodeAdapter(PARAGRAPH_INFO), - getText: makeGetTextAdapter(), - info: makeInfoAdapter(), - comments: makeCommentsAdapter(), - write: makeWriteAdapter(), - format: formatAdpt, - trackChanges: makeTrackChangesAdapter(), - create: makeCreateAdapter(), - lists: makeListsAdapter(), - }); - - const target = { kind: 'text', blockId: 'p1', range: { start: 0, end: 2 } } as const; - api.format.fontFamily({ target, value: 'Arial' }); - expect(formatAdpt.fontFamily).toHaveBeenCalledWith( - { target, value: 'Arial' }, - { changeMode: 'direct', dryRun: false }, - ); - }); - - it('delegates format.color to adapter.color', () => { - const formatAdpt = makeFormatAdapter(); - const api = createDocumentApi({ - find: makeFindAdapter(QUERY_RESULT), - getNode: makeGetNodeAdapter(PARAGRAPH_INFO), - getText: makeGetTextAdapter(), - info: makeInfoAdapter(), - comments: makeCommentsAdapter(), - write: makeWriteAdapter(), - format: formatAdpt, - trackChanges: makeTrackChangesAdapter(), - create: makeCreateAdapter(), - lists: makeListsAdapter(), - }); - - const target = { kind: 'text', blockId: 'p1', range: { start: 0, end: 2 } } as const; - api.format.color({ target, value: '#ff0000' }); - expect(formatAdpt.color).toHaveBeenCalledWith( - { target, value: '#ff0000' }, - { changeMode: 'direct', dryRun: false }, - ); - }); - it('delegates format.align to adapter.align', () => { const formatAdpt = makeFormatAdapter(); const api = createDocumentApi({ @@ -1664,7 +1592,7 @@ describe('createDocumentApi', () => { const target = { kind: 'text', blockId: 'p1', range: { start: 0, end: 2 } } as const; api.format.bold({ target }); expect(formatAdpt.apply).toHaveBeenCalledWith( - { target, inline: { bold: 'on' } }, + { target, inline: { bold: true } }, { changeMode: 'direct', dryRun: false }, ); }); diff --git a/packages/document-api/src/index.ts b/packages/document-api/src/index.ts index d44c57bcb5..273627888a 100644 --- a/packages/document-api/src/index.ts +++ b/packages/document-api/src/index.ts @@ -48,17 +48,14 @@ import { executeFind, type FindAdapter, type FindOptions } from './find/find.js' import type { FormatAdapter, FormatApi, - FormatBoldInput, - FormatItalicInput, - FormatUnderlineInput, + FormatInlineAliasApi, + FormatInlineAliasInput, FormatStrikethroughInput, StyleApplyInput, - FormatFontSizeInput, - FormatFontFamilyInput, - FormatColorInput, FormatAlignInput, } from './format/format.js'; -import { executeStyleApply, executeFontSize, executeFontFamily, executeColor, executeAlign } from './format/format.js'; +import { executeStyleApply, executeInlineAlias, executeAlign } from './format/format.js'; +import { INLINE_PROPERTY_REGISTRY, type InlineRunPatchKey } from './format/inline-run-patch.js'; import type { StylesAdapter, StylesApi, @@ -232,18 +229,41 @@ export type { InfoAdapter, InfoInput } from './info/info.js'; export type { WriteAdapter, WriteRequest } from './write/write.js'; export type { FormatAdapter, + FormatInlineAliasApi, + FormatInlineAliasInput, FormatBoldInput, FormatItalicInput, FormatUnderlineInput, FormatStrikethroughInput, StyleApplyInput, StyleApplyOptions, - FormatFontSizeInput, - FormatFontFamilyInput, - FormatColorInput, FormatAlignInput, } from './format/format.js'; export { ALIGNMENTS, type Alignment } from './format/format.js'; +export type { + InlineRunPatch, + InlineRunPatchKey, + InlinePropertyStorage, + InlinePropertyType, + InlinePropertyCarrier, + InlinePropertyRegistryEntry, + UnderlinePatch, + ShadingPatch, + BorderPatch, + FitTextPatch, + LangPatch, + RFontsPatch, + EastAsianLayoutPatch, + StylisticSetPatch, +} from './format/inline-run-patch.js'; +export { + INLINE_PROPERTY_REGISTRY, + INLINE_PROPERTY_KEY_SET, + INLINE_PROPERTY_BY_KEY, + INLINE_PROPERTY_KEYS_BY_STORAGE, + validateInlineRunPatch, + buildInlineRunPatchSchema, +} from './format/inline-run-patch.js'; export { PROPERTY_REGISTRY } from './styles/styles.js'; export type { PropertyDefinition, @@ -591,9 +611,21 @@ export interface DocumentApiAdapters { * } * ``` */ +function buildFormatInlineAliasApi(adapter: FormatAdapter): FormatInlineAliasApi { + return Object.fromEntries( + INLINE_PROPERTY_REGISTRY.map((entry) => { + const key = entry.key as InlineRunPatchKey; + const handler = (input: FormatInlineAliasInput, options?: MutationOptions) => + executeInlineAlias(adapter, key, input, options); + return [key, handler]; + }), + ) as FormatInlineAliasApi; +} + export function createDocumentApi(adapters: DocumentApiAdapters): DocumentApi { const capFn = () => executeCapabilities(adapters.capabilities); const capabilities: CapabilitiesApi = Object.assign(capFn, { get: capFn }); + const inlineAliasApi = buildFormatInlineAliasApi(adapters.format); const api: DocumentApi = { find(selectorOrQuery: Selector | Query, options?: FindOptions): FindOutput { @@ -638,30 +670,13 @@ export function createDocumentApi(adapters: DocumentApiAdapters): DocumentApi { return executeDelete(adapters.write, input, options); }, format: { - bold(input: FormatBoldInput, options?: MutationOptions): TextMutationReceipt { - return executeStyleApply(adapters.format, { ...input, inline: { bold: 'on' } }, options); - }, - italic(input: FormatItalicInput, options?: MutationOptions): TextMutationReceipt { - return executeStyleApply(adapters.format, { ...input, inline: { italic: 'on' } }, options); - }, - underline(input: FormatUnderlineInput, options?: MutationOptions): TextMutationReceipt { - return executeStyleApply(adapters.format, { ...input, inline: { underline: 'on' } }, options); - }, + ...inlineAliasApi, strikethrough(input: FormatStrikethroughInput, options?: MutationOptions): TextMutationReceipt { - return executeStyleApply(adapters.format, { ...input, inline: { strike: 'on' } }, options); + return executeInlineAlias(adapters.format, 'strike', { ...input, value: true }, options); }, apply(input: StyleApplyInput, options?: MutationOptions): TextMutationReceipt { return executeStyleApply(adapters.format, input, options); }, - fontSize(input: FormatFontSizeInput, options?: MutationOptions): TextMutationReceipt { - return executeFontSize(adapters.format, input, options); - }, - fontFamily(input: FormatFontFamilyInput, options?: MutationOptions): TextMutationReceipt { - return executeFontFamily(adapters.format, input, options); - }, - color(input: FormatColorInput, options?: MutationOptions): TextMutationReceipt { - return executeColor(adapters.format, input, options); - }, align(input: FormatAlignInput, options?: MutationOptions): TextMutationReceipt { return executeAlign(adapters.format, input, options); }, diff --git a/packages/document-api/src/invoke/invoke.test.ts b/packages/document-api/src/invoke/invoke.test.ts index a269d35627..1493955074 100644 --- a/packages/document-api/src/invoke/invoke.test.ts +++ b/packages/document-api/src/invoke/invoke.test.ts @@ -38,7 +38,7 @@ function makeAdapters() { lists: { enabled: false }, dryRun: { enabled: false }, }, - format: { properties: {} }, + format: { supportedInlineProperties: {} as DocumentApiCapabilities['format']['supportedInlineProperties'] }, operations: {} as DocumentApiCapabilities['operations'], planEngine: { supportedStepOps: [], @@ -94,9 +94,6 @@ function makeAdapters() { }); const formatAdapter: FormatAdapter = { apply: vi.fn(formatReceipt), - fontSize: vi.fn(formatReceipt), - fontFamily: vi.fn(formatReceipt), - color: vi.fn(formatReceipt), align: vi.fn(formatReceipt), }; const stylesAdapter: StylesAdapter = { @@ -304,40 +301,13 @@ describe('invoke', () => { const api = createDocumentApi(adapters); const input = { target: { kind: 'text' as const, blockId: 'p1', range: { start: 0, end: 2 } }, - inline: { bold: 'on' }, + inline: { bold: true }, }; const direct = api.format.apply(input); const invoked = api.invoke({ operationId: 'format.apply', input }); expect(invoked).toEqual(direct); }); - it('format.fontSize: 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 } }, value: '14pt' }; - const direct = api.format.fontSize(input); - const invoked = api.invoke({ operationId: 'format.fontSize', input }); - expect(invoked).toEqual(direct); - }); - - 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 } }, value: 'Arial' }; - const direct = api.format.fontFamily(input); - const invoked = api.invoke({ operationId: 'format.fontFamily', input }); - expect(invoked).toEqual(direct); - }); - - it('format.color: 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 } }, value: '#ff0000' }; - const direct = api.format.color(input); - const invoked = api.invoke({ operationId: 'format.color', input }); - expect(invoked).toEqual(direct); - }); - it('format.align: invoke returns same result as direct call', () => { const { adapters } = makeAdapters(); const api = createDocumentApi(adapters); diff --git a/packages/document-api/src/invoke/invoke.ts b/packages/document-api/src/invoke/invoke.ts index 1e4997305a..fca3666a26 100644 --- a/packages/document-api/src/invoke/invoke.ts +++ b/packages/document-api/src/invoke/invoke.ts @@ -8,6 +8,7 @@ import type { OperationId } from '../contract/types.js'; import type { OperationRegistry } from '../contract/operation-registry.js'; import type { DocumentApi } from '../index.js'; +import { INLINE_PROPERTY_REGISTRY } from '../format/inline-run-patch.js'; // --------------------------------------------------------------------------- // TypedDispatchTable — compile-time contract between registry and dispatch @@ -21,6 +22,29 @@ export type TypedDispatchTable = { [K in OperationId]: TypedDispatchHandler; }; +type FormatInlineAliasOperationId = `format.${(typeof INLINE_PROPERTY_REGISTRY)[number]['key']}`; + +function buildFormatInlineAliasDispatch(api: DocumentApi): Pick { + return Object.fromEntries( + INLINE_PROPERTY_REGISTRY.map((entry) => { + const operationId = `format.${entry.key}` as FormatInlineAliasOperationId; + return [ + operationId, + ( + input: OperationRegistry[typeof operationId]['input'], + options?: OperationRegistry[typeof operationId]['options'], + ) => + ( + api.format[entry.key] as ( + input: OperationRegistry[typeof operationId]['input'], + options?: OperationRegistry[typeof operationId]['options'], + ) => OperationRegistry[typeof operationId]['output'] + )(input, options), + ]; + }), + ) as Pick; +} + /** * Builds a dispatch table that maps every OperationId to the corresponding * direct method call on the given DocumentApi instance. @@ -30,6 +54,8 @@ export type TypedDispatchTable = { * time that each handler conforms to the {@link OperationRegistry} contract. */ export function buildDispatchTable(api: DocumentApi): TypedDispatchTable { + const formatInlineAliasDispatch = buildFormatInlineAliasDispatch(api); + return { // --- Singleton reads --- find: (input, options) => @@ -49,9 +75,7 @@ export function buildDispatchTable(api: DocumentApi): TypedDispatchTable { // --- format.* --- 'format.apply': (input, options) => api.format.apply(input, options), - 'format.fontSize': (input, options) => api.format.fontSize(input, options), - 'format.fontFamily': (input, options) => api.format.fontFamily(input, options), - 'format.color': (input, options) => api.format.color(input, options), + ...formatInlineAliasDispatch, 'format.align': (input, options) => api.format.align(input, options), // --- styles.* --- diff --git a/packages/document-api/src/overview-examples.test.ts b/packages/document-api/src/overview-examples.test.ts index fa073ebb3a..cdf6a80518 100644 --- a/packages/document-api/src/overview-examples.test.ts +++ b/packages/document-api/src/overview-examples.test.ts @@ -78,9 +78,6 @@ function makeWriteAdapter() { function makeFormatAdapter() { return { apply: vi.fn(() => makeTextMutationReceipt()), - fontSize: vi.fn(() => makeTextMutationReceipt()), - fontFamily: vi.fn(() => makeTextMutationReceipt()), - color: vi.fn(() => makeTextMutationReceipt()), align: vi.fn(() => makeTextMutationReceipt()), }; } @@ -209,14 +206,7 @@ function makeCapabilitiesAdapter(): { get: ReturnType } { lists: { enabled: true }, dryRun: { enabled: true }, }, - format: { - properties: { - bold: { kind: 'toggle', directives: ['on', 'off', 'clear'] }, - italic: { kind: 'toggle', directives: ['on', 'off', 'clear'] }, - underline: { kind: 'toggle', directives: ['on', 'off', 'clear'] }, - strike: { kind: 'toggle', directives: ['on', 'off', 'clear'] }, - }, - }, + format: { supportedInlineProperties: {} as DocumentApiCapabilities['format']['supportedInlineProperties'] }, operations: Object.fromEntries( [ 'find', @@ -307,8 +297,10 @@ function makeApi() { range: { start: 0, end: 3 }, text: 'foo', styles: { - direct: { bold: 'clear', italic: 'clear', underline: 'clear', strike: 'clear' }, - effective: { bold: false, italic: false, underline: false, strike: false }, + bold: false, + italic: false, + underline: false, + strike: false, }, ref: 'ref:run-1', }, @@ -405,7 +397,7 @@ describe('overview.mdx examples', () => { id: 'style-terms', op: 'format.apply', where: { by: 'ref' as const, ref }, - args: { inline: { bold: 'on' } }, + args: { inline: { bold: true } }, }, ], }; @@ -461,7 +453,7 @@ describe('overview.mdx examples', () => { const target = { kind: 'text', blockId: 'p1', range: { start: 0, end: 3 } }; if (caps.operations['format.apply'].available) { - doc.format.apply({ target, inline: { bold: 'on' } }); + doc.format.apply({ target, inline: { bold: true } }); } if (caps.global.trackChanges.enabled) { @@ -576,7 +568,7 @@ describe('src/README.md workflow examples', () => { const caps = doc.capabilities(); if (caps.operations['format.apply'].available) { - doc.format.apply({ target, inline: { bold: 'on' } }); + doc.format.apply({ target, inline: { bold: true } }); } if (caps.global.trackChanges.enabled) { doc.insert({ value: 'tracked' }, { changeMode: 'tracked' }); diff --git a/packages/document-api/src/types/mutation-plan.types.ts b/packages/document-api/src/types/mutation-plan.types.ts index bb9fad2dc9..ad06e6a3a8 100644 --- a/packages/document-api/src/types/mutation-plan.types.ts +++ b/packages/document-api/src/types/mutation-plan.types.ts @@ -8,7 +8,8 @@ import type { NodeAddress } from './base.js'; import type { TextAddress, TrackedChangeAddress } from './address.js'; import type { TextSelector, NodeSelector } from './query.js'; -import type { InsertStylePolicy, StylePolicy, SetMarks } from './style-policy.types.js'; +import type { InsertStylePolicy, StylePolicy } from './style-policy.types.js'; +import type { InlineRunPatch } from '../format/inline-run-patch.js'; // --------------------------------------------------------------------------- // Universal targeting model @@ -111,7 +112,7 @@ export type StyleApplyStep = { op: 'format.apply'; where: StepWhere; args: { - inline: SetMarks; + inline: InlineRunPatch; }; }; diff --git a/packages/super-editor/src/core/super-converter/styles.js b/packages/super-editor/src/core/super-converter/styles.js index 9a44746044..424490b26e 100644 --- a/packages/super-editor/src/core/super-converter/styles.js +++ b/packages/super-editor/src/core/super-converter/styles.js @@ -75,18 +75,24 @@ export function encodeMarksFromRPr(runProperties, docx) { break; case 'underline': let underlineType = value['w:val']; - if (!underlineType) { - break; - } let underlineColor = value['w:color']; if (underlineColor && underlineColor.toLowerCase() !== 'auto' && !underlineColor.startsWith('#')) { underlineColor = `#${underlineColor}`; } + const underlineThemeColor = value['w:themeColor']; + const underlineThemeTint = value['w:themeTint']; + const underlineThemeShade = value['w:themeShade']; + if (!underlineType && !underlineColor && !underlineThemeColor && !underlineThemeTint && !underlineThemeShade) { + break; + } marks.push({ type: key, attrs: { underlineType, underlineColor, + underlineThemeColor, + underlineThemeTint, + underlineThemeShade, }, }); break; @@ -522,7 +528,8 @@ export function decodeRPrFromMarks(marks) { runProperties[type] = mark.attrs.value !== '0' && mark.attrs.value !== false; break; case 'underline': { - const { underlineType, underlineColor } = mark.attrs; + const { underlineType, underlineColor, underlineThemeColor, underlineThemeTint, underlineThemeShade } = + mark.attrs; const underlineAttrs = {}; if (underlineType) { underlineAttrs['w:val'] = underlineType; @@ -530,6 +537,15 @@ export function decodeRPrFromMarks(marks) { if (underlineColor) { underlineAttrs['w:color'] = underlineColor.replace('#', ''); } + if (underlineThemeColor) { + underlineAttrs['w:themeColor'] = underlineThemeColor; + } + if (underlineThemeTint) { + underlineAttrs['w:themeTint'] = underlineThemeTint; + } + if (underlineThemeShade) { + underlineAttrs['w:themeShade'] = underlineThemeShade; + } if (Object.keys(underlineAttrs).length > 0) { runProperties.underline = underlineAttrs; } diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/rpr/rpr-translator.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/rpr/rpr-translator.js index 738ff34642..2d2e32f701 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/w/rpr/rpr-translator.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/rpr/rpr-translator.js @@ -39,6 +39,11 @@ import { translator as rtlTranslator } from '../rtl/rtl-translator.js'; import { translator as csTranslator } from '../cs/cs-translator.js'; import { translator as iCsTranslator } from '../iCs/iCs-translator.js'; import { translator as webHiddenTranslator } from '../webHidden/webHidden-translator.js'; +import { translator as ligaturesTranslator } from '../w14-ligatures/ligatures-translator.js'; +import { translator as numFormTranslator } from '../w14-numForm/numForm-translator.js'; +import { translator as numSpacingTranslator } from '../w14-numSpacing/numSpacing-translator.js'; +import { translator as stylisticSetsTranslator } from '../w14-stylisticSets/stylisticSets-translator.js'; +import { translator as cntxtAltsTranslator } from '../w14-cntxtAlts/cntxtAlts-translator.js'; // Property translators for w:rPr child elements // Each translator handles a specific property of the run properties @@ -81,6 +86,11 @@ export const propertyTranslators = [ underlineTranslator, vanishTranslator, vertAlignTranslator, + ligaturesTranslator, + numFormTranslator, + numSpacingTranslator, + stylisticSetsTranslator, + cntxtAltsTranslator, webHiddenTranslator, wTranslator, ]; diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/rpr/rpr-translator.test.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/rpr/rpr-translator.test.js index a1504a1ba8..7de6455f37 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/w/rpr/rpr-translator.test.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/rpr/rpr-translator.test.js @@ -17,6 +17,8 @@ describe('w:rPr translator (attribute aggregator)', () => { { name: 'w:color', attributes: { 'w:val': 'FF0000' } }, { name: 'w:lang', attributes: { 'w:val': 'en-US' } }, { name: 'w:shd', attributes: { 'w:fill': 'CCCCCC', 'w:val': 'clear' } }, + { name: 'w14:ligatures', attributes: { 'w14:val': 'standardContextual' } }, + { name: 'w14:cntxtAlts', attributes: { 'w14:val': '0' } }, ]); const result = translator.encode(params); @@ -33,6 +35,8 @@ describe('w:rPr translator (attribute aggregator)', () => { fill: 'CCCCCC', val: 'clear', }, + ligatures: 'standardContextual', + contextualAlternates: false, }); }); diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-cntxtAlts/cntxtAlts-translator.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-cntxtAlts/cntxtAlts-translator.js new file mode 100644 index 0000000000..7ebc5265a0 --- /dev/null +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-cntxtAlts/cntxtAlts-translator.js @@ -0,0 +1,27 @@ +import { NodeTranslator } from '@translator'; +import { parseBoolean } from '../../utils.js'; + +const XML_NAME = 'w14:cntxtAlts'; +const SD_KEY = 'contextualAlternates'; + +function readVal(node) { + return node?.attributes?.['w14:val'] ?? node?.attributes?.['w:val']; +} + +export const translator = NodeTranslator.from({ + xmlName: XML_NAME, + sdNodeOrKeyName: SD_KEY, + encode: ({ nodes }) => { + const rawValue = readVal(nodes?.[0]); + if (rawValue == null) return true; + return parseBoolean(rawValue); + }, + decode: ({ node }) => { + const value = node?.attrs?.[SD_KEY]; + if (value == null) return undefined; + if (value === true) return { attributes: {} }; + return { + attributes: { 'w14:val': '0' }, + }; + }, +}); diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-cntxtAlts/cntxtAlts-translator.test.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-cntxtAlts/cntxtAlts-translator.test.js new file mode 100644 index 0000000000..fb3e2123c6 --- /dev/null +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-cntxtAlts/cntxtAlts-translator.test.js @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest'; +import { NodeTranslator } from '../../../node-translator/node-translator.js'; +import { translator } from './cntxtAlts-translator.js'; + +describe('w14:cntxtAlts translator', () => { + it('builds a NodeTranslator instance', () => { + expect(translator).toBeInstanceOf(NodeTranslator); + expect(translator.xmlName).toBe('w14:cntxtAlts'); + expect(translator.sdNodeOrKeyName).toBe('contextualAlternates'); + }); + + it('encodes true when w14:val is omitted', () => { + const encoded = translator.encode({ + nodes: [{ name: 'w14:cntxtAlts', attributes: {} }], + }); + expect(encoded).toBe(true); + }); + + it('encodes false when w14:val is 0', () => { + const encoded = translator.encode({ + nodes: [{ name: 'w14:cntxtAlts', attributes: { 'w14:val': '0' } }], + }); + expect(encoded).toBe(false); + }); + + it('decodes false to w14:val=0', () => { + const decoded = translator.decode({ + node: { attrs: { contextualAlternates: false } }, + }); + expect(decoded).toEqual({ + attributes: { 'w14:val': '0' }, + }); + }); +}); diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-cntxtAlts/index.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-cntxtAlts/index.js new file mode 100644 index 0000000000..4ac829ec89 --- /dev/null +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-cntxtAlts/index.js @@ -0,0 +1 @@ +export * from './cntxtAlts-translator.js'; diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-ligatures/index.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-ligatures/index.js new file mode 100644 index 0000000000..9dfb484462 --- /dev/null +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-ligatures/index.js @@ -0,0 +1 @@ +export * from './ligatures-translator.js'; diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-ligatures/ligatures-translator.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-ligatures/ligatures-translator.js new file mode 100644 index 0000000000..17c2c2fb4c --- /dev/null +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-ligatures/ligatures-translator.js @@ -0,0 +1,23 @@ +import { NodeTranslator } from '@translator'; + +const XML_NAME = 'w14:ligatures'; +const SD_KEY = 'ligatures'; + +function readVal(node) { + return node?.attributes?.['w14:val'] ?? node?.attributes?.['w:val']; +} + +export const translator = NodeTranslator.from({ + xmlName: XML_NAME, + sdNodeOrKeyName: SD_KEY, + encode: ({ nodes }) => { + return readVal(nodes?.[0]) ?? undefined; + }, + decode: ({ node }) => { + const value = node?.attrs?.[SD_KEY]; + if (value == null) return undefined; + return { + attributes: { 'w14:val': String(value) }, + }; + }, +}); diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-ligatures/ligatures-translator.test.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-ligatures/ligatures-translator.test.js new file mode 100644 index 0000000000..386d276eb1 --- /dev/null +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-ligatures/ligatures-translator.test.js @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest'; +import { NodeTranslator } from '../../../node-translator/node-translator.js'; +import { translator } from './ligatures-translator.js'; + +describe('w14:ligatures translator', () => { + it('builds a NodeTranslator instance', () => { + expect(translator).toBeInstanceOf(NodeTranslator); + expect(translator.xmlName).toBe('w14:ligatures'); + expect(translator.sdNodeOrKeyName).toBe('ligatures'); + }); + + it('encodes ligatures from w14:val', () => { + const encoded = translator.encode({ + nodes: [{ name: 'w14:ligatures', attributes: { 'w14:val': 'standardContextual' } }], + }); + expect(encoded).toBe('standardContextual'); + }); + + it('decodes ligatures to w14:val', () => { + const decoded = translator.decode({ + node: { attrs: { ligatures: 'historical' } }, + }); + expect(decoded).toEqual({ + attributes: { 'w14:val': 'historical' }, + }); + }); +}); diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-numForm/index.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-numForm/index.js new file mode 100644 index 0000000000..9d44207bb4 --- /dev/null +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-numForm/index.js @@ -0,0 +1 @@ +export * from './numForm-translator.js'; diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-numForm/numForm-translator.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-numForm/numForm-translator.js new file mode 100644 index 0000000000..2c2d749730 --- /dev/null +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-numForm/numForm-translator.js @@ -0,0 +1,23 @@ +import { NodeTranslator } from '@translator'; + +const XML_NAME = 'w14:numForm'; +const SD_KEY = 'numForm'; + +function readVal(node) { + return node?.attributes?.['w14:val'] ?? node?.attributes?.['w:val']; +} + +export const translator = NodeTranslator.from({ + xmlName: XML_NAME, + sdNodeOrKeyName: SD_KEY, + encode: ({ nodes }) => { + return readVal(nodes?.[0]) ?? undefined; + }, + decode: ({ node }) => { + const value = node?.attrs?.[SD_KEY]; + if (value == null) return undefined; + return { + attributes: { 'w14:val': String(value) }, + }; + }, +}); diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-numForm/numForm-translator.test.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-numForm/numForm-translator.test.js new file mode 100644 index 0000000000..9465a9e6b5 --- /dev/null +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-numForm/numForm-translator.test.js @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest'; +import { NodeTranslator } from '../../../node-translator/node-translator.js'; +import { translator } from './numForm-translator.js'; + +describe('w14:numForm translator', () => { + it('builds a NodeTranslator instance', () => { + expect(translator).toBeInstanceOf(NodeTranslator); + expect(translator.xmlName).toBe('w14:numForm'); + expect(translator.sdNodeOrKeyName).toBe('numForm'); + }); + + it('encodes numForm from w14:val', () => { + const encoded = translator.encode({ + nodes: [{ name: 'w14:numForm', attributes: { 'w14:val': 'lining' } }], + }); + expect(encoded).toBe('lining'); + }); + + it('decodes numForm to w14:val', () => { + const decoded = translator.decode({ + node: { attrs: { numForm: 'oldStyle' } }, + }); + expect(decoded).toEqual({ + attributes: { 'w14:val': 'oldStyle' }, + }); + }); +}); diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-numSpacing/index.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-numSpacing/index.js new file mode 100644 index 0000000000..5555358619 --- /dev/null +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-numSpacing/index.js @@ -0,0 +1 @@ +export * from './numSpacing-translator.js'; diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-numSpacing/numSpacing-translator.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-numSpacing/numSpacing-translator.js new file mode 100644 index 0000000000..d0e50d05b3 --- /dev/null +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-numSpacing/numSpacing-translator.js @@ -0,0 +1,23 @@ +import { NodeTranslator } from '@translator'; + +const XML_NAME = 'w14:numSpacing'; +const SD_KEY = 'numSpacing'; + +function readVal(node) { + return node?.attributes?.['w14:val'] ?? node?.attributes?.['w:val']; +} + +export const translator = NodeTranslator.from({ + xmlName: XML_NAME, + sdNodeOrKeyName: SD_KEY, + encode: ({ nodes }) => { + return readVal(nodes?.[0]) ?? undefined; + }, + decode: ({ node }) => { + const value = node?.attrs?.[SD_KEY]; + if (value == null) return undefined; + return { + attributes: { 'w14:val': String(value) }, + }; + }, +}); diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-numSpacing/numSpacing-translator.test.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-numSpacing/numSpacing-translator.test.js new file mode 100644 index 0000000000..3960b810d5 --- /dev/null +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-numSpacing/numSpacing-translator.test.js @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest'; +import { NodeTranslator } from '../../../node-translator/node-translator.js'; +import { translator } from './numSpacing-translator.js'; + +describe('w14:numSpacing translator', () => { + it('builds a NodeTranslator instance', () => { + expect(translator).toBeInstanceOf(NodeTranslator); + expect(translator.xmlName).toBe('w14:numSpacing'); + expect(translator.sdNodeOrKeyName).toBe('numSpacing'); + }); + + it('encodes numSpacing from w14:val', () => { + const encoded = translator.encode({ + nodes: [{ name: 'w14:numSpacing', attributes: { 'w14:val': 'proportional' } }], + }); + expect(encoded).toBe('proportional'); + }); + + it('decodes numSpacing to w14:val', () => { + const decoded = translator.decode({ + node: { attrs: { numSpacing: 'tabular' } }, + }); + expect(decoded).toEqual({ + attributes: { 'w14:val': 'tabular' }, + }); + }); +}); diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-stylisticSets/index.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-stylisticSets/index.js new file mode 100644 index 0000000000..f9ae1dd366 --- /dev/null +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-stylisticSets/index.js @@ -0,0 +1 @@ +export * from './stylisticSets-translator.js'; diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-stylisticSets/stylisticSets-translator.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-stylisticSets/stylisticSets-translator.js new file mode 100644 index 0000000000..67797a9b87 --- /dev/null +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-stylisticSets/stylisticSets-translator.js @@ -0,0 +1,68 @@ +import { NodeTranslator } from '@translator'; +import { parseBoolean, parseInteger } from '../../utils.js'; + +const XML_NAME = 'w14:stylisticSets'; +const SD_KEY = 'stylisticSets'; +const STYLISTIC_SET_CHILD_XML_NAME = 'w14:ss'; + +function readAttr(attributes, key) { + return attributes?.[`w14:${key}`] ?? attributes?.[`w:${key}`]; +} + +function encodeStylisticSet(node) { + const id = parseInteger(readAttr(node?.attributes, 'id')); + if (id == null) return undefined; + + const valRaw = readAttr(node?.attributes, 'val'); + const val = valRaw == null ? undefined : parseBoolean(valRaw); + + return { + id, + ...(val === undefined ? {} : { val }), + }; +} + +export const translator = NodeTranslator.from({ + xmlName: XML_NAME, + sdNodeOrKeyName: SD_KEY, + encode: ({ nodes }) => { + const stylisticSetsNode = nodes?.[0]; + const children = Array.isArray(stylisticSetsNode?.elements) ? stylisticSetsNode.elements : []; + + const encoded = children + .filter((child) => child?.name === STYLISTIC_SET_CHILD_XML_NAME) + .map(encodeStylisticSet) + .filter(Boolean); + + return encoded.length > 0 ? encoded : undefined; + }, + decode: ({ node }) => { + const values = node?.attrs?.[SD_KEY]; + if (!Array.isArray(values) || values.length === 0) return undefined; + + const elements = values + .filter((entry) => Number.isFinite(entry?.id)) + .map((entry) => { + const attributes = { + 'w14:id': String(entry.id), + }; + + if (entry.val !== undefined) { + attributes['w14:val'] = entry.val ? '1' : '0'; + } + + return { + name: STYLISTIC_SET_CHILD_XML_NAME, + attributes, + }; + }); + + if (elements.length === 0) return undefined; + + return { + name: XML_NAME, + attributes: {}, + elements, + }; + }, +}); diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-stylisticSets/stylisticSets-translator.test.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-stylisticSets/stylisticSets-translator.test.js new file mode 100644 index 0000000000..de1010ad32 --- /dev/null +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/w14-stylisticSets/stylisticSets-translator.test.js @@ -0,0 +1,52 @@ +import { describe, expect, it } from 'vitest'; +import { NodeTranslator } from '../../../node-translator/node-translator.js'; +import { translator } from './stylisticSets-translator.js'; + +describe('w14:stylisticSets translator', () => { + it('builds a NodeTranslator instance', () => { + expect(translator).toBeInstanceOf(NodeTranslator); + expect(translator.xmlName).toBe('w14:stylisticSets'); + expect(translator.sdNodeOrKeyName).toBe('stylisticSets'); + }); + + it('encodes w14:ss children into stylisticSets entries', () => { + const encoded = translator.encode({ + nodes: [ + { + name: 'w14:stylisticSets', + elements: [ + { name: 'w14:ss', attributes: { 'w14:id': '3', 'w14:val': '1' } }, + { name: 'w14:ss', attributes: { 'w14:id': '5', 'w14:val': '0' } }, + ], + }, + ], + }); + + expect(encoded).toEqual([ + { id: 3, val: true }, + { id: 5, val: false }, + ]); + }); + + it('decodes stylisticSets entries into w14:ss children', () => { + const decoded = translator.decode({ + node: { + attrs: { + stylisticSets: [ + { id: 7, val: true }, + { id: 9, val: false }, + ], + }, + }, + }); + + expect(decoded).toEqual({ + name: 'w14:stylisticSets', + attributes: {}, + elements: [ + { name: 'w14:ss', attributes: { 'w14:id': '7', 'w14:val': '1' } }, + { name: 'w14:ss', attributes: { 'w14:id': '9', 'w14:val': '0' } }, + ], + }); + }); +}); diff --git a/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts b/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts index 29c8abcd9f..aacd9538f8 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 @@ -3,9 +3,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { Editor } from '../../core/Editor.js'; import { COMMAND_CATALOG, + INLINE_PROPERTY_REGISTRY, MUTATING_OPERATION_IDS, OPERATION_IDS, buildInternalContractSchemas, + type InlineRunPatchKey, type OperationId, } from '@superdoc/document-api'; import { @@ -17,13 +19,7 @@ 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 } from '../plan-engine/plan-wrappers.js'; -import { - formatFontSizeWrapper, - formatFontFamilyWrapper, - formatColorWrapper, - formatAlignWrapper, -} from '../plan-engine/format-value-wrappers.js'; +import { styleApplyWrapper, formatAlignWrapper } from '../plan-engine/plan-wrappers.js'; import { stylesApplyAdapter } from '../styles-adapter.js'; import { createTableWrapper } from '../plan-engine/create-table-wrapper.js'; import { @@ -1215,6 +1211,82 @@ function expectThrowCode(operationId: OperationId, run: () => unknown): void { expect(COMMAND_CATALOG[operationId].throws.preApply).toContain(capturedCode); } +function buildFormatInlinePatch(key: InlineRunPatchKey): Record { + // Conformance vectors verify operation-level contract semantics (throw/failure/success) + // across all format aliases; a stable patch keeps mock-editor dependencies minimal. + if (!INLINE_PROPERTY_REGISTRY.some((entry) => entry.key === key)) { + throw new Error(`Unknown inline property key "${key}"`); + } + return { bold: true }; +} + +function buildFormatInlineMutationVector(key: InlineRunPatchKey): MutationVector { + return { + throwCase: () => { + const { editor } = makeTextEditor(); + return styleApplyWrapper( + editor, + { + target: { kind: 'text', blockId: 'missing', range: { start: 0, end: 1 } }, + inline: buildFormatInlinePatch(key), + } as any, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const { editor } = makeTextEditor(); + return styleApplyWrapper( + editor, + { + target: { kind: 'text', blockId: 'p1', range: { start: 2, end: 2 } }, + inline: buildFormatInlinePatch(key), + } as any, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const { editor } = makeTextEditor(); + return styleApplyWrapper( + editor, + { + target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } }, + inline: buildFormatInlinePatch(key), + } as any, + { changeMode: 'direct' }, + ); + }, + }; +} + +const formatInlineMutationVectors = Object.fromEntries( + INLINE_PROPERTY_REGISTRY.map((entry) => { + const operationId = `format.${entry.key}` as OperationId; + return [operationId, buildFormatInlineMutationVector(entry.key)]; + }), +) as Partial>; + +const formatInlineDryRunVectors = Object.fromEntries( + INLINE_PROPERTY_REGISTRY.map((entry) => { + const operationId = `format.${entry.key}` as OperationId; + return [ + operationId, + () => { + const { editor, dispatch } = makeTextEditor(); + const result = styleApplyWrapper( + editor, + { + target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } }, + inline: buildFormatInlinePatch(entry.key), + } as any, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + ]; + }), +) as Partial unknown>>; + const mutationVectors: Partial> = { 'blocks.delete': { throwCase: () => { @@ -1338,84 +1410,7 @@ const mutationVectors: Partial> = { ); }, }, - 'format.fontSize': { - throwCase: () => { - const { editor } = makeTextEditor(); - return formatFontSizeWrapper( - editor, - { target: { kind: 'text', blockId: 'missing', range: { start: 0, end: 1 } }, value: '14pt' }, - { changeMode: 'direct' }, - ); - }, - failureCase: () => { - const { editor } = makeTextEditor(); - return formatFontSizeWrapper( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 2, end: 2 } }, value: '14pt' }, - { changeMode: 'direct' }, - ); - }, - applyCase: () => { - const { editor } = makeTextEditor(); - return formatFontSizeWrapper( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } }, value: '14pt' }, - { changeMode: 'direct' }, - ); - }, - }, - 'format.fontFamily': { - throwCase: () => { - const { editor } = makeTextEditor(); - return formatFontFamilyWrapper( - editor, - { target: { kind: 'text', blockId: 'missing', range: { start: 0, end: 1 } }, value: 'Arial' }, - { changeMode: 'direct' }, - ); - }, - failureCase: () => { - const { editor } = makeTextEditor(); - return formatFontFamilyWrapper( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 2, end: 2 } }, value: 'Arial' }, - { changeMode: 'direct' }, - ); - }, - applyCase: () => { - const { editor } = makeTextEditor(); - return formatFontFamilyWrapper( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } }, value: 'Arial' }, - { changeMode: 'direct' }, - ); - }, - }, - 'format.color': { - throwCase: () => { - const { editor } = makeTextEditor(); - return formatColorWrapper( - editor, - { target: { kind: 'text', blockId: 'missing', range: { start: 0, end: 1 } }, value: '#ff0000' }, - { changeMode: 'direct' }, - ); - }, - failureCase: () => { - const { editor } = makeTextEditor(); - return formatColorWrapper( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 2, end: 2 } }, value: '#ff0000' }, - { changeMode: 'direct' }, - ); - }, - applyCase: () => { - const { editor } = makeTextEditor(); - return formatColorWrapper( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } }, value: '#ff0000' }, - { changeMode: 'direct' }, - ); - }, - }, + ...formatInlineMutationVectors, 'format.align': { throwCase: () => { const { editor } = makeTextEditor(); @@ -2951,36 +2946,7 @@ const dryRunVectors: Partial unknown>> = { expect(tr.addMark).not.toHaveBeenCalled(); return result; }, - 'format.fontSize': () => { - const { editor, dispatch } = makeTextEditor(); - const result = formatFontSizeWrapper( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } }, value: '14pt' }, - { changeMode: 'direct', dryRun: true }, - ); - expect(dispatch).not.toHaveBeenCalled(); - return result; - }, - 'format.fontFamily': () => { - const { editor, dispatch } = makeTextEditor(); - const result = formatFontFamilyWrapper( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } }, value: 'Arial' }, - { changeMode: 'direct', dryRun: true }, - ); - expect(dispatch).not.toHaveBeenCalled(); - return result; - }, - 'format.color': () => { - const { editor, dispatch } = makeTextEditor(); - const result = formatColorWrapper( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } }, value: '#ff0000' }, - { changeMode: 'direct', dryRun: true }, - ); - expect(dispatch).not.toHaveBeenCalled(); - return result; - }, + ...formatInlineDryRunVectors, 'format.align': () => { const { editor, dispatch } = makeTextEditor(); const result = formatAlignWrapper( diff --git a/packages/super-editor/src/document-api-adapters/assemble-adapters.test.ts b/packages/super-editor/src/document-api-adapters/assemble-adapters.test.ts index 35fe131a75..73d898828d 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,9 +24,6 @@ describe('assembleDocumentApiAdapters', () => { expect(adapters).toHaveProperty('comments'); expect(adapters).toHaveProperty('write.write'); expect(adapters).toHaveProperty('format.apply'); - expect(adapters).toHaveProperty('format.fontSize'); - expect(adapters).toHaveProperty('format.fontFamily'); - expect(adapters).toHaveProperty('format.color'); expect(adapters).toHaveProperty('format.align'); expect(adapters).toHaveProperty('trackChanges.list'); expect(adapters).toHaveProperty('trackChanges.get'); @@ -74,9 +71,6 @@ 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.fontSize).toBe('function'); - expect(typeof adapters.format.fontFamily).toBe('function'); - expect(typeof adapters.format.color).toBe('function'); expect(typeof adapters.format.align).toBe('function'); expect(typeof adapters.create.paragraph).toBe('function'); expect(typeof adapters.create.heading).toBe('function'); diff --git a/packages/super-editor/src/document-api-adapters/assemble-adapters.ts b/packages/super-editor/src/document-api-adapters/assemble-adapters.ts index 1b7249b16c..b82e5868e5 100644 --- a/packages/super-editor/src/document-api-adapters/assemble-adapters.ts +++ b/packages/super-editor/src/document-api-adapters/assemble-adapters.ts @@ -6,14 +6,13 @@ 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 } from './plan-engine/plan-wrappers.js'; -import { stylesApplyAdapter } from './styles-adapter.js'; import { - formatFontSizeWrapper, - formatFontFamilyWrapper, - formatColorWrapper, + writeWrapper, + insertStructuredWrapper, + styleApplyWrapper, formatAlignWrapper, -} from './plan-engine/format-value-wrappers.js'; +} from './plan-engine/plan-wrappers.js'; +import { stylesApplyAdapter } from './styles-adapter.js'; import { trackChangesListWrapper, trackChangesGetWrapper, @@ -136,9 +135,6 @@ export function assembleDocumentApiAdapters(editor: Editor): DocumentApiAdapters }, format: { apply: (input, options) => styleApplyWrapper(editor, input, options), - fontSize: (input, options) => formatFontSizeWrapper(editor, input, options), - fontFamily: (input, options) => formatFontFamilyWrapper(editor, input, options), - color: (input, options) => formatColorWrapper(editor, input, options), align: (input, options) => formatAlignWrapper(editor, input, options), }, styles: { diff --git a/packages/super-editor/src/document-api-adapters/capabilities-adapter.test.ts b/packages/super-editor/src/document-api-adapters/capabilities-adapter.test.ts index 10cfab9c36..a113a678f3 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 @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; import type { Editor } from '../core/Editor.js'; -import { OPERATION_IDS } from '@superdoc/document-api'; +import { INLINE_PROPERTY_REGISTRY, OPERATION_IDS } from '@superdoc/document-api'; import { TrackFormatMarkName } from '../extensions/track-changes/constants.js'; import { getDocumentApiCapabilities } from './capabilities-adapter.js'; @@ -259,94 +259,163 @@ describe('getDocumentApiCapabilities', () => { }); // --------------------------------------------------------------------------- - // format.fontSize / fontFamily / color / align capability reporting + // format.apply / format.align capability reporting // --------------------------------------------------------------------------- - describe('format value operations', () => { - function makeFormatEditor(overrides: { commands?: Record; marks?: Record } = {}) { + describe('format capabilities', () => { + function makeFormatEditor( + overrides: { + commands?: Record; + marks?: Record; + nodes?: Record; + } = {}, + ) { return makeEditor({ commands: { - setFontSize: vi.fn(() => true), - unsetFontSize: vi.fn(() => true), - setFontFamily: vi.fn(() => true), - unsetFontFamily: vi.fn(() => true), - setColor: vi.fn(() => true), - unsetColor: vi.fn(() => true), setTextAlign: vi.fn(() => true), unsetTextAlign: vi.fn(() => true), ...overrides.commands, } as unknown as Editor['commands'], schema: { marks: { + bold: { create: vi.fn(() => ({ type: 'bold' })) }, + italic: { create: vi.fn(() => ({ type: 'italic' })) }, + underline: { create: vi.fn(() => ({ type: 'underline' })) }, + strike: { create: vi.fn(() => ({ type: 'strike' })) }, + highlight: { create: vi.fn(() => ({ type: 'highlight' })) }, textStyle: { create: vi.fn(() => ({ type: 'textStyle' })) }, + [TrackFormatMarkName]: { create: vi.fn(() => ({ type: TrackFormatMarkName })) }, ...overrides.marks, }, + nodes: { + run: { name: 'run' }, + ...overrides.nodes, + }, } as unknown as Editor['schema'], }); } - it('reports inline format ops as available when commands and textStyle mark are present', () => { - const capabilities = getDocumentApiCapabilities(makeFormatEditor()); - - expect(capabilities.operations['format.fontSize'].available).toBe(true); - expect(capabilities.operations['format.fontFamily'].available).toBe(true); - expect(capabilities.operations['format.color'].available).toBe(true); + it('reports format.apply as available when at least one inline property is supported', () => { + const capabilities = getDocumentApiCapabilities(makeFormatEditor({ marks: { bold: undefined } })); + expect(capabilities.operations['format.apply'].available).toBe(true); }); - it('reports format.align as available when set and unset commands are present', () => { + it('reports a capability entry for every inline property registry key', () => { const capabilities = getDocumentApiCapabilities(makeFormatEditor()); - - expect(capabilities.operations['format.align'].available).toBe(true); + const propertyKeys = Object.keys(capabilities.format.supportedInlineProperties).sort(); + const registryKeys = INLINE_PROPERTY_REGISTRY.map((entry) => entry.key).sort(); + expect(propertyKeys).toEqual(registryKeys); }); - it('reports inline format ops as unavailable when textStyle mark is missing', () => { + it('reports textStyle-backed properties as unavailable when textStyle mark is missing', () => { const capabilities = getDocumentApiCapabilities(makeFormatEditor({ marks: { textStyle: undefined } })); + expect(capabilities.format.supportedInlineProperties.fontSize.available).toBe(false); + expect(capabilities.format.supportedInlineProperties.color.available).toBe(false); + expect(capabilities.format.supportedInlineProperties.bold.available).toBe(true); + }); - expect(capabilities.operations['format.fontSize'].available).toBe(false); - expect(capabilities.operations['format.fontFamily'].available).toBe(false); - expect(capabilities.operations['format.color'].available).toBe(false); - // align is paragraph-level — it does not require the textStyle mark - expect(capabilities.operations['format.align'].available).toBe(true); + it('reports run-attribute properties as unavailable when the run node is missing', () => { + const capabilities = getDocumentApiCapabilities(makeFormatEditor({ nodes: { run: undefined } })); + expect(capabilities.format.supportedInlineProperties.rFonts.available).toBe(false); + expect(capabilities.format.supportedInlineProperties.lang.available).toBe(false); + expect(capabilities.format.supportedInlineProperties.bold.available).toBe(true); }); - it('reports format.fontSize as unavailable when unsetFontSize command is missing', () => { - const capabilities = getDocumentApiCapabilities(makeFormatEditor({ commands: { unsetFontSize: undefined } })); + it('reports tracked support only for tracked inline properties', () => { + const capabilities = getDocumentApiCapabilities(makeFormatEditor()); + expect(capabilities.format.supportedInlineProperties.bold.tracked).toBe(true); + expect(capabilities.format.supportedInlineProperties.rFonts.tracked).toBe(false); + }); + + it('reports format.apply tracked=false when only non-tracked (run-attribute) properties are available', () => { + // Editor has: run node, TrackFormatMarkName, insertTrackedChange, user + // But NO mark-backed inline properties (bold, italic, etc.) — only run-attribute ones + const capabilities = getDocumentApiCapabilities( + makeFormatEditor({ + marks: { + bold: undefined, + italic: undefined, + underline: undefined, + strike: undefined, + highlight: undefined, + textStyle: undefined, + }, + }), + ); + // format.apply is available because run-attribute properties exist + expect(capabilities.operations['format.apply'].available).toBe(true); + // But tracked should be false — no tracked property is available + expect(capabilities.operations['format.apply'].tracked).toBe(false); + }); - expect(capabilities.operations['format.fontSize'].available).toBe(false); - expect(capabilities.operations['format.fontSize'].reasons).toContain('OPERATION_UNAVAILABLE'); + 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'); }); - it('uses OPERATION_UNAVAILABLE without COMMAND_UNAVAILABLE for inline format ops missing textStyle mark', () => { - const capabilities = getDocumentApiCapabilities(makeFormatEditor({ marks: { textStyle: undefined } })); + // ----------------------------------------------------------------------- + // format. operation-level capability parity + // ----------------------------------------------------------------------- - const fontSizeReasons = capabilities.operations['format.fontSize'].reasons ?? []; - expect(fontSizeReasons).toContain('OPERATION_UNAVAILABLE'); - expect(fontSizeReasons).not.toContain('COMMAND_UNAVAILABLE'); + it('reports operations["format.bold"] as unavailable when bold mark is missing', () => { + const capabilities = getDocumentApiCapabilities(makeFormatEditor({ marks: { bold: undefined } })); + expect(capabilities.operations['format.bold'].available).toBe(false); + expect(capabilities.operations['format.bold'].reasons).toContain('OPERATION_UNAVAILABLE'); + expect(capabilities.operations['format.bold'].reasons).not.toContain('COMMAND_UNAVAILABLE'); }); - it('reports all format value ops with dryRun support', () => { - const capabilities = getDocumentApiCapabilities(makeFormatEditor()); + it('reports operations["format.color"] tracked=false when TrackFormatMarkName is missing', () => { + const capabilities = getDocumentApiCapabilities( + makeFormatEditor({ marks: { [TrackFormatMarkName]: undefined } }), + ); + // color is textStyle-backed → still available + expect(capabilities.operations['format.color'].available).toBe(true); + expect(capabilities.operations['format.color'].tracked).toBe(false); + }); - expect(capabilities.operations['format.fontSize'].dryRun).toBe(true); - expect(capabilities.operations['format.fontFamily'].dryRun).toBe(true); - expect(capabilities.operations['format.color'].dryRun).toBe(true); - expect(capabilities.operations['format.align'].dryRun).toBe(true); + it('reports operations["format.rFonts"] as unavailable when run node is missing', () => { + const capabilities = getDocumentApiCapabilities(makeFormatEditor({ nodes: { run: undefined } })); + expect(capabilities.operations['format.rFonts'].available).toBe(false); + expect(capabilities.operations['format.rFonts'].reasons).toContain('OPERATION_UNAVAILABLE'); }); - it('reports all format value ops as direct-only (tracked = false)', () => { + it('reports operations["format.rFonts"] tracked=false because run-attribute properties are not tracked', () => { const capabilities = getDocumentApiCapabilities(makeFormatEditor()); + expect(capabilities.operations['format.rFonts'].available).toBe(true); + expect(capabilities.operations['format.rFonts'].tracked).toBe(false); + }); - expect(capabilities.operations['format.fontSize'].tracked).toBe(false); - expect(capabilities.operations['format.fontFamily'].tracked).toBe(false); - expect(capabilities.operations['format.color'].tracked).toBe(false); - expect(capabilities.operations['format.align'].tracked).toBe(false); + it('ensures every format. operation matches its supportedInlineProperties entry', () => { + const capabilities = getDocumentApiCapabilities(makeFormatEditor()); + for (const entry of INLINE_PROPERTY_REGISTRY) { + const operationId = `format.${entry.key}` as `format.${typeof entry.key}`; + const operation = capabilities.operations[operationId]; + const property = capabilities.format.supportedInlineProperties[entry.key]; + expect(operation.available, `${operationId} available mismatch`).toBe(property.available); + expect(operation.tracked, `${operationId} tracked mismatch`).toBe(property.tracked); + } + }); + + it('ensures parity holds when marks/nodes are partially missing', () => { + // Remove textStyle (affects color, fontSize, etc.) and run node (affects rFonts, lang, etc.) + const capabilities = getDocumentApiCapabilities( + makeFormatEditor({ marks: { textStyle: undefined }, nodes: { run: undefined } }), + ); + for (const entry of INLINE_PROPERTY_REGISTRY) { + const operationId = `format.${entry.key}` as `format.${typeof entry.key}`; + const operation = capabilities.operations[operationId]; + const property = capabilities.format.supportedInlineProperties[entry.key]; + expect(operation.available, `${operationId} available mismatch`).toBe(property.available); + expect(operation.tracked, `${operationId} tracked mismatch`).toBe(property.tracked); + } }); }); diff --git a/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts b/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts index a279933b26..28b8887168 100644 --- a/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts +++ b/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts @@ -2,14 +2,15 @@ import type { Editor } from '../core/Editor.js'; import { CAPABILITY_REASON_CODES, COMMAND_CATALOG, - MARK_KEYS, - INLINE_DIRECTIVES, - CORE_TOGGLE_PROPERTY_ID_SET, + INLINE_PROPERTY_BY_KEY, + INLINE_PROPERTY_KEY_SET, + INLINE_PROPERTY_REGISTRY, type CapabilityReasonCode, type DocumentApiCapabilities, + type InlinePropertyRegistryEntry, + type InlineRunPatchKey, type PlanEngineCapabilities, type FormatCapabilities, - type FormatPropertyCapability, type OperationId, OPERATION_IDS, } from '@superdoc/document-api'; @@ -29,9 +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.fontSize': ['setTextSelection', 'setFontSize', 'unsetFontSize'], - 'format.fontFamily': ['setTextSelection', 'setFontFamily', 'unsetFontFamily'], - 'format.color': ['setTextSelection', 'setColor', 'unsetColor'], 'format.align': ['setTextSelection', 'setTextAlign', 'unsetTextAlign'], 'create.paragraph': ['insertParagraphAt'], 'create.heading': ['insertHeadingAt'], @@ -125,24 +123,30 @@ function hasMarkCapability(editor: Editor, markName: string): boolean { return Boolean(editor.schema?.marks?.[markName]); } -/** Mark key → editor schema mark name mapping (shared source of truth for format.apply). */ -const STYLE_MARK_SCHEMA_NAMES: Record = { - bold: 'bold', - italic: 'italic', - underline: 'underline', - strike: 'strike', -}; - /** Operation IDs whose availability is determined by schema mark presence, not editor commands. */ function isMarkBackedOperation(operationId: OperationId): boolean { - return operationId === 'format.apply'; + return operationId === 'format.apply' || getInlineAliasKey(operationId) !== undefined; } /** - * Inline value-format operations (fontSize, fontFamily, color) require the 'textStyle' - * mark in the schema — they apply values via `setMark('textStyle', ...)`. + * If `operationId` is a `format.` alias, returns the corresponding + * inline-property registry entry. Returns `undefined` otherwise. */ -const INLINE_FORMAT_OPERATIONS = new Set(['format.fontSize', 'format.fontFamily', 'format.color']); +function getInlineAliasKey(operationId: OperationId): InlineRunPatchKey | undefined { + if (!operationId.startsWith('format.')) return undefined; + const key = operationId.slice('format.'.length); + if (INLINE_PROPERTY_KEY_SET.has(key)) return key as InlineRunPatchKey; + return undefined; +} + +function isInlinePropertyAvailable(editor: Editor, property: InlinePropertyRegistryEntry): boolean { + if (property.storage === 'mark') { + if (property.carrier.storage !== 'mark') return false; + const markName = property.carrier.markName === 'textStyle' ? 'textStyle' : property.carrier.markName; + return hasMarkCapability(editor, markName); + } + return Boolean(editor.schema?.nodes?.run); +} function hasTrackedModeCapability(editor: Editor, operationId: OperationId): boolean { if (!hasCommand(editor, 'insertTrackedChange')) return false; @@ -150,6 +154,20 @@ function hasTrackedModeCapability(editor: Editor, operationId: OperationId): boo // report tracked mode as unavailable when no user is configured so capability- // gated clients don't offer tracked actions that would deterministically fail. if (!editor.options?.user) return false; + + // Inline alias operations additionally require the per-property tracked flag. + const inlineKey = getInlineAliasKey(operationId); + if (inlineKey !== undefined) { + if (!INLINE_PROPERTY_BY_KEY[inlineKey].tracked) return false; + return Boolean(editor.schema?.marks?.[TrackFormatMarkName]); + } + + if (operationId === 'format.apply') { + if (!editor.schema?.marks?.[TrackFormatMarkName]) return false; + // Only report tracked if at least one tracked inline property is available. + return INLINE_PROPERTY_REGISTRY.some((property) => property.tracked && isInlinePropertyAvailable(editor, property)); + } + if (isMarkBackedOperation(operationId)) { return Boolean(editor.schema?.marks?.[TrackFormatMarkName]); } @@ -189,7 +207,9 @@ function pushReason(reasons: CapabilityReasonCode[], reason: CapabilityReasonCod /** Operations that determine availability through non-command mechanisms. */ function isNonCommandBackedOperation(operationId: OperationId): boolean { - return operationId === 'format.apply' || operationId === 'styles.apply' || INLINE_FORMAT_OPERATIONS.has(operationId); + return ( + operationId === 'format.apply' || operationId === 'styles.apply' || getInlineAliasKey(operationId) !== undefined + ); } /** Checks whether the styles part has a valid w:styles root element. */ @@ -219,14 +239,15 @@ function getStylesApplyUnavailableReason(editor: Editor): CapabilityReasonCode | } function isOperationAvailable(editor: Editor, operationId: OperationId): boolean { - // format.apply is available if at least one mark type exists in the schema + // format.apply is available when at least one inline property can be executed. if (operationId === 'format.apply') { - return MARK_KEYS.some((key) => hasMarkCapability(editor, STYLE_MARK_SCHEMA_NAMES[key] ?? key)); + return INLINE_PROPERTY_REGISTRY.some((property) => isInlinePropertyAvailable(editor, property)); } - // Inline format ops (fontSize, fontFamily, color) require the textStyle mark in the schema - if (INLINE_FORMAT_OPERATIONS.has(operationId)) { - return hasAllCommands(editor, operationId) && hasMarkCapability(editor, 'textStyle'); + // format. aliases derive availability from the corresponding inline property. + const inlineKey = getInlineAliasKey(operationId); + if (inlineKey !== undefined) { + return isInlinePropertyAvailable(editor, INLINE_PROPERTY_BY_KEY[inlineKey]); } // styles.apply requires converter + styles part + no collaboration @@ -359,15 +380,20 @@ const SUPPORTED_SET_MARKS = ['bold', 'italic', 'underline', 'strike'] as const; const REGEX_MAX_PATTERN_LENGTH = 1024; function buildFormatCapabilities(editor: Editor): FormatCapabilities { - const properties: Record = {}; - for (const key of MARK_KEYS) { - if (hasMarkCapability(editor, STYLE_MARK_SCHEMA_NAMES[key] ?? key)) { - // Classify from registry: pure toggles vs composite (underline has rich attrs) - const kind = CORE_TOGGLE_PROPERTY_ID_SET.has(key) ? 'toggle' : 'composite'; - properties[key] = { kind, directives: [...INLINE_DIRECTIVES] }; - } + const trackedInlinePropertiesSupported = hasTrackedModeCapability(editor, 'format.apply'); + const supportedInlineProperties = {} as FormatCapabilities['supportedInlineProperties']; + + for (const property of INLINE_PROPERTY_REGISTRY) { + const available = isInlinePropertyAvailable(editor, property); + supportedInlineProperties[property.key] = { + available, + tracked: available && property.tracked && trackedInlinePropertiesSupported, + type: property.type, + storage: property.storage, + }; } - return { properties }; + + return { supportedInlineProperties }; } function buildPlanEngineCapabilities(): PlanEngineCapabilities { diff --git a/packages/super-editor/src/document-api-adapters/plan-engine/executor.test.ts b/packages/super-editor/src/document-api-adapters/plan-engine/executor.test.ts index bf2d8b4e64..351eb950bf 100644 --- a/packages/super-editor/src/document-api-adapters/plan-engine/executor.test.ts +++ b/packages/super-editor/src/document-api-adapters/plan-engine/executor.test.ts @@ -1,11 +1,12 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import type { Editor } from '../../core/Editor.js'; -import type { TextRewriteStep, StyleApplyStep, AssertStep } from '@superdoc/document-api'; +import type { TextRewriteStep, TextInsertStep, StyleApplyStep, AssertStep } from '@superdoc/document-api'; import type { CompiledTarget } from './executor-registry.types.js'; import type { CompiledPlan } from './compiler.js'; import { executeCompiledPlan, executeCreateStep, + executeTextInsert, executeSpanTextDelete, executeSpanTextRewrite, executeStyleApply, @@ -186,6 +187,98 @@ function setupResolveTextRange(from: number, to: number) { mockedDeps.resolveTextRangeInBlock.mockReturnValue({ from, to }); } +function createTestMark(name: string, attrs: Record = {}) { + return { + type: { + name, + create: (nextAttrs?: Record | null) => + createTestMark(name, (nextAttrs ?? {}) as Record), + }, + attrs, + eq: (other: any) => other?.type?.name === name, + }; +} + +describe('executeTextInsert: setMarks tri-state directives', () => { + it('maps on/off/clear to canonical mark emission', () => { + const boldCreate = vi.fn((attrs?: Record | null) => + createTestMark('bold', (attrs ?? {}) as Record), + ); + const italicCreate = vi.fn((attrs?: Record | null) => + createTestMark('italic', (attrs ?? {}) as Record), + ); + const underlineCreate = vi.fn((attrs?: Record | null) => + createTestMark('underline', (attrs ?? {}) as Record), + ); + const strikeCreate = vi.fn((attrs?: Record | null) => + createTestMark('strike', (attrs ?? {}) as Record), + ); + + const text = vi.fn((value: string, marks?: unknown[]) => ({ + type: { name: 'text' }, + text: value, + marks: marks ?? [], + })); + + const editor = { + state: { + schema: { + marks: { + bold: { create: boldCreate }, + italic: { create: italicCreate }, + underline: { create: underlineCreate }, + strike: { create: strikeCreate }, + }, + text, + }, + }, + } as unknown as Editor; + + const tr = { + doc: { + resolve: vi.fn(() => ({ marks: () => [] })), + }, + insert: vi.fn(), + }; + + const target = makeTarget({ op: 'text.insert' as any, absFrom: 3, absTo: 3 }) as any; + const step: TextInsertStep = { + id: 'insert-tristate', + op: 'text.insert', + where: { by: 'select', select: { type: 'text', pattern: 'x' }, require: 'first' }, + args: { + position: 'before', + content: { text: 'hello' }, + style: { + inline: { + mode: 'set', + setMarks: { + bold: 'off', + italic: 'on', + underline: 'off', + strike: 'clear', + }, + }, + }, + }, + } as any; + + const outcome = executeTextInsert(editor, tr as any, target, step, { map: (pos: number) => pos } as any); + + expect(outcome).toEqual({ changed: true }); + expect(boldCreate).toHaveBeenCalledWith({ value: '0' }); + expect(italicCreate).toHaveBeenCalledTimes(1); + expect(underlineCreate).toHaveBeenCalledWith({ underlineType: 'none' }); + expect(strikeCreate).not.toHaveBeenCalled(); + + const insertedNode = tr.insert.mock.calls[0][1]; + const insertedMarks = insertedNode.marks as Array<{ type: { name: string }; attrs: Record }>; + expect(insertedMarks.map((mark) => mark.type.name)).toEqual(['bold', 'italic', 'underline']); + expect(insertedMarks.find((mark) => mark.type.name === 'bold')?.attrs).toEqual({ value: '0' }); + expect(insertedMarks.find((mark) => mark.type.name === 'underline')?.attrs).toEqual({ underlineType: 'none' }); + }); +}); + // --------------------------------------------------------------------------- // text.rewrite — style preservation behavioral tests // --------------------------------------------------------------------------- diff --git a/packages/super-editor/src/document-api-adapters/plan-engine/executor.ts b/packages/super-editor/src/document-api-adapters/plan-engine/executor.ts index 63676da945..3d2f1e4795 100644 --- a/packages/super-editor/src/document-api-adapters/plan-engine/executor.ts +++ b/packages/super-editor/src/document-api-adapters/plan-engine/executor.ts @@ -22,11 +22,13 @@ import type { SetMarks, ReplacementPayload, Query, - MarkKey, - InlineToggleDirective, + InlineRunPatchKey, + UnderlinePatch, + RFontsPatch, + BorderPatch, + StyleApplyInput, } from '@superdoc/document-api'; -import { MARK_KEYS } from '@superdoc/document-api'; -import { TOGGLE_MARK_SPECS } from './mark-directives.js'; +import { INLINE_PROPERTY_BY_KEY } from '@superdoc/document-api'; import type { Editor } from '../../core/Editor.js'; import type { CompiledPlan } from './compiler.js'; import type { @@ -43,11 +45,12 @@ import { getBlockIndex } from '../helpers/index-cache.js'; import { resolveBlockInsertionPos } from './create-insertion.js'; import { applyDirectMutationMeta, applyTrackedMutationMeta } from '../helpers/transaction-meta.js'; import { captureRunsInRange, resolveInlineStyle } from './style-resolver.js'; +import { TOGGLE_MARK_SPECS } from './mark-directives.js'; import { mapBlockNodeType } from '../helpers/node-address-resolver.js'; import { resolveWithinScope, scopeByRange } from '../helpers/adapter-utils.js'; import { normalizeReplacementText } from './replacement-normalizer.js'; import { Fragment, Slice } from 'prosemirror-model'; -import type { Mark as ProseMirrorMark, MarkType, Node as ProseMirrorNode } from 'prosemirror-model'; +import type { Mark as ProseMirrorMark, MarkType, Node as ProseMirrorNode, NodeType } from 'prosemirror-model'; import type { Transaction } from 'prosemirror-state'; import type { Mapping } from 'prosemirror-transform'; @@ -60,6 +63,7 @@ const DEFAULT_INLINE_POLICY: import('@superdoc/document-api').InlineStylePolicy mode: 'preserve', onNonUniform: 'majority', }; +const CORE_SET_MARK_KEYS = ['bold', 'italic', 'underline', 'strike'] as const; function asProseMirrorMarks(marks: readonly unknown[]): readonly ProseMirrorMark[] { return marks as readonly ProseMirrorMark[]; @@ -89,51 +93,532 @@ function buildMarksFromSetMarks(editor: Editor, setMarks?: SetMarks): readonly P const { schema } = editor.state; const marks: ProseMirrorMark[] = []; - for (const key of MARK_KEYS) { + for (const key of CORE_SET_MARK_KEYS) { const directive = setMarks[key]; if (!directive) continue; - const markType = schema.marks[TOGGLE_MARK_SPECS[key].schemaName]; - if (!markType) continue; const spec = TOGGLE_MARK_SPECS[key]; + const markType = schema.marks[spec.schemaName]; + if (!markType) continue; + if (directive === 'on') { - marks.push(spec.createOn(markType) as unknown as ProseMirrorMark); - } else if (directive === 'off') { - marks.push(markType.create(spec.offAttrs) as unknown as ProseMirrorMark); + marks.push(spec.createOn(markType as unknown as { create: MarkType['create'] }) as unknown as ProseMirrorMark); + continue; + } + if (directive === 'off') { + marks.push(markType.create(spec.offAttrs)); } - // 'clear' → skip (no mark) + // `clear` intentionally emits no mark. } return marks; } // --------------------------------------------------------------------------- -// Shared inline style patch — applies boolean mark patches to a range +// Shared inline style patch — registry-driven mark + runAttribute mutation // --------------------------------------------------------------------------- -/** Applies inline mark directives (bold, italic, underline, strike) to a document range. */ -function applyInlineMarkPatches( +type InlineRunPatch = StyleApplyInput['inline']; +type TextStylePatchKey = 'color' | 'fontSize' | 'letterSpacing' | 'vertAlign' | 'position'; +type TextStylePatch = Partial>; + +interface InlineTextSegment { + from: number; + to: number; + marks: readonly ProseMirrorMark[]; +} + +interface OverlappingRun { + pos: number; +} + +const BOOLEAN_INLINE_MARK_KEYS = ['bold', 'italic', 'strike'] as const; +const TEXT_STYLE_KEYS = ['color', 'fontSize', 'letterSpacing', 'vertAlign', 'position'] as const; +const PRESERVE_RUN_PROPERTIES_META_KEY = 'sdPreserveRunPropertiesKeys'; + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function isDeepEqual(left: unknown, right: unknown): boolean { + return JSON.stringify(left) === JSON.stringify(right); +} + +function normalizeHexColor(color: string): string { + const trimmed = color.trim(); + if (trimmed.startsWith('#')) return trimmed; + if (/^[0-9a-fA-F]{3,8}$/.test(trimmed)) return `#${trimmed}`; + return trimmed; +} + +function toPointString(value: number): string { + return `${value}pt`; +} + +function toHalfPoints(value: number): number { + return Math.round(value * 2); +} + +function collectInlineTextSegments(doc: ProseMirrorNode, absFrom: number, absTo: number): InlineTextSegment[] { + const segments: InlineTextSegment[] = []; + + doc.nodesBetween(absFrom, absTo, (node, pos) => { + if (!node.isText) return; + + const from = Math.max(absFrom, pos); + const to = Math.min(absTo, pos + node.nodeSize); + if (from >= to) return; + + segments.push({ + from, + to, + marks: (node.marks ?? []) as readonly ProseMirrorMark[], + }); + }); + + return segments; +} + +function findMark(marks: readonly ProseMirrorMark[], markTypeName: string): ProseMirrorMark | undefined { + return marks.find((mark) => mark.type.name === markTypeName); +} + +function compactAttrs(attrs: Record): Record { + return Object.fromEntries(Object.entries(attrs).filter(([, value]) => value !== null && value !== undefined)); +} + +function applyTriStateMarkPatch( + tr: Transaction, + markType: MarkType | undefined, + absFrom: number, + absTo: number, + value: boolean | null | undefined, +): boolean { + if (value === undefined || !markType) return false; + + if (value === null) { + tr.removeMark(absFrom, absTo, markType); + return true; + } + + if (value === true) { + tr.addMark(absFrom, absTo, markType.create()); + return true; + } + + tr.addMark(absFrom, absTo, markType.create({ value: '0' })); + return true; +} + +function applyHighlightPatch( + tr: Transaction, + markType: MarkType | undefined, + absFrom: number, + absTo: number, + value: string | null | undefined, +): boolean { + if (value === undefined || !markType) return false; + + if (value === null) { + tr.removeMark(absFrom, absTo, markType); + return true; + } + + tr.addMark(absFrom, absTo, markType.create({ color: value })); + return true; +} + +function mergeTextStyleAttrs(currentAttrs: Record, patch: TextStylePatch): Record { + const next = { ...currentAttrs }; + + for (const key of TEXT_STYLE_KEYS) { + const value = patch[key]; + if (value === undefined) continue; + + if (value === null) { + delete next[key]; + continue; + } + + if (key === 'color' && typeof value === 'string') { + next.color = normalizeHexColor(value); + continue; + } + if ((key === 'fontSize' || key === 'letterSpacing' || key === 'position') && typeof value === 'number') { + next[key] = toPointString(value); + continue; + } + + next[key] = value as string; + } + + return compactAttrs(next); +} + +function applyTextStylePatch( + tr: Transaction, + markType: MarkType | undefined, + absFrom: number, + absTo: number, + patch: TextStylePatch, +): boolean { + if (!markType) return false; + if (Object.keys(patch).length === 0) return false; + + const segments = collectInlineTextSegments(tr.doc, absFrom, absTo); + let changed = false; + + for (const segment of segments) { + const existingMark = findMark(segment.marks, markType.name); + const existingAttrs = (existingMark?.attrs ?? {}) as Record; + const nextAttrs = mergeTextStyleAttrs(existingAttrs, patch); + const hadMark = Boolean(existingMark); + const shouldKeepMark = Object.keys(nextAttrs).length > 0; + + if (!hadMark && !shouldKeepMark) continue; + if (hadMark && shouldKeepMark && isDeepEqual(existingAttrs, nextAttrs)) continue; + + tr.removeMark(segment.from, segment.to, markType); + if (shouldKeepMark) { + tr.addMark(segment.from, segment.to, markType.create(nextAttrs)); + } + changed = true; + } + + return changed; +} + +function mergeUnderlineAttrs(currentAttrs: Record, patchValue: InlineRunPatch['underline']) { + if (patchValue === null) return null; + + const next = { ...currentAttrs }; + + if (patchValue === true) { + if (!next.underlineType || next.underlineType === 'none') next.underlineType = 'single'; + return compactAttrs(next); + } + + if (patchValue === false) { + next.underlineType = 'none'; + return compactAttrs(next); + } + + const patch = patchValue as UnderlinePatch; + + if (patch.style !== undefined) { + if (patch.style === null) delete next.underlineType; + else next.underlineType = patch.style; + } + if (patch.color !== undefined) { + if (patch.color === null) delete next.underlineColor; + else next.underlineColor = normalizeHexColor(patch.color); + } + if (patch.themeColor !== undefined) { + if (patch.themeColor === null) delete next.underlineThemeColor; + else next.underlineThemeColor = patch.themeColor; + } + + if (!next.underlineType && (next.underlineColor || next.underlineThemeColor)) { + next.underlineType = 'single'; + } + + const compacted = compactAttrs(next); + return Object.keys(compacted).length > 0 ? compacted : null; +} + +function applyUnderlinePatch( + tr: Transaction, + markType: MarkType | undefined, + absFrom: number, + absTo: number, + patchValue: InlineRunPatch['underline'], +): boolean { + if (patchValue === undefined || !markType) return false; + + const segments = collectInlineTextSegments(tr.doc, absFrom, absTo); + let changed = false; + + for (const segment of segments) { + const existingMark = findMark(segment.marks, markType.name); + const existingAttrs = (existingMark?.attrs ?? {}) as Record; + const nextAttrs = mergeUnderlineAttrs(existingAttrs, patchValue); + const hadMark = Boolean(existingMark); + + if (!hadMark && nextAttrs === null) continue; + if (hadMark && nextAttrs !== null && isDeepEqual(existingAttrs, nextAttrs)) continue; + + tr.removeMark(segment.from, segment.to, markType); + if (nextAttrs !== null) { + tr.addMark(segment.from, segment.to, markType.create(nextAttrs)); + } + changed = true; + } + + return changed; +} + +function collectOverlappingRuns( + doc: ProseMirrorNode, + runType: NodeType, + absFrom: number, + absTo: number, +): OverlappingRun[] { + const runs: OverlappingRun[] = []; + + doc.nodesBetween(absFrom, absTo, (node, pos) => { + if (node.type !== runType) return true; + + const runFrom = pos + 1; + const runTo = pos + node.nodeSize - 1; + if (Math.max(absFrom, runFrom) < Math.min(absTo, runTo)) { + runs.push({ pos }); + } + return false; + }); + + return runs; +} + +function normalizeRFontsPatch(patch: RFontsPatch): Record { + const next: Record = {}; + + for (const [key, value] of Object.entries(patch)) { + if (key === 'csTheme') { + next.cstheme = value; + continue; + } + next[key] = value; + } + + return next; +} + +function normalizeBorderPatch(patch: BorderPatch): Record { + const next: Record = {}; + + if (patch.val !== undefined) next.val = patch.val; + if (patch.sz !== undefined) next.size = patch.sz; + if (patch.color !== undefined) next.color = patch.color; + if (patch.space !== undefined) next.space = patch.space; + + return next; +} + +function normalizeRunAttributePatchValue(key: InlineRunPatchKey, value: unknown): unknown { + if (value === null) return null; + + switch (key) { + case 'border': + return normalizeBorderPatch(value as BorderPatch); + case 'rFonts': + return normalizeRFontsPatch(value as RFontsPatch); + case 'fontSizeCs': + return toHalfPoints(value as number); + case 'kerning': + return toHalfPoints(value as number); + case 'stylisticSets': + return Array.isArray(value) ? value.map((entry) => ({ ...entry })) : value; + default: + return value; + } +} + +function mergeRunAttributeValue(currentValue: unknown, patchValue: unknown): { changed: boolean; nextValue?: unknown } { + if (patchValue === null) { + return { changed: currentValue !== undefined }; + } + + if (Array.isArray(patchValue)) { + if (isDeepEqual(currentValue, patchValue)) return { changed: false, nextValue: currentValue }; + return { changed: true, nextValue: patchValue }; + } + + if (isRecord(patchValue)) { + const merged = isRecord(currentValue) ? { ...currentValue } : {}; + for (const [key, value] of Object.entries(patchValue)) { + if (value === null || value === undefined) delete merged[key]; + else merged[key] = value; + } + if (Object.keys(merged).length === 0) { + return { changed: currentValue !== undefined }; + } + if (isDeepEqual(currentValue, merged)) return { changed: false, nextValue: currentValue }; + return { changed: true, nextValue: merged }; + } + + if (Object.is(currentValue, patchValue)) return { changed: false, nextValue: currentValue }; + return { changed: true, nextValue: patchValue }; +} + +function buildRunAttributeUpdates(inline: InlineRunPatch): Record { + const updates: Record = {}; + const keys = Object.keys(inline) as InlineRunPatchKey[]; + + for (const key of keys) { + const entry = INLINE_PROPERTY_BY_KEY[key]; + if (!entry || entry.storage !== 'runAttribute') continue; + + const value = inline[key]; + if (value === undefined) continue; + + const carrier = entry.carrier; + const runPropertyKey = + carrier.storage === 'runAttribute' + ? carrier.runPropertyKey + : (() => { + throw planError('INTERNAL_ERROR', `Invalid carrier for runAttribute key "${key}"`); + })(); + + updates[runPropertyKey] = normalizeRunAttributePatchValue(key, value); + } + + return updates; +} + +function applyRunAttributePatch( + tr: Transaction, + runType: NodeType | undefined, + absFrom: number, + absTo: number, + updates: Record, +): boolean { + if (!runType) return false; + if (Object.keys(updates).length === 0) return false; + + const overlappingRuns = collectOverlappingRuns(tr.doc, runType, absFrom, absTo).sort( + (left, right) => right.pos - left.pos, + ); + if (overlappingRuns.length === 0) return false; + + if (Object.prototype.hasOwnProperty.call(updates, 'fontFamily')) { + tr.setMeta(PRESERVE_RUN_PROPERTIES_META_KEY, ['fontFamily']); + } + + let changed = false; + + for (const run of overlappingRuns) { + const runPos = tr.mapping.map(run.pos, 1); + const runNode = tr.doc.nodeAt(runPos); + if (!runNode || runNode.type !== runType) continue; + + const mappedFrom = tr.mapping.map(absFrom, 1); + const mappedTo = tr.mapping.map(absTo, -1); + const runContentFrom = runPos + 1; + const runContentTo = runPos + runNode.nodeSize - 1; + + const patchFrom = Math.max(mappedFrom, runContentFrom); + const patchTo = Math.min(mappedTo, runContentTo); + if (patchFrom >= patchTo) continue; + + const currentRunProperties = isRecord(runNode.attrs?.runProperties) + ? { ...(runNode.attrs.runProperties as Record) } + : {}; + + const nextRunProperties = { ...currentRunProperties }; + let runChanged = false; + + for (const [runPropertyKey, patchValue] of Object.entries(updates)) { + const existingValue = nextRunProperties[runPropertyKey]; + const mergeResult = mergeRunAttributeValue(existingValue, patchValue); + if (!mergeResult.changed) continue; + + if (mergeResult.nextValue === undefined) { + delete nextRunProperties[runPropertyKey]; + } else { + nextRunProperties[runPropertyKey] = mergeResult.nextValue; + } + + runChanged = true; + } + + if (!runChanged) continue; + + const normalizedNextRunProperties = Object.keys(nextRunProperties).length > 0 ? nextRunProperties : null; + const fullRunSelected = patchFrom === runContentFrom && patchTo === runContentTo; + + if (fullRunSelected) { + tr.setNodeMarkup( + runPos, + runNode.type, + { ...runNode.attrs, runProperties: normalizedNextRunProperties }, + runNode.marks, + ); + changed = true; + continue; + } + + const relativeFrom = patchFrom - runContentFrom; + const relativeTo = patchTo - runContentFrom; + if (relativeFrom === relativeTo) continue; + + const replacementRuns: ProseMirrorNode[] = []; + + if (relativeFrom > 0) { + const leftContent = runNode.content.cut(0, relativeFrom); + replacementRuns.push(runType.create(runNode.attrs, leftContent, runNode.marks)); + } + + const middleContent = runNode.content.cut(relativeFrom, relativeTo); + replacementRuns.push( + runType.create({ ...runNode.attrs, runProperties: normalizedNextRunProperties }, middleContent, runNode.marks), + ); + + if (relativeTo < runNode.content.size) { + const rightContent = runNode.content.cut(relativeTo, runNode.content.size); + replacementRuns.push(runType.create(runNode.attrs, rightContent, runNode.marks)); + } + + // Pass node array directly so the transaction builds Fragment using its own + // prosemirror-model instance (avoids cross-instance Fragment errors). + tr.replaceWith(runPos, runPos + runNode.nodeSize, replacementRuns); + changed = true; + } + + return changed; +} + +function applyInlinePatchToRange( editor: Editor, tr: Transaction, absFrom: number, absTo: number, - inline: StyleApplyStep['args']['inline'], + inline: InlineRunPatch, ): boolean { + if (absFrom >= absTo) return false; + const { schema } = editor.state; let changed = false; - for (const key of MARK_KEYS) { - const directive = inline[key] as InlineToggleDirective | undefined; - if (!directive) continue; + for (const key of BOOLEAN_INLINE_MARK_KEYS) { + const markType = schema.marks[key]; + const value = inline[key] as boolean | null | undefined; + if (applyTriStateMarkPatch(tr, markType, absFrom, absTo, value)) { + changed = true; + } + } - const spec = TOGGLE_MARK_SPECS[key]; - const markType = schema.marks[spec.schemaName] as MarkType | undefined; - if (!markType) continue; + if (applyUnderlinePatch(tr, schema.marks.underline, absFrom, absTo, inline.underline)) { + changed = true; + } - if (applyDirectiveToTransaction(tr, absFrom, absTo, markType, spec, directive)) { - changed = true; + if (applyHighlightPatch(tr, schema.marks.highlight, absFrom, absTo, inline.highlight)) { + changed = true; + } + + const textStylePatch: TextStylePatch = {}; + for (const key of TEXT_STYLE_KEYS) { + if (inline[key] !== undefined) { + (textStylePatch as Record)[key] = inline[key]; } } + if (applyTextStylePatch(tr, schema.marks.textStyle, absFrom, absTo, textStylePatch)) { + changed = true; + } + + const runAttributeUpdates = buildRunAttributeUpdates(inline); + if (applyRunAttributePatch(tr, schema.nodes?.run, absFrom, absTo, runAttributeUpdates)) { + changed = true; + } return changed; } @@ -236,82 +721,6 @@ export function executeTextDelete( return { changed: true }; } -/** - * Collects sub-ranges within [from, to) where the mark is NOT already in ON - * form. Nodes already ON are skipped so their rich attrs (e.g. wavy underline) - * are preserved. Returns the sub-ranges in reverse document order for safe - * sequential application without position-shift concerns. - * - * Falls back to the full range if `doc.nodesBetween` is unavailable (mocks). - */ -function collectNonOnSubRanges( - doc: ProseMirrorNode, - from: number, - to: number, - markType: MarkType, - spec: (typeof TOGGLE_MARK_SPECS)[MarkKey], -): Array<{ from: number; to: number }> { - if (from === to) return []; - if (typeof doc.nodesBetween !== 'function') return [{ from, to }]; - - const ranges: Array<{ from: number; to: number }> = []; - doc.nodesBetween(from, to, (node, pos) => { - if (!node.isText) return; - const mark = node.marks.find((m) => m.type === markType); - if (mark && spec.isOn(mark as unknown as Parameters[0])) return; - // Node is absent or OFF — needs conversion to ON - ranges.push({ - from: Math.max(pos, from), - to: Math.min(pos + node.nodeSize, to), - }); - }); - - // Reverse for safe back-to-front application - ranges.reverse(); - return ranges; -} - -/** - * Applies a single inline toggle directive to a transaction range. - * Shared by both range and span style-apply executors. - * - * Returns true if any transaction steps were actually emitted. - */ -function applyDirectiveToTransaction( - tr: Transaction, - absFrom: number, - absTo: number, - markType: MarkType, - spec: (typeof TOGGLE_MARK_SPECS)[MarkKey], - directive: InlineToggleDirective, -): boolean { - const stepsBefore = tr.steps?.length ?? 0; - - switch (directive) { - case 'on': { - // Apply ON only to sub-ranges that aren't already ON, preserving rich - // attrs (e.g. wavy/colored underline) on nodes that already have them. - const subRanges = collectNonOnSubRanges(tr.doc, absFrom, absTo, markType, spec); - for (const r of subRanges) { - tr.removeMark(r.from, r.to, markType); - tr.addMark(r.from, r.to, spec.createOn(markType) as unknown as ProseMirrorMark); - } - break; - } - case 'off': - // Remove any existing, then add canonical OFF - tr.removeMark(absFrom, absTo, markType); - tr.addMark(absFrom, absTo, markType.create(spec.offAttrs)); - break; - case 'clear': - // Remove mark entirely - tr.removeMark(absFrom, absTo, markType); - break; - } - - return (tr.steps?.length ?? 0) > stepsBefore; -} - export function executeStyleApply( editor: Editor, tr: Transaction, @@ -321,11 +730,7 @@ export function executeStyleApply( ): { changed: boolean } { const absFrom = mapping.map(target.absFrom); const absTo = mapping.map(target.absTo); - - // Collapsed-range rule: silent no-op — no error, no document change. - if (absFrom === absTo) return { changed: false }; - - return { changed: applyInlineMarkPatches(editor, tr, absFrom, absTo, step.args.inline) }; + return { changed: applyInlinePatchToRange(editor, tr, absFrom, absTo, step.args.inline) }; } // --------------------------------------------------------------------------- @@ -462,15 +867,14 @@ export function executeSpanStyleApply( mapping: Mapping, ): { changed: boolean } { validateMappedSpanContiguity(target, mapping, step.id); + + // Apply marks uniformly across the full span const firstSeg = target.segments[0]; const lastSeg = target.segments[target.segments.length - 1]; const absFrom = mapping.map(firstSeg.absFrom, 1); const absTo = mapping.map(lastSeg.absTo, -1); - // Collapsed-range rule: silent no-op — no error, no document change. - if (absFrom === absTo) return { changed: false }; - - return { changed: applyInlineMarkPatches(editor, tr, absFrom, absTo, step.args.inline) }; + return { changed: applyInlinePatchToRange(editor, tr, absFrom, absTo, step.args.inline) }; } // --------------------------------------------------------------------------- diff --git a/packages/super-editor/src/document-api-adapters/plan-engine/format-value-wrappers.ts b/packages/super-editor/src/document-api-adapters/plan-engine/format-value-wrappers.ts deleted file mode 100644 index 7ddfe93961..0000000000 --- a/packages/super-editor/src/document-api-adapters/plan-engine/format-value-wrappers.ts +++ /dev/null @@ -1,224 +0,0 @@ -/** - * Wrappers for value-based format operations: fontSize, fontFamily, color, align. - * - * fontSize, fontFamily, and color are inline text-style marks applied via `setMark('textStyle', ...)`. - * align is a paragraph-level attribute applied via `updateAttributes('paragraph', ...)`. - * - * All four are direct-only in v1 (tracked mode rejected with CAPABILITY_UNAVAILABLE). - * They route through `executeDomainCommand` — no plan-engine step executors are registered. - */ - -import type { - FormatFontSizeInput, - FormatFontFamilyInput, - FormatColorInput, - FormatAlignInput, - MutationOptions, - TextAddress, - TextMutationReceipt, -} from '@superdoc/document-api'; -import type { Editor } from '../../core/Editor.js'; -import { DocumentApiAdapterError } from '../errors.js'; -import { resolveTextTarget } from '../helpers/adapter-utils.js'; -import { buildTextMutationResolution, readTextAtResolvedRange } from '../helpers/text-mutation-resolution.js'; -import { requireEditorCommand, requireSchemaMark, rejectTrackedMode } from '../helpers/mutation-helpers.js'; -import { executeDomainCommand } from './plan-wrappers.js'; - -// --------------------------------------------------------------------------- -// Shared: resolve target and build resolution -// --------------------------------------------------------------------------- - -interface ResolvedFormat { - target: TextAddress; - from: number; - to: number; - resolution: ReturnType; -} - -function resolveFormatTarget(editor: Editor, target: TextAddress, operation: string): ResolvedFormat { - const range = resolveTextTarget(editor, target); - if (!range) { - throw new DocumentApiAdapterError('TARGET_NOT_FOUND', `${operation} target could not be resolved.`, { target }); - } - const text = readTextAtResolvedRange(editor, range); - const resolution = buildTextMutationResolution({ requestedTarget: target, target, range, text }); - return { target, from: range.from, to: range.to, resolution }; -} - -function collapsedTargetFailure(resolution: ResolvedFormat['resolution'], operation: string): TextMutationReceipt { - return { - success: false, - resolution, - failure: { code: 'INVALID_TARGET', message: `${operation} requires a non-collapsed target range.` }, - }; -} - -function noOpFailure(resolution: ResolvedFormat['resolution'], operation: string): TextMutationReceipt { - return { - success: false, - resolution, - failure: { code: 'NO_OP', message: `${operation} produced no change.` }, - }; -} - -// --------------------------------------------------------------------------- -// Shared: inline value format wrapper (fontSize, fontFamily, color) -// --------------------------------------------------------------------------- - -interface InlineFormatConfig { - operation: string; - setCommand: string; - unsetCommand: string; -} - -function inlineValueFormatWrapper( - editor: Editor, - target: TextAddress, - value: string | number | null, - options: MutationOptions | undefined, - config: InlineFormatConfig, -): TextMutationReceipt { - rejectTrackedMode(config.operation, options); - - const resolved = resolveFormatTarget(editor, target, config.operation); - if (resolved.from === resolved.to) { - return collapsedTargetFailure(resolved.resolution, config.operation); - } - - requireSchemaMark(editor, 'textStyle', config.operation); - - const setTextSelection = requireEditorCommand( - editor.commands?.setTextSelection as ((range: { from: number; to: number }) => boolean) | undefined, - `${config.operation} (setTextSelection)`, - ); - - const activeCommand = value !== null ? config.setCommand : config.unsetCommand; - requireEditorCommand( - (editor.commands as Record)?.[activeCommand] as ((...args: unknown[]) => boolean) | undefined, - `${config.operation} (${activeCommand})`, - ); - - if (options?.dryRun) { - return { success: true, resolution: resolved.resolution }; - } - - const receipt = executeDomainCommand( - editor, - () => { - setTextSelection({ from: resolved.from, to: resolved.to }); - const cmd = (editor.commands as Record boolean>)[activeCommand]; - return value !== null ? cmd(value) : cmd(); - }, - { expectedRevision: options?.expectedRevision }, - ); - - if (receipt.steps[0]?.effect !== 'changed') { - return noOpFailure(resolved.resolution, config.operation); - } - - return { success: true, resolution: resolved.resolution }; -} - -// --------------------------------------------------------------------------- -// format.fontSize -// --------------------------------------------------------------------------- - -export function formatFontSizeWrapper( - editor: Editor, - input: FormatFontSizeInput, - options?: MutationOptions, -): TextMutationReceipt { - return inlineValueFormatWrapper(editor, input.target, input.value, options, { - operation: 'format.fontSize', - setCommand: 'setFontSize', - unsetCommand: 'unsetFontSize', - }); -} - -// --------------------------------------------------------------------------- -// format.fontFamily -// --------------------------------------------------------------------------- - -export function formatFontFamilyWrapper( - editor: Editor, - input: FormatFontFamilyInput, - options?: MutationOptions, -): TextMutationReceipt { - return inlineValueFormatWrapper(editor, input.target, input.value, options, { - operation: 'format.fontFamily', - setCommand: 'setFontFamily', - unsetCommand: 'unsetFontFamily', - }); -} - -// --------------------------------------------------------------------------- -// format.color -// --------------------------------------------------------------------------- - -export function formatColorWrapper( - editor: Editor, - input: FormatColorInput, - options?: MutationOptions, -): TextMutationReceipt { - return inlineValueFormatWrapper(editor, input.target, input.value, options, { - operation: 'format.color', - setCommand: 'setColor', - unsetCommand: 'unsetColor', - }); -} - -// --------------------------------------------------------------------------- -// format.align (paragraph-level — different execution path) -// --------------------------------------------------------------------------- - -export function formatAlignWrapper( - editor: Editor, - input: FormatAlignInput, - options?: MutationOptions, -): TextMutationReceipt { - const operation = 'format.align'; - rejectTrackedMode(operation, options); - - const resolved = resolveFormatTarget(editor, input.target, operation); - // Align allows collapsed targets — a cursor identifies the containing paragraph. - - const setTextSelection = requireEditorCommand( - editor.commands?.setTextSelection as ((range: { from: number; to: number }) => boolean) | undefined, - `${operation} (setTextSelection)`, - ); - - if (input.alignment !== null) { - requireEditorCommand( - editor.commands?.setTextAlign as ((alignment: string) => boolean) | undefined, - `${operation} (setTextAlign)`, - ); - } else { - requireEditorCommand( - editor.commands?.unsetTextAlign as (() => boolean) | undefined, - `${operation} (unsetTextAlign)`, - ); - } - - if (options?.dryRun) { - return { success: true, resolution: resolved.resolution }; - } - - const receipt = executeDomainCommand( - editor, - () => { - setTextSelection({ from: resolved.from, to: resolved.to }); - - if (input.alignment !== null) { - return (editor.commands as Record boolean>).setTextAlign(input.alignment); - } - return (editor.commands as Record boolean>).unsetTextAlign(); - }, - { expectedRevision: options?.expectedRevision }, - ); - - if (receipt.steps[0]?.effect !== 'changed') { - return noOpFailure(resolved.resolution, operation); - } - - return { success: true, resolution: resolved.resolution }; -} diff --git a/packages/super-editor/src/document-api-adapters/plan-engine/plan-wrappers.ts b/packages/super-editor/src/document-api-adapters/plan-engine/plan-wrappers.ts index 2b07ad565f..10dca56b71 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,10 +16,12 @@ import type { TextMutationResolution, WriteRequest, StyleApplyInput, - SetMarks, + FormatAlignInput, + InlineRunPatchKey, PlanReceipt, ReceiptFailure, } from '@superdoc/document-api'; +import { INLINE_PROPERTY_BY_KEY } from '@superdoc/document-api'; import type { Editor } from '../../core/Editor.js'; import type { CompiledPlan } from './compiler.js'; import type { CompiledTarget } from './executor-registry.types.js'; @@ -28,7 +30,12 @@ import { getRevision } from './revision-tracker.js'; import { DocumentApiAdapterError } from '../errors.js'; import { resolveDefaultInsertTarget, resolveTextTarget, type ResolvedTextTarget } from '../helpers/adapter-utils.js'; import { buildTextMutationResolution, readTextAtResolvedRange } from '../helpers/text-mutation-resolution.js'; -import { ensureTrackedCapability, requireSchemaMark } from '../helpers/mutation-helpers.js'; +import { + ensureTrackedCapability, + requireEditorCommand, + requireSchemaMark, + rejectTrackedMode, +} from '../helpers/mutation-helpers.js'; import { TrackFormatMarkName } from '../../extensions/track-changes/constants.js'; import { markdownToPmFragment } from '../../core/helpers/markdown/markdownToPmContent.js'; import { processContent } from '../../core/helpers/contentProcessor.js'; @@ -361,58 +368,103 @@ export function writeWrapper(editor: Editor, request: WriteRequest, options?: Mu // Canonical format.apply wrapper (multi-style inline patch semantics) // --------------------------------------------------------------------------- -/** Map from mark key to editor schema mark name. */ -const MARK_KEY_TO_SCHEMA_NAME: Record = { - bold: 'bold', - italic: 'italic', - underline: 'underline', - strike: 'strike', -}; +interface ResolvedFormatTarget { + target: TextAddress; + range: ResolvedTextTarget; + resolution: TextMutationResolution; +} -export function styleApplyWrapper( - editor: Editor, - input: StyleApplyInput, - options?: MutationOptions, -): TextMutationReceipt { - const normalizedInput = normalizeFormatLocator(input); - const range = resolveTextTarget(editor, normalizedInput.target!); +function resolveFormatTarget(editor: Editor, target: TextAddress, operation: string): ResolvedFormatTarget { + const range = resolveTextTarget(editor, target); if (!range) { - throw new DocumentApiAdapterError('TARGET_NOT_FOUND', 'format.apply target could not be resolved.', { - target: normalizedInput.target, - }); + throw new DocumentApiAdapterError('TARGET_NOT_FOUND', `${operation} target could not be resolved.`, { target }); } - const resolution = buildTextMutationResolution({ - requestedTarget: input.target, - target: normalizedInput.target!, + requestedTarget: target, + target, range, text: readTextAtResolvedRange(editor, range), }); + return { target, range, resolution }; +} + +function noOpFailure(resolution: TextMutationResolution, operation: string): TextMutationReceipt { + return { + success: false, + resolution, + failure: { code: 'NO_OP', message: `${operation} produced no change.` }, + }; +} + +function ensureInlinePropertyCapabilities(editor: Editor, keys: readonly InlineRunPatchKey[]): void { + let requiresTextStyle = false; + let requiresRunNode = false; + + for (const key of keys) { + const entry = INLINE_PROPERTY_BY_KEY[key]; + if (!entry) continue; + + if (entry.storage === 'mark') { + const carrier = entry.carrier; + if (carrier.storage !== 'mark') continue; + if (carrier.markName === 'textStyle') { + requiresTextStyle = true; + continue; + } + requireSchemaMark(editor, carrier.markName, 'format.apply'); + continue; + } + + requiresRunNode = true; + } + + if (requiresTextStyle) { + requireSchemaMark(editor, 'textStyle', 'format.apply'); + } + + if (requiresRunNode && !editor.state.schema.nodes.run) { + throw new DocumentApiAdapterError('CAPABILITY_UNAVAILABLE', 'format.apply requires a run node in the schema.'); + } +} + +function ensureTrackedInlinePropertySupport(keys: readonly InlineRunPatchKey[]): void { + const unsupportedTrackedKeys = keys.filter((key) => INLINE_PROPERTY_BY_KEY[key]?.tracked === false); + if (unsupportedTrackedKeys.length === 0) return; + + throw new DocumentApiAdapterError( + 'CAPABILITY_UNAVAILABLE', + `format.apply tracked mode is not available for: ${unsupportedTrackedKeys.join(', ')}`, + { keys: unsupportedTrackedKeys, changeMode: 'tracked' }, + ); +} + +export function styleApplyWrapper( + editor: Editor, + input: StyleApplyInput, + options?: MutationOptions, +): TextMutationReceipt { + const normalizedInput = normalizeFormatLocator(input); + const resolved = resolveFormatTarget(editor, normalizedInput.target!, 'format.apply'); - if (range.from === range.to) { + if (resolved.range.from === resolved.range.to) { return { success: false, - resolution, + resolution: resolved.resolution, failure: { code: 'INVALID_TARGET', message: 'format.apply requires a non-collapsed target range.' }, }; } - // Validate that at least one requested inline style exists in the schema - const markKeys = Object.keys(input.inline).filter((k) => input.inline[k as keyof SetMarks] !== undefined); - for (const key of markKeys) { - const schemaName = MARK_KEY_TO_SCHEMA_NAME[key]; - if (schemaName) { - requireSchemaMark(editor, schemaName, 'format.apply'); - } - } + const inlineKeys = Object.keys(input.inline) as InlineRunPatchKey[]; + ensureInlinePropertyCapabilities(editor, inlineKeys); const mode = options?.changeMode ?? 'direct'; if (mode === 'tracked') { + ensureTrackedInlinePropertySupport(inlineKeys); ensureTrackedCapability(editor, { operation: 'format.apply', requireMarks: [TrackFormatMarkName] }); } if (options?.dryRun) { - return { success: true, resolution }; + return { success: true, resolution: resolved.resolution }; } // Build single-step compiled plan using the full inline payload @@ -431,9 +483,9 @@ export function styleApplyWrapper( blockId: normalizedInput.target!.blockId, from: normalizedInput.target!.range.start, to: normalizedInput.target!.range.end, - absFrom: range.from, - absTo: range.to, - text: resolution.text, + absFrom: resolved.range.from, + absTo: resolved.range.to, + text: resolved.resolution.text, marks: [], }; @@ -448,7 +500,58 @@ export function styleApplyWrapper( expectedRevision: options?.expectedRevision, }); - return mapPlanReceiptToTextReceipt(receipt, resolution); + 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 }; } // --------------------------------------------------------------------------- diff --git a/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.js b/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.js index 421ebdd65b..75513db8a7 100644 --- a/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.js +++ b/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.js @@ -24,6 +24,8 @@ const RUN_PROPERTIES_DERIVED_FROM_MARKS = new Set([ 'position', ]); +const RUN_PROPERTY_PRESERVE_META_KEY = 'sdPreserveRunPropertiesKeys'; + /** * ProseMirror plugin that recalculates inline `runProperties` for changed runs, * keeping run attributes aligned with decoded mark styles and resolved paragraph styles. @@ -48,6 +50,17 @@ export const calculateInlineRunPropertiesPlugin = (editor) => const runType = newState.schema.nodes.run; if (!runType) return null; + const preservedDerivedKeys = new Set(); + transactions.forEach((transaction) => { + const keys = transaction.getMeta(RUN_PROPERTY_PRESERVE_META_KEY); + if (!Array.isArray(keys)) return; + keys.forEach((key) => { + if (typeof key === 'string' && key.length > 0) { + preservedDerivedKeys.add(key); + } + }); + }); + // Find all runs affected by changes, regardless of step type const changedRanges = collectChangedRangesThroughTransactions(transactions, newState.doc.content.size); @@ -74,7 +87,14 @@ export const calculateInlineRunPropertiesPlugin = (editor) => const { paragraphNode, paragraphPos, tableInfo } = getRunContext($pos); if (!paragraphNode || paragraphPos === undefined) return; - const { segments, firstInlineProps } = segmentRunByInlineProps(runNode, paragraphNode, tableInfo, $pos, editor); + const { segments, firstInlineProps } = segmentRunByInlineProps( + runNode, + paragraphNode, + tableInfo, + $pos, + editor, + preservedDerivedKeys, + ); const runProperties = firstInlineProps ?? null; let firstRunPos = firstRunPosByParagraph.get(paragraphPos); @@ -239,7 +259,7 @@ function findFirstRunPosInParagraph(paragraphNode, paragraphPos, runType) { * @param {object} editor * @returns {{ segments: Array<{ inlineProps: Record|null, inlineKey: string, content: import('prosemirror-model').Node[] }>, firstInlineProps: Record|null }} */ -function segmentRunByInlineProps(runNode, paragraphNode, tableInfo, $pos, editor) { +function segmentRunByInlineProps(runNode, paragraphNode, tableInfo, $pos, editor, preservedDerivedKeys) { const segments = []; let lastKey = null; let boundaryCounter = 0; @@ -253,6 +273,7 @@ function segmentRunByInlineProps(runNode, paragraphNode, tableInfo, $pos, editor tableInfo, $pos, editor, + preservedDerivedKeys, ); const last = segments[segments.length - 1]; if (last && inlineKey === lastKey) { @@ -291,7 +312,15 @@ function segmentRunByInlineProps(runNode, paragraphNode, tableInfo, $pos, editor * @param {object} editor * @returns {{ inlineProps: Record|null, inlineKey: string }} */ -function computeInlineRunProps(marks, existingRunProperties, paragraphNode, tableInfo, $pos, editor) { +function computeInlineRunProps( + marks, + existingRunProperties, + paragraphNode, + tableInfo, + $pos, + editor, + preservedDerivedKeys, +) { const runPropertiesFromMarks = decodeRPrFromMarks(marks); const paragraphProperties = getResolvedParagraphProperties(paragraphNode) || calculateResolvedParagraphProperties(editor, paragraphNode, $pos); @@ -311,6 +340,7 @@ function computeInlineRunProps(marks, existingRunProperties, paragraphNode, tabl runPropertiesFromStyles, existingRunProperties, editor, + preservedDerivedKeys, ); const inlineProps = Object.keys(inlineRunProperties).length ? inlineRunProperties : null; const inlineKey = stableStringifyInlineProps(inlineProps); @@ -326,9 +356,16 @@ function computeInlineRunProps(marks, existingRunProperties, paragraphNode, tabl * @param {object} editor Editor instance used to normalize mark-level font-family comparisons. * @returns {Record} Inline run properties that override styled defaults. */ -function getInlineRunProperties(runPropertiesFromMarks, runPropertiesFromStyles, existingRunProperties, editor) { +function getInlineRunProperties( + runPropertiesFromMarks, + runPropertiesFromStyles, + existingRunProperties, + editor, + preservedDerivedKeys = new Set(), +) { const inlineRunProperties = {}; for (const key in runPropertiesFromMarks) { + if (preservedDerivedKeys.has(key)) continue; const valueFromMarks = runPropertiesFromMarks[key]; const valueFromStyles = runPropertiesFromStyles[key]; if (JSON.stringify(valueFromMarks) !== JSON.stringify(valueFromStyles)) { @@ -346,7 +383,7 @@ function getInlineRunProperties(runPropertiesFromMarks, runPropertiesFromStyles, if (existingRunProperties != null) { Object.keys(existingRunProperties).forEach((key) => { - if (RUN_PROPERTIES_DERIVED_FROM_MARKS.has(key)) return; + if (RUN_PROPERTIES_DERIVED_FROM_MARKS.has(key) && !preservedDerivedKeys.has(key)) return; if (key in inlineRunProperties) return; if (existingRunProperties[key] === undefined) return; inlineRunProperties[key] = existingRunProperties[key]; diff --git a/packages/super-editor/src/extensions/types/mark-attributes.ts b/packages/super-editor/src/extensions/types/mark-attributes.ts index a9409d3fa6..b1b4f88151 100644 --- a/packages/super-editor/src/extensions/types/mark-attributes.ts +++ b/packages/super-editor/src/extensions/types/mark-attributes.ts @@ -58,6 +58,12 @@ export interface UnderlineAttrs { underlineType?: UnderlineStyle | null; /** Underline color (hex, 'auto', etc.) */ underlineColor?: string | null; + /** Underline theme color token (e.g., 'accent1') */ + underlineThemeColor?: string | null; + /** Underline theme tint value */ + underlineThemeTint?: string | null; + /** Underline theme shade value */ + underlineThemeShade?: string | null; } // ============================================ diff --git a/packages/super-editor/src/extensions/underline/underline.js b/packages/super-editor/src/extensions/underline/underline.js index 23651fecca..44da638824 100644 --- a/packages/super-editor/src/extensions/underline/underline.js +++ b/packages/super-editor/src/extensions/underline/underline.js @@ -74,6 +74,15 @@ export const Underline = Mark.create({ underlineColor: { default: null, }, + underlineThemeColor: { + default: null, + }, + underlineThemeTint: { + default: null, + }, + underlineThemeShade: { + default: null, + }, }; }, diff --git a/tests/doc-api-stories/tests/formatting/inline-formatting.ts b/tests/doc-api-stories/tests/formatting/inline-formatting.ts index bd6d95103e..1bc28c9cf9 100644 --- a/tests/doc-api-stories/tests/formatting/inline-formatting.ts +++ b/tests/doc-api-stories/tests/formatting/inline-formatting.ts @@ -1,469 +1,243 @@ import { describe, expect, it } from 'vitest'; -import { corpusDoc, unwrap, useStoryHarness } from '../harness'; - -type InlineDirective = 'on' | 'off' | 'clear'; - -type InlinePatch = { - bold?: InlineDirective; - italic?: InlineDirective; - underline?: InlineDirective; - strike?: InlineDirective; -}; - -type TextTarget = { - kind: 'text'; - blockId: string; - range: { start: number; end: number }; -}; - -type RunStyles = { - direct: { - bold: InlineDirective; - italic: InlineDirective; - underline: InlineDirective; - strike: InlineDirective; - }; - effective: { - bold: boolean; - italic: boolean; - underline: boolean; - strike: boolean; - }; -}; - -const SOURCE_FIXTURE = 'basic/first-arial.docx'; - -function sid(label: string): string { - return `${label}-${Date.now()}-${Math.floor(Math.random() * 1_000_000)}`; -} - -function mutationSuccess(payload: any): boolean { - return payload?.receipt?.success ?? payload?.success ?? false; -} - -function assertMutationSuccess(payload: any): void { - expect(mutationSuccess(payload)).toBe(true); -} - -function buildTextTarget(blockId: string, text: string): TextTarget { - return { - kind: 'text', - blockId, - range: { start: 0, end: text.length }, - }; -} - +import { unwrap, useStoryHarness } from '../harness'; + +/** + * End-to-end story tests for all inline formatting operations. + * + * Each test opens a blank document, inserts descriptive text, then applies + * the corresponding format operation. Starting from a blank doc proves the + * full pipeline: SDK → CLI → document-api → adapter → editor, without + * depending on any pre-existing corpus document. + * + * The blank DOCX template contains a single empty paragraph with a stable + * `w14:paraId` attribute that survives DOCX export/reimport cycles, so the + * blockId returned by `insert` remains valid for subsequent operations. + * + * Covered operations: + * format.apply — boolean marks and value/object inline run patches + * format.align — paragraph-level alignment (center, right, justify) + */ describe('document-api story: inline formatting', () => { - const { client, copyDoc, outPath, runCli } = useStoryHarness('formatting/inline-formatting', { + const { client, outPath } = useStoryHarness('formatting/inline-formatting', { preserveResults: true, }); - async function saveResult(sessionId: string, docName: string): Promise { - await client.doc.save({ sessionId, out: outPath(docName) }); - } + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- - async function seedBlankFormattableRange( - sessionId: string, - sourceDocName: string, - text: string, - ): Promise<{ text: string; pattern: string; target: TextTarget }> { + /** + * Opens a blank doc, inserts the given descriptive text, and returns a + * target spanning the full inserted range. + * + * Each test gets its own session (and thus its own working doc on disk). + */ + async function setupFormattableText(sessionId: string, text: string) { + // Open a blank document (no doc path → uses built-in blank DOCX template) await client.doc.open({ sessionId }); + // Insert text into the blank doc's single paragraph. + // Without an explicit target, insert uses the first paragraph. const insertResult = unwrap(await client.doc.insert({ sessionId, value: text })); - assertMutationSuccess(insertResult); + expect(insertResult.receipt?.success).toBe(true); - const blockId = insertResult?.target?.blockId as string | undefined; - expect(typeof blockId).toBe('string'); - if (!blockId) { - throw new Error('insert did not return target.blockId'); - } - - await saveResult(sessionId, sourceDocName); + // The receipt's hoisted target contains the paragraph's stable blockId. + const blockId = insertResult.target?.blockId; + if (!blockId) throw new Error('Insert did not return a target blockId.'); + // Build a target spanning the full inserted text return { - text, - pattern: text, - target: buildTextTarget(blockId, text), + kind: 'text' as const, + blockId, + range: { start: 0, end: text.length }, }; } - async function openFixtureDoc(sessionId: string, sourceDocName: string): Promise { - const sourceDoc = await copyDoc(corpusDoc(SOURCE_FIXTURE), sourceDocName); - await client.doc.open({ doc: sourceDoc, sessionId }); - } - - async function firstLinePattern(sessionId: string): Promise { - const textResult = unwrap(await client.doc.getText({ sessionId })); - const firstLine = String(textResult?.text ?? '').split('\n')[0] ?? ''; - expect(firstLine.length).toBeGreaterThan(1); - return firstLine; + /** 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) }); } - async function queryFirstTextMatch(sessionId: string, pattern: string): Promise { - const match = unwrap( - await client.doc.query.match({ - sessionId, - select: { type: 'text', pattern, caseSensitive: true }, - require: 'first', - }), - ); - - expect(Array.isArray(match?.items)).toBe(true); - expect(match.items.length).toBeGreaterThan(0); - expect(match.items[0]?.matchKind).toBe('text'); - return match; - } + // --------------------------------------------------------------------------- + // format.apply — boolean mark patches + // --------------------------------------------------------------------------- - async function findFirstTextTarget(sessionId: string, pattern: string): Promise { - const findResult = unwrap( - await client.doc.find({ - sessionId, - type: 'text', - pattern, - require: 'first', - }), - ); + it('bold: applies bold to inserted text', async () => { + const sid = `bold-${Date.now()}`; + const target = await setupFormattableText(sid, 'This text should be bold'); - const target = findResult?.items?.[0]?.context?.textRanges?.[0] as TextTarget | undefined; - expect(target?.kind).toBe('text'); - expect(typeof target?.blockId).toBe('string'); - return target as TextTarget; - } + const result = unwrap(await client.doc.format.apply({ sessionId: sid, target, inline: { bold: true } })); + expect(result.receipt?.success).toBe(true); + await saveResult(sid, 'bold.docx'); + }); - function firstRunStyles(match: any): RunStyles { - const styles = match?.items?.[0]?.blocks?.[0]?.runs?.[0]?.styles; - expect(styles).toBeDefined(); - expect(styles.direct).toBeDefined(); - expect(styles.effective).toBeDefined(); - return styles as RunStyles; - } + it('italic: applies italic to inserted text', async () => { + const sid = `italic-${Date.now()}`; + const target = await setupFormattableText(sid, 'This text should be italic'); - async function applyInline(sessionId: string, target: TextTarget, inline: InlinePatch): Promise { - const result = unwrap(await client.doc.format.apply({ sessionId, target, inline })); - assertMutationSuccess(result); - return result; - } + const result = unwrap(await client.doc.format.apply({ sessionId: sid, target, inline: { italic: true } })); + expect(result.receipt?.success).toBe(true); + await saveResult(sid, 'italic.docx'); + }); - async function applyRunDocDefaultsPatch( - sourceDoc: string, - patch: Record, - outDoc: string, - ): Promise { - const envelope = await runCli([ - 'styles', - 'apply', - sourceDoc, - '--target-json', - JSON.stringify({ scope: 'docDefaults', channel: 'run' }), - '--patch-json', - JSON.stringify(patch), - '--out', - outDoc, - ]); - - const payload = envelope?.data ?? envelope; - const receipt = payload?.receipt ?? payload; - expect(receipt).toBeDefined(); - expect(receipt.success).toBe(true); - return receipt; - } + it('underline: applies underline to inserted text', async () => { + const sid = `underline-${Date.now()}`; + const target = await setupFormattableText(sid, 'This text should be underlined'); - it('bold on: applies bold to inserted text', async () => { - const sessionId = sid('bold-on'); - const { target } = await seedBlankFormattableRange(sessionId, 'bold-on-source.docx', 'This text should be bold.'); - await applyInline(sessionId, target, { bold: 'on' }); - await saveResult(sessionId, 'bold-on.docx'); + const result = unwrap(await client.doc.format.apply({ sessionId: sid, target, inline: { underline: true } })); + expect(result.receipt?.success).toBe(true); + await saveResult(sid, 'underline.docx'); }); - it('italic on: applies italic to inserted text', async () => { - const sessionId = sid('italic-on'); - const { target } = await seedBlankFormattableRange( - sessionId, - 'italic-on-source.docx', - 'This text should be italic.', - ); - await applyInline(sessionId, target, { italic: 'on' }); - await saveResult(sessionId, 'italic-on.docx'); - }); + it('strikethrough: applies strike to inserted text', async () => { + const sid = `strike-${Date.now()}`; + const target = await setupFormattableText(sid, 'This text should be struck through'); - it('underline on: applies underline to inserted text', async () => { - const sessionId = sid('underline-on'); - const { target } = await seedBlankFormattableRange( - sessionId, - 'underline-on-source.docx', - 'This text should be underlined.', - ); - await applyInline(sessionId, target, { underline: 'on' }); - await saveResult(sessionId, 'underline-on.docx'); + const result = unwrap(await client.doc.format.apply({ sessionId: sid, target, inline: { strike: true } })); + expect(result.receipt?.success).toBe(true); + await saveResult(sid, 'strike.docx'); }); - it('strike on: applies strike to inserted text', async () => { - const sessionId = sid('strike-on'); - const { target } = await seedBlankFormattableRange( - sessionId, - 'strike-on-source.docx', - 'This text should be struck through.', - ); - await applyInline(sessionId, target, { strike: 'on' }); - await saveResult(sessionId, 'strike-on.docx'); - }); + it('multi-mark: applies bold + italic in a single call', async () => { + const sid = `multi-${Date.now()}`; + const target = await setupFormattableText(sid, 'This text should be bold and italic'); - it('multi-mark on: applies bold + italic in one call', async () => { - const sessionId = sid('multi-mark-on'); - const { target } = await seedBlankFormattableRange( - sessionId, - 'multi-mark-on-source.docx', - 'This text should be bold and italic.', + const result = unwrap( + await client.doc.format.apply({ + sessionId: sid, + target, + inline: { bold: true, italic: true }, + }), ); - await applyInline(sessionId, target, { bold: 'on', italic: 'on' }); - await saveResult(sessionId, 'multi-mark-on.docx'); + expect(result.receipt?.success).toBe(true); + await saveResult(sid, 'multi-mark.docx'); }); - it('fontSize numeric: sets point size', async () => { - const sessionId = sid('font-size-num'); - const { target } = await seedBlankFormattableRange( - sessionId, - 'font-size-num-source.docx', - 'This text should be 24pt.', - ); + // --------------------------------------------------------------------------- + // format.apply — value/object inline patches + // --------------------------------------------------------------------------- - const result = unwrap(await client.doc.format.fontSize({ sessionId, target, value: 24 })); - assertMutationSuccess(result); + it('fontSize: sets a numeric point size', async () => { + const sid = `fontSize-num-${Date.now()}`; + const target = await setupFormattableText(sid, 'This text should be 24pt'); - await saveResult(sessionId, 'font-size-num.docx'); + const result = unwrap(await client.doc.format.apply({ sessionId: sid, target, inline: { fontSize: 24 } })); + expect(result.receipt?.success).toBe(true); + await saveResult(sid, 'fontSize-num.docx'); }); - it('fontSize string: sets point size from string', async () => { - const sessionId = sid('font-size-str'); - const { target } = await seedBlankFormattableRange( - sessionId, - 'font-size-str-source.docx', - 'This text should be 14pt.', - ); + it('fontSize: clears an existing point size', async () => { + const sid = `fontSize-clear-${Date.now()}`; + const target = await setupFormattableText(sid, 'This text should reset font size'); - const result = unwrap(await client.doc.format.fontSize({ sessionId, target, value: '14pt' })); - assertMutationSuccess(result); + const setResult = unwrap(await client.doc.format.apply({ sessionId: sid, target, inline: { fontSize: 14 } })); + expect(setResult.receipt?.success).toBe(true); - await saveResult(sessionId, 'font-size-str.docx'); + const result = unwrap(await client.doc.format.apply({ sessionId: sid, target, inline: { fontSize: null } })); + expect(result.receipt?.success).toBe(true); + await saveResult(sid, 'fontSize-clear.docx'); }); - it('fontFamily: sets font family', async () => { - const sessionId = sid('font-family'); - const { target } = await seedBlankFormattableRange( - sessionId, - 'font-family-source.docx', - 'This text should be Courier New.', - ); - - const result = unwrap(await client.doc.format.fontFamily({ sessionId, target, value: 'Courier New' })); - assertMutationSuccess(result); + it('rFonts: sets run font family attributes', async () => { + const sid = `fontFamily-${Date.now()}`; + const target = await setupFormattableText(sid, 'This text should be Courier New'); - await saveResult(sessionId, 'font-family.docx'); + const result = unwrap( + await client.doc.format.apply({ + sessionId: sid, + target, + inline: { + rFonts: { ascii: 'Courier New', hAnsi: 'Courier New' }, + }, + }), + ); + expect(result.receipt?.success).toBe(true); + await saveResult(sid, 'fontFamily.docx'); }); - it('color: sets text color', async () => { - const sessionId = sid('color'); - const { target } = await seedBlankFormattableRange(sessionId, 'color-source.docx', 'This text should be red.'); - - const result = unwrap(await client.doc.format.color({ sessionId, target, value: '#FF0000' })); - assertMutationSuccess(result); + it('color: sets a hex color', async () => { + const sid = `color-${Date.now()}`; + const target = await setupFormattableText(sid, 'This text should be red'); - await saveResult(sessionId, 'color.docx'); + const result = unwrap(await client.doc.format.apply({ sessionId: sid, target, inline: { color: '#FF0000' } })); + expect(result.receipt?.success).toBe(true); + await saveResult(sid, 'color.docx'); }); - it('align center: centers paragraph', async () => { - const sessionId = sid('align-center'); - const { target } = await seedBlankFormattableRange( - sessionId, - 'align-center-source.docx', - 'This paragraph should be centered.', - ); + // --------------------------------------------------------------------------- + // format.align (paragraph-level) + // --------------------------------------------------------------------------- - const result = unwrap(await client.doc.format.align({ sessionId, target, alignment: 'center' })); - assertMutationSuccess(result); + it('align center: centers the paragraph', async () => { + const sid = `align-center-${Date.now()}`; + const target = await setupFormattableText(sid, 'This paragraph should be centered'); - await saveResult(sessionId, 'align-center.docx'); + const result = unwrap(await client.doc.format.align({ sessionId: sid, target, alignment: 'center' })); + expect(result.receipt?.success).toBe(true); + await saveResult(sid, 'align-center.docx'); }); - it('align right: right-aligns paragraph', async () => { - const sessionId = sid('align-right'); - const { target } = await seedBlankFormattableRange( - sessionId, - 'align-right-source.docx', - 'This paragraph should be right aligned.', - ); - - const result = unwrap(await client.doc.format.align({ sessionId, target, alignment: 'right' })); - assertMutationSuccess(result); + 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'); - await saveResult(sessionId, 'align-right.docx'); + const result = unwrap(await client.doc.format.align({ sessionId: sid, target, alignment: 'right' })); + expect(result.receipt?.success).toBe(true); + await saveResult(sid, 'align-right.docx'); }); - it('align justify: justifies paragraph', async () => { - const sessionId = sid('align-justify'); - const { target } = await seedBlankFormattableRange( - sessionId, - 'align-justify-source.docx', - 'This paragraph should be fully justified across multiple wrapped lines so the alignment difference is visually obvious in exported output.', + it('align justify: justifies the paragraph', async () => { + const sid = `align-justify-${Date.now()}`; + const target = await setupFormattableText( + 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, target, alignment: 'justify' })); - assertMutationSuccess(result); - - await saveResult(sessionId, 'align-justify.docx'); + const result = unwrap(await client.doc.format.align({ sessionId: sid, target, alignment: 'justify' })); + expect(result.receipt?.success).toBe(true); + await saveResult(sid, 'align-justify.docx'); }); - it('combined value formats: fontSize + fontFamily + color on same range', async () => { - const sessionId = sid('combined-values'); - const { target } = await seedBlankFormattableRange( - sessionId, - 'combined-values-source.docx', - 'This text should be 18pt Georgia in blue.', - ); - - const sizeResult = unwrap(await client.doc.format.fontSize({ sessionId, target, value: 18 })); - assertMutationSuccess(sizeResult); - - const familyResult = unwrap(await client.doc.format.fontFamily({ sessionId, target, value: 'Georgia' })); - assertMutationSuccess(familyResult); + // --------------------------------------------------------------------------- + // Combined: multiple inline run patches on the same range + // --------------------------------------------------------------------------- - const colorResult = unwrap(await client.doc.format.color({ sessionId, target, value: '#0000FF' })); - assertMutationSuccess(colorResult); - - await saveResult(sessionId, 'combined-values.docx'); - }); - - it('dryRun format.apply: reports success without mutating', async () => { - const sessionId = sid('dry-run'); - const { target } = await seedBlankFormattableRange( - sessionId, - 'dry-run-source.docx', - 'This text should remain unchanged.', - ); + it('combined: fontSize + rFonts + color on the same text', async () => { + const sid = `combined-${Date.now()}`; + const target = await setupFormattableText(sid, 'This text should be 18pt Georgia in blue'); const result = unwrap( await client.doc.format.apply({ - sessionId, + sessionId: sid, target, - inline: { bold: 'on' }, - dryRun: true, + inline: { + fontSize: 18, + color: '#0000FF', + rFonts: { ascii: 'Georgia', hAnsi: 'Georgia' }, + }, }), ); - assertMutationSuccess(result); - await saveResult(sessionId, 'dry-run.docx'); + expect(result.receipt?.success).toBe(true); + await saveResult(sid, 'combined.docx'); }); - it('directive cycle: source inherits bold ON, then off -> clear -> on', async () => { - const seedSessionId = sid('directive-cycle-seed'); - const probe = 'Directive cycle probe text.'; - const { pattern } = await seedBlankFormattableRange(seedSessionId, '.directive-cycle-base.docx', probe); + // --------------------------------------------------------------------------- + // dryRun: verify no mutation occurs + // --------------------------------------------------------------------------- - const sourceDoc = outPath('directive-cycle-source.docx'); - const stylesReceipt = await applyRunDocDefaultsPatch( - outPath('.directive-cycle-base.docx'), - { bold: true }, - sourceDoc, - ); - expect(stylesReceipt.after?.bold).toBe('on'); - - const sessionId = sid('directive-cycle'); - await client.doc.open({ doc: sourceDoc, sessionId }); - - const sourceMatch = await queryFirstTextMatch(sessionId, pattern); - const sourceStyles = firstRunStyles(sourceMatch); - expect(['on', 'clear']).toContain(sourceStyles.direct.bold); - expect(sourceStyles.effective.bold).toBe(true); - const target = await findFirstTextTarget(sessionId, pattern); - - await applyInline(sessionId, target, { bold: 'off' }); - const offMatch = await queryFirstTextMatch(sessionId, pattern); - const offStyles = firstRunStyles(offMatch); - expect(typeof offStyles.effective.bold).toBe('boolean'); - await saveResult(sessionId, 'directive-cycle-off.docx'); - - await applyInline(sessionId, target, { bold: 'clear' }); - const clearMatch = await queryFirstTextMatch(sessionId, pattern); - const clearStyles = firstRunStyles(clearMatch); - expect(typeof clearStyles.effective.bold).toBe('boolean'); - await saveResult(sessionId, 'directive-cycle-clear.docx'); - - await applyInline(sessionId, target, { bold: 'on' }); - const onMatch = await queryFirstTextMatch(sessionId, pattern); - const onStyles = firstRunStyles(onMatch); - expect(onStyles.direct.bold).toBe('on'); - expect(onStyles.effective.bold).toBe(true); - await saveResult(sessionId, 'directive-cycle-on.docx'); - }); + it('dryRun: format.apply returns success without mutating', async () => { + const sid = `dryRun-${Date.now()}`; + const target = await setupFormattableText(sid, 'This text should not actually change'); - it('query.match meta: text selector sets effectiveResolved=true', async () => { - const sessionId = sid('meta-text'); - await openFixtureDoc(sessionId, 'meta-text-source.docx'); - - const pattern = await firstLinePattern(sessionId); - const match = await queryFirstTextMatch(sessionId, pattern); - expect(match.meta?.effectiveResolved).toBe(true); - }); - - it('query.match meta: node selector sets effectiveResolved=false', async () => { - const sessionId = sid('meta-node'); - await openFixtureDoc(sessionId, 'meta-node-source.docx'); - - const nodeMatch = unwrap( - await client.doc.query.match({ - sessionId, - select: { type: 'node', nodeType: 'paragraph' }, - require: 'first', - }), - ); - - expect(Array.isArray(nodeMatch?.items)).toBe(true); - expect(nodeMatch.items.length).toBeGreaterThan(0); - expect(nodeMatch.items[0]?.matchKind).toBe('node'); - expect(nodeMatch.meta?.effectiveResolved).toBe(false); - }); - - it('node-ref mutation: mutations.apply format.apply bold on', async () => { - const sessionId = sid('node-ref-bold-on'); - await openFixtureDoc(sessionId, 'node-ref-source.docx'); - - const nodeMatch = unwrap( - await client.doc.query.match({ - sessionId, - select: { type: 'node', nodeType: 'paragraph' }, - require: 'first', - }), - ); - - const paragraphRef = nodeMatch?.items?.[0]?.handle?.ref as string | undefined; - expect(typeof paragraphRef).toBe('string'); - if (!paragraphRef) { - throw new Error('Could not resolve paragraph ref from node selector.'); - } - - const applyResult = unwrap( - await client.doc.mutations.apply({ - sessionId, - expectedRevision: nodeMatch.evaluatedRevision, - atomic: true, - changeMode: 'direct', - steps: [ - { - id: `node-ref-bold-on-${Date.now()}-${Math.floor(Math.random() * 1_000_000)}`, - op: 'format.apply', - where: { by: 'ref', ref: paragraphRef }, - args: { inline: { bold: 'on' } }, - }, - ], + const result = unwrap( + await client.doc.format.apply({ + sessionId: sid, + target, + inline: { bold: true }, + dryRun: true, }), ); - expect(applyResult?.success).toBe(true); - - const pattern = await firstLinePattern(sessionId); - const updatedMatch = await queryFirstTextMatch(sessionId, pattern); - const updatedStyles = firstRunStyles(updatedMatch); - expect(updatedStyles.direct.bold).toBe('on'); - expect(updatedStyles.effective.bold).toBe(true); - - await saveResult(sessionId, 'node-ref-bold-on.docx'); + expect(result.receipt?.success).toBe(true); + await saveResult(sid, 'dryRun.docx'); }); });