From 01ac5232aaf98d08b0b7971853d2735176d87381 Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Tue, 17 Feb 2026 19:32:42 -0800 Subject: [PATCH 1/5] feat(document-api): add format.italic, format.underline, and format.strikethrough --- apps/docs/document-api/overview.mdx | 5 +- .../reference/_generated-manifest.json | 7 +- .../reference/capabilities/get.mdx | 99 + .../document-api/reference/format/index.mdx | 3 + .../document-api/reference/format/italic.mdx | 854 +++++++ .../reference/format/strikethrough.mdx | 854 +++++++ .../reference/format/underline.mdx | 854 +++++++ apps/docs/document-api/reference/index.mdx | 5 +- .../generated/agent/compatibility-hints.json | 29 +- .../generated/agent/remediation-map.json | 32 +- .../generated/agent/workflow-playbooks.json | 2 +- .../manifests/document-api-tools.json | 2060 +++++++++++++++- .../schemas/document-api-contract.json | 2066 ++++++++++++++++- .../src/contract/operation-definitions.ts | 36 + .../src/contract/operation-registry.ts | 10 +- packages/document-api/src/contract/schemas.ts | 33 + packages/document-api/src/format/format.ts | 118 + packages/document-api/src/index.test.ts | 83 +- packages/document-api/src/index.ts | 33 +- .../document-api/src/invoke/invoke.test.ts | 47 +- packages/document-api/src/invoke/invoke.ts | 3 + .../contract-conformance.test.ts | 127 +- .../assemble-adapters.ts | 10 +- .../capabilities-adapter.ts | 21 +- .../format-adapter.test.ts | 262 ++- .../document-api-adapters/format-adapter.ts | 78 +- .../src/document-api-adapters/index.ts | 10 +- 27 files changed, 7616 insertions(+), 125 deletions(-) create mode 100644 apps/docs/document-api/reference/format/italic.mdx create mode 100644 apps/docs/document-api/reference/format/strikethrough.mdx create mode 100644 apps/docs/document-api/reference/format/underline.mdx diff --git a/apps/docs/document-api/overview.mdx b/apps/docs/document-api/overview.mdx index d1d4f26bd..820d4a5e0 100644 --- a/apps/docs/document-api/overview.mdx +++ b/apps/docs/document-api/overview.mdx @@ -32,7 +32,7 @@ Use the tables below to see what operations are available and where each one is | Core | 8 | [Reference](/document-api/reference/core/index) | | Capabilities | 1 | [Reference](/document-api/reference/capabilities/index) | | Create | 1 | [Reference](/document-api/reference/create/index) | -| Format | 1 | [Reference](/document-api/reference/format/index) | +| Format | 4 | [Reference](/document-api/reference/format/index) | | Lists | 8 | [Reference](/document-api/reference/lists/index) | | Comments | 11 | [Reference](/document-api/reference/comments/index) | | Track Changes | 6 | [Reference](/document-api/reference/track-changes/index) | @@ -48,6 +48,9 @@ Use the tables below to see what operations are available and where each one is | `editor.doc.replace(...)` | [`replace`](/document-api/reference/replace) | | `editor.doc.delete(...)` | [`delete`](/document-api/reference/delete) | | `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.underline(...)` | [`format.underline`](/document-api/reference/format/underline) | +| `editor.doc.format.strikethrough(...)` | [`format.strikethrough`](/document-api/reference/format/strikethrough) | | `editor.doc.create.paragraph(...)` | [`create.paragraph`](/document-api/reference/create/paragraph) | | `editor.doc.lists.list(...)` | [`lists.list`](/document-api/reference/lists/list) | | `editor.doc.lists.get(...)` | [`lists.get`](/document-api/reference/lists/get) | diff --git a/apps/docs/document-api/reference/_generated-manifest.json b/apps/docs/document-api/reference/_generated-manifest.json index 66211da53..4d73ade2a 100644 --- a/apps/docs/document-api/reference/_generated-manifest.json +++ b/apps/docs/document-api/reference/_generated-manifest.json @@ -22,6 +22,9 @@ "apps/docs/document-api/reference/find.mdx", "apps/docs/document-api/reference/format/bold.mdx", "apps/docs/document-api/reference/format/index.mdx", + "apps/docs/document-api/reference/format/italic.mdx", + "apps/docs/document-api/reference/format/strikethrough.mdx", + "apps/docs/document-api/reference/format/underline.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", @@ -68,7 +71,7 @@ }, { "key": "format", - "operationIds": ["format.bold"], + "operationIds": ["format.bold", "format.italic", "format.underline", "format.strikethrough"], "pagePath": "apps/docs/document-api/reference/format/index.mdx", "title": "Format" }, @@ -120,5 +123,5 @@ } ], "marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}", - "sourceHash": "8d875ceb279d213c1e779882f103d39fa2106545ce94c6b1553ae06b6c321e03" + "sourceHash": "9f6796429b2a5ee8e11fe0cf9e482e59be289d0f0efed98f1c74766a6d1199d1" } diff --git a/apps/docs/document-api/reference/capabilities/get.mdx b/apps/docs/document-api/reference/capabilities/get.mdx index 518516789..08931d97c 100644 --- a/apps/docs/document-api/reference/capabilities/get.mdx +++ b/apps/docs/document-api/reference/capabilities/get.mdx @@ -665,6 +665,102 @@ description: Generated reference for capabilities.get ], "type": "object" }, + "format.italic": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "format.strikethrough": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "format.underline": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, "getNode": { "additionalProperties": false, "properties": { @@ -1316,6 +1412,9 @@ description: Generated reference for capabilities.get "replace", "delete", "format.bold", + "format.italic", + "format.underline", + "format.strikethrough", "create.paragraph", "lists.list", "lists.get", diff --git a/apps/docs/document-api/reference/format/index.mdx b/apps/docs/document-api/reference/format/index.mdx index d472ae109..bbdf8300f 100644 --- a/apps/docs/document-api/reference/format/index.mdx +++ b/apps/docs/document-api/reference/format/index.mdx @@ -15,3 +15,6 @@ Formatting mutations. | Operation | Member path | Mutates | Idempotency | Tracked | Dry run | | --- | --- | --- | --- | --- | --- | | [`format.bold`](./bold) | `format.bold` | Yes | `conditional` | Yes | Yes | +| [`format.italic`](./italic) | `format.italic` | Yes | `conditional` | Yes | Yes | +| [`format.underline`](./underline) | `format.underline` | Yes | `conditional` | Yes | Yes | +| [`format.strikethrough`](./strikethrough) | `format.strikethrough` | Yes | `conditional` | Yes | Yes | 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 000000000..6f2083529 --- /dev/null +++ b/apps/docs/document-api/reference/format/italic.mdx @@ -0,0 +1,854 @@ +--- +title: format.italic +sidebarTitle: format.italic +description: Generated 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` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `COMMAND_UNAVAILABLE` +- `TRACK_CHANGE_COMMAND_UNAVAILABLE` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Input schema + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + +## Output schema + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "inserted": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + } + ] + }, + "type": "array" + }, + "removed": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + } + ] + }, + "type": "array" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": [ + "from", + "to" + ], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": [ + "target", + "range", + "text" + ], + "type": "object" + }, + "success": { + "const": true + }, + "updated": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + } + ] + }, + "type": "array" + } + }, + "required": [ + "success", + "resolution" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": [ + "from", + "to" + ], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": [ + "target", + "range", + "text" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + +## Success schema + +```json +{ + "additionalProperties": false, + "properties": { + "inserted": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + } + ] + }, + "type": "array" + }, + "removed": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + } + ] + }, + "type": "array" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": [ + "from", + "to" + ], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": [ + "target", + "range", + "text" + ], + "type": "object" + }, + "success": { + "const": true + }, + "updated": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + } + ] + }, + "type": "array" + } + }, + "required": [ + "success", + "resolution" + ], + "type": "object" +} +``` + +## Failure schema + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": [ + "from", + "to" + ], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": [ + "target", + "range", + "text" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" +} +``` diff --git a/apps/docs/document-api/reference/format/strikethrough.mdx b/apps/docs/document-api/reference/format/strikethrough.mdx new file mode 100644 index 000000000..2059db366 --- /dev/null +++ b/apps/docs/document-api/reference/format/strikethrough.mdx @@ -0,0 +1,854 @@ +--- +title: format.strikethrough +sidebarTitle: format.strikethrough +description: Generated reference for format.strikethrough +--- + +{/* 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.strikethrough` +- API member path: `editor.doc.format.strikethrough(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `yes` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `COMMAND_UNAVAILABLE` +- `TRACK_CHANGE_COMMAND_UNAVAILABLE` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Input schema + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + +## Output schema + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "inserted": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + } + ] + }, + "type": "array" + }, + "removed": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + } + ] + }, + "type": "array" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": [ + "from", + "to" + ], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": [ + "target", + "range", + "text" + ], + "type": "object" + }, + "success": { + "const": true + }, + "updated": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + } + ] + }, + "type": "array" + } + }, + "required": [ + "success", + "resolution" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": [ + "from", + "to" + ], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": [ + "target", + "range", + "text" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + +## Success schema + +```json +{ + "additionalProperties": false, + "properties": { + "inserted": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + } + ] + }, + "type": "array" + }, + "removed": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + } + ] + }, + "type": "array" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": [ + "from", + "to" + ], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": [ + "target", + "range", + "text" + ], + "type": "object" + }, + "success": { + "const": true + }, + "updated": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + } + ] + }, + "type": "array" + } + }, + "required": [ + "success", + "resolution" + ], + "type": "object" +} +``` + +## Failure schema + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": [ + "from", + "to" + ], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": [ + "target", + "range", + "text" + ], + "type": "object" + }, + "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 000000000..50ff5b6db --- /dev/null +++ b/apps/docs/document-api/reference/format/underline.mdx @@ -0,0 +1,854 @@ +--- +title: format.underline +sidebarTitle: format.underline +description: Generated 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` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `COMMAND_UNAVAILABLE` +- `TRACK_CHANGE_COMMAND_UNAVAILABLE` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Input schema + +```json +{ + "additionalProperties": false, + "properties": { + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + } + }, + "required": [ + "target" + ], + "type": "object" +} +``` + +## Output schema + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "inserted": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + } + ] + }, + "type": "array" + }, + "removed": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + } + ] + }, + "type": "array" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": [ + "from", + "to" + ], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": [ + "target", + "range", + "text" + ], + "type": "object" + }, + "success": { + "const": true + }, + "updated": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + } + ] + }, + "type": "array" + } + }, + "required": [ + "success", + "resolution" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": [ + "from", + "to" + ], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": [ + "target", + "range", + "text" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure", + "resolution" + ], + "type": "object" + } + ] +} +``` + +## Success schema + +```json +{ + "additionalProperties": false, + "properties": { + "inserted": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + } + ] + }, + "type": "array" + }, + "removed": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + } + ] + }, + "type": "array" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": [ + "from", + "to" + ], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": [ + "target", + "range", + "text" + ], + "type": "object" + }, + "success": { + "const": true + }, + "updated": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + } + ] + }, + "type": "array" + } + }, + "required": [ + "success", + "resolution" + ], + "type": "object" +} +``` + +## Failure schema + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": [ + "from", + "to" + ], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": [ + "target", + "range", + "text" + ], + "type": "object" + }, + "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 9648b8135..bcb00c2f8 100644 --- a/apps/docs/document-api/reference/index.mdx +++ b/apps/docs/document-api/reference/index.mdx @@ -16,7 +16,7 @@ Document API is currently alpha and subject to breaking changes. | Core | 8 | [Open](./core/index) | | Capabilities | 1 | [Open](./capabilities/index) | | Create | 1 | [Open](./create/index) | -| Format | 1 | [Open](./format/index) | +| Format | 4 | [Open](./format/index) | | Lists | 8 | [Open](./lists/index) | | Comments | 11 | [Open](./comments/index) | | Track Changes | 6 | [Open](./track-changes/index) | @@ -34,6 +34,9 @@ Document API is currently alpha and subject to breaking changes. | [`replace`](./replace) | Core | `replace` | Yes | `conditional` | Yes | Yes | | [`delete`](./delete) | Core | `delete` | Yes | `conditional` | Yes | Yes | | [`format.bold`](./format/bold) | Format | `format.bold` | Yes | `conditional` | Yes | Yes | +| [`format.italic`](./format/italic) | Format | `format.italic` | Yes | `conditional` | Yes | Yes | +| [`format.underline`](./format/underline) | Format | `format.underline` | Yes | `conditional` | Yes | Yes | +| [`format.strikethrough`](./format/strikethrough) | Format | `format.strikethrough` | Yes | `conditional` | Yes | Yes | | [`create.paragraph`](./create/paragraph) | Create | `create.paragraph` | Yes | `non-idempotent` | Yes | Yes | | [`lists.list`](./lists/list) | Lists | `lists.list` | No | `idempotent` | No | No | | [`lists.get`](./lists/get) | Lists | `lists.get` | No | `idempotent` | No | No | diff --git a/packages/document-api/generated/agent/compatibility-hints.json b/packages/document-api/generated/agent/compatibility-hints.json index 55fc4d83d..e1ad770bc 100644 --- a/packages/document-api/generated/agent/compatibility-hints.json +++ b/packages/document-api/generated/agent/compatibility-hints.json @@ -145,6 +145,33 @@ "supportsDryRun": true, "supportsTrackedMode": true }, + "format.italic": { + "deterministicTargetResolution": true, + "memberPath": "format.italic", + "mutates": true, + "postApplyThrowForbidden": true, + "requiresPreflightCapabilitiesCheck": true, + "supportsDryRun": true, + "supportsTrackedMode": true + }, + "format.strikethrough": { + "deterministicTargetResolution": true, + "memberPath": "format.strikethrough", + "mutates": true, + "postApplyThrowForbidden": true, + "requiresPreflightCapabilitiesCheck": true, + "supportsDryRun": true, + "supportsTrackedMode": true + }, + "format.underline": { + "deterministicTargetResolution": true, + "memberPath": "format.underline", + "mutates": true, + "postApplyThrowForbidden": true, + "requiresPreflightCapabilitiesCheck": true, + "supportsDryRun": true, + "supportsTrackedMode": true + }, "getNode": { "deterministicTargetResolution": true, "memberPath": "getNode", @@ -326,5 +353,5 @@ "supportsTrackedMode": false } }, - "sourceHash": "8d875ceb279d213c1e779882f103d39fa2106545ce94c6b1553ae06b6c321e03" + "sourceHash": "9f6796429b2a5ee8e11fe0cf9e482e59be289d0f0efed98f1c74766a6d1199d1" } diff --git a/packages/document-api/generated/agent/remediation-map.json b/packages/document-api/generated/agent/remediation-map.json index 38fda4f4b..b735bb04e 100644 --- a/packages/document-api/generated/agent/remediation-map.json +++ b/packages/document-api/generated/agent/remediation-map.json @@ -18,6 +18,9 @@ "create.paragraph", "delete", "format.bold", + "format.italic", + "format.strikethrough", + "format.underline", "insert", "lists.exit", "lists.indent", @@ -44,6 +47,9 @@ "create.paragraph", "delete", "format.bold", + "format.italic", + "format.strikethrough", + "format.underline", "insert", "lists.exit", "lists.indent", @@ -74,6 +80,9 @@ "comments.setInternal", "create.paragraph", "format.bold", + "format.italic", + "format.strikethrough", + "format.underline", "lists.exit", "lists.indent", "lists.insert", @@ -97,6 +106,9 @@ "comments.setInternal", "create.paragraph", "format.bold", + "format.italic", + "format.strikethrough", + "format.underline", "lists.exit", "lists.indent", "lists.insert", @@ -120,6 +132,9 @@ "comments.setInternal", "create.paragraph", "format.bold", + "format.italic", + "format.strikethrough", + "format.underline", "insert", "lists.exit", "lists.indent", @@ -137,6 +152,9 @@ "comments.setInternal", "create.paragraph", "format.bold", + "format.italic", + "format.strikethrough", + "format.underline", "insert", "lists.exit", "lists.indent", @@ -209,6 +227,9 @@ "create.paragraph", "delete", "format.bold", + "format.italic", + "format.strikethrough", + "format.underline", "getNode", "getNodeById", "insert", @@ -239,6 +260,9 @@ "create.paragraph", "delete", "format.bold", + "format.italic", + "format.strikethrough", + "format.underline", "getNode", "getNodeById", "insert", @@ -264,6 +288,9 @@ "create.paragraph", "delete", "format.bold", + "format.italic", + "format.strikethrough", + "format.underline", "insert", "lists.exit", "lists.indent", @@ -277,6 +304,9 @@ "create.paragraph", "delete", "format.bold", + "format.italic", + "format.strikethrough", + "format.underline", "insert", "lists.exit", "lists.indent", @@ -288,5 +318,5 @@ ] } ], - "sourceHash": "8d875ceb279d213c1e779882f103d39fa2106545ce94c6b1553ae06b6c321e03" + "sourceHash": "9f6796429b2a5ee8e11fe0cf9e482e59be289d0f0efed98f1c74766a6d1199d1" } diff --git a/packages/document-api/generated/agent/workflow-playbooks.json b/packages/document-api/generated/agent/workflow-playbooks.json index 2b6f01785..9a80a3aa6 100644 --- a/packages/document-api/generated/agent/workflow-playbooks.json +++ b/packages/document-api/generated/agent/workflow-playbooks.json @@ -1,6 +1,6 @@ { "contractVersion": "0.1.0", - "sourceHash": "8d875ceb279d213c1e779882f103d39fa2106545ce94c6b1553ae06b6c321e03", + "sourceHash": "9f6796429b2a5ee8e11fe0cf9e482e59be289d0f0efed98f1c74766a6d1199d1", "workflows": [ { "id": "find-mutate", diff --git a/packages/document-api/generated/manifests/document-api-tools.json b/packages/document-api/generated/manifests/document-api-tools.json index f372d928c..44c1d20d7 100644 --- a/packages/document-api/generated/manifests/document-api-tools.json +++ b/packages/document-api/generated/manifests/document-api-tools.json @@ -2,7 +2,7 @@ "contractVersion": "0.1.0", "generatedAt": null, "sourceCommit": null, - "sourceHash": "8d875ceb279d213c1e779882f103d39fa2106545ce94c6b1553ae06b6c321e03", + "sourceHash": "9f6796429b2a5ee8e11fe0cf9e482e59be289d0f0efed98f1c74766a6d1199d1", "tools": [ { "description": "Read Document API data via `find`.", @@ -3527,6 +3527,1977 @@ "supportsDryRun": true, "supportsTrackedMode": true }, + { + "description": "Apply Document API mutation `format.italic`.", + "deterministicTargetResolution": true, + "failureSchema": { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": ["INVALID_TARGET"] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": ["success", "failure", "resolution"], + "type": "object" + }, + "idempotency": "conditional", + "inputSchema": { + "additionalProperties": false, + "properties": { + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + } + }, + "required": ["target"], + "type": "object" + }, + "memberPath": "format.italic", + "mutates": true, + "name": "format.italic", + "outputSchema": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "inserted": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "removed": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": true + }, + "updated": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + } + }, + "required": ["success", "resolution"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": ["INVALID_TARGET"] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": ["success", "failure", "resolution"], + "type": "object" + } + ] + }, + "possibleFailureCodes": ["INVALID_TARGET"], + "preApplyThrows": [ + "TARGET_NOT_FOUND", + "COMMAND_UNAVAILABLE", + "TRACK_CHANGE_COMMAND_UNAVAILABLE", + "CAPABILITY_UNAVAILABLE" + ], + "remediationHints": [], + "successSchema": { + "additionalProperties": false, + "properties": { + "inserted": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "removed": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": true + }, + "updated": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + } + }, + "required": ["success", "resolution"], + "type": "object" + }, + "supportsDryRun": true, + "supportsTrackedMode": true + }, + { + "description": "Apply Document API mutation `format.underline`.", + "deterministicTargetResolution": true, + "failureSchema": { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": ["INVALID_TARGET"] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": ["success", "failure", "resolution"], + "type": "object" + }, + "idempotency": "conditional", + "inputSchema": { + "additionalProperties": false, + "properties": { + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + } + }, + "required": ["target"], + "type": "object" + }, + "memberPath": "format.underline", + "mutates": true, + "name": "format.underline", + "outputSchema": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "inserted": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "removed": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": true + }, + "updated": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + } + }, + "required": ["success", "resolution"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": ["INVALID_TARGET"] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": ["success", "failure", "resolution"], + "type": "object" + } + ] + }, + "possibleFailureCodes": ["INVALID_TARGET"], + "preApplyThrows": [ + "TARGET_NOT_FOUND", + "COMMAND_UNAVAILABLE", + "TRACK_CHANGE_COMMAND_UNAVAILABLE", + "CAPABILITY_UNAVAILABLE" + ], + "remediationHints": [], + "successSchema": { + "additionalProperties": false, + "properties": { + "inserted": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "removed": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": true + }, + "updated": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + } + }, + "required": ["success", "resolution"], + "type": "object" + }, + "supportsDryRun": true, + "supportsTrackedMode": true + }, + { + "description": "Apply Document API mutation `format.strikethrough`.", + "deterministicTargetResolution": true, + "failureSchema": { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": ["INVALID_TARGET"] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": ["success", "failure", "resolution"], + "type": "object" + }, + "idempotency": "conditional", + "inputSchema": { + "additionalProperties": false, + "properties": { + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + } + }, + "required": ["target"], + "type": "object" + }, + "memberPath": "format.strikethrough", + "mutates": true, + "name": "format.strikethrough", + "outputSchema": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "inserted": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "removed": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": true + }, + "updated": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + } + }, + "required": ["success", "resolution"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": ["INVALID_TARGET"] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": ["success", "failure", "resolution"], + "type": "object" + } + ] + }, + "possibleFailureCodes": ["INVALID_TARGET"], + "preApplyThrows": [ + "TARGET_NOT_FOUND", + "COMMAND_UNAVAILABLE", + "TRACK_CHANGE_COMMAND_UNAVAILABLE", + "CAPABILITY_UNAVAILABLE" + ], + "remediationHints": [], + "successSchema": { + "additionalProperties": false, + "properties": { + "inserted": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "removed": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": true + }, + "updated": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + } + }, + "required": ["success", "resolution"], + "type": "object" + }, + "supportsDryRun": true, + "supportsTrackedMode": true + }, { "description": "Apply Document API mutation `create.paragraph`.", "deterministicTargetResolution": true, @@ -10090,6 +12061,90 @@ "required": ["available", "tracked", "dryRun"], "type": "object" }, + "format.italic": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": ["available", "tracked", "dryRun"], + "type": "object" + }, + "format.strikethrough": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": ["available", "tracked", "dryRun"], + "type": "object" + }, + "format.underline": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": ["available", "tracked", "dryRun"], + "type": "object" + }, "getNode": { "additionalProperties": false, "properties": { @@ -10661,6 +12716,9 @@ "replace", "delete", "format.bold", + "format.italic", + "format.underline", + "format.strikethrough", "create.paragraph", "lists.list", "lists.get", diff --git a/packages/document-api/generated/schemas/document-api-contract.json b/packages/document-api/generated/schemas/document-api-contract.json index 346cd1b8b..1fcec28ad 100644 --- a/packages/document-api/generated/schemas/document-api-contract.json +++ b/packages/document-api/generated/schemas/document-api-contract.json @@ -571,6 +571,90 @@ "required": ["available", "tracked", "dryRun"], "type": "object" }, + "format.italic": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": ["available", "tracked", "dryRun"], + "type": "object" + }, + "format.strikethrough": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": ["available", "tracked", "dryRun"], + "type": "object" + }, + "format.underline": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": ["available", "tracked", "dryRun"], + "type": "object" + }, "getNode": { "additionalProperties": false, "properties": { @@ -1142,6 +1226,9 @@ "replace", "delete", "format.bold", + "format.italic", + "format.underline", + "format.strikethrough", "create.paragraph", "lists.list", "lists.get", @@ -6432,6 +6519,1983 @@ "type": "object" } }, + "format.italic": { + "failureSchema": { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": ["INVALID_TARGET"] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": ["success", "failure", "resolution"], + "type": "object" + }, + "inputSchema": { + "additionalProperties": false, + "properties": { + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + } + }, + "required": ["target"], + "type": "object" + }, + "memberPath": "format.italic", + "metadata": { + "deterministicTargetResolution": true, + "idempotency": "conditional", + "mutates": true, + "possibleFailureCodes": ["INVALID_TARGET"], + "supportsDryRun": true, + "supportsTrackedMode": true, + "throws": { + "postApplyForbidden": true, + "preApply": [ + "TARGET_NOT_FOUND", + "COMMAND_UNAVAILABLE", + "TRACK_CHANGE_COMMAND_UNAVAILABLE", + "CAPABILITY_UNAVAILABLE" + ] + } + }, + "outputSchema": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "inserted": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "removed": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": true + }, + "updated": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + } + }, + "required": ["success", "resolution"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": ["INVALID_TARGET"] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": ["success", "failure", "resolution"], + "type": "object" + } + ] + }, + "successSchema": { + "additionalProperties": false, + "properties": { + "inserted": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "removed": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": true + }, + "updated": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + } + }, + "required": ["success", "resolution"], + "type": "object" + } + }, + "format.strikethrough": { + "failureSchema": { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": ["INVALID_TARGET"] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": ["success", "failure", "resolution"], + "type": "object" + }, + "inputSchema": { + "additionalProperties": false, + "properties": { + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + } + }, + "required": ["target"], + "type": "object" + }, + "memberPath": "format.strikethrough", + "metadata": { + "deterministicTargetResolution": true, + "idempotency": "conditional", + "mutates": true, + "possibleFailureCodes": ["INVALID_TARGET"], + "supportsDryRun": true, + "supportsTrackedMode": true, + "throws": { + "postApplyForbidden": true, + "preApply": [ + "TARGET_NOT_FOUND", + "COMMAND_UNAVAILABLE", + "TRACK_CHANGE_COMMAND_UNAVAILABLE", + "CAPABILITY_UNAVAILABLE" + ] + } + }, + "outputSchema": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "inserted": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "removed": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": true + }, + "updated": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + } + }, + "required": ["success", "resolution"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": ["INVALID_TARGET"] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": ["success", "failure", "resolution"], + "type": "object" + } + ] + }, + "successSchema": { + "additionalProperties": false, + "properties": { + "inserted": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "removed": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": true + }, + "updated": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + } + }, + "required": ["success", "resolution"], + "type": "object" + } + }, + "format.underline": { + "failureSchema": { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": ["INVALID_TARGET"] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": ["success", "failure", "resolution"], + "type": "object" + }, + "inputSchema": { + "additionalProperties": false, + "properties": { + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + } + }, + "required": ["target"], + "type": "object" + }, + "memberPath": "format.underline", + "metadata": { + "deterministicTargetResolution": true, + "idempotency": "conditional", + "mutates": true, + "possibleFailureCodes": ["INVALID_TARGET"], + "supportsDryRun": true, + "supportsTrackedMode": true, + "throws": { + "postApplyForbidden": true, + "preApply": [ + "TARGET_NOT_FOUND", + "COMMAND_UNAVAILABLE", + "TRACK_CHANGE_COMMAND_UNAVAILABLE", + "CAPABILITY_UNAVAILABLE" + ] + } + }, + "outputSchema": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "inserted": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "removed": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": true + }, + "updated": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + } + }, + "required": ["success", "resolution"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": ["INVALID_TARGET"] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "type": "object" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": ["success", "failure", "resolution"], + "type": "object" + } + ] + }, + "successSchema": { + "additionalProperties": false, + "properties": { + "inserted": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "removed": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + }, + "resolution": { + "additionalProperties": false, + "properties": { + "range": { + "additionalProperties": false, + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + }, + "required": ["from", "to"], + "type": "object" + }, + "requestedTarget": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "target": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "text": { + "type": "string" + } + }, + "required": ["target", "range", "text"], + "type": "object" + }, + "success": { + "const": true + }, + "updated": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "comment" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + } + ] + }, + "type": "array" + } + }, + "required": ["success", "resolution"], + "type": "object" + } + }, "getNode": { "inputSchema": { "oneOf": [ @@ -10774,5 +12838,5 @@ } }, "sourceCommit": null, - "sourceHash": "8d875ceb279d213c1e779882f103d39fa2106545ce94c6b1553ae06b6c321e03" + "sourceHash": "9f6796429b2a5ee8e11fe0cf9e482e59be289d0f0efed98f1c74766a6d1199d1" } diff --git a/packages/document-api/src/contract/operation-definitions.ts b/packages/document-api/src/contract/operation-definitions.ts index cc1d0cb4f..89714e48e 100644 --- a/packages/document-api/src/contract/operation-definitions.ts +++ b/packages/document-api/src/contract/operation-definitions.ts @@ -204,6 +204,42 @@ export const OPERATION_DEFINITIONS = { referenceDocPath: 'format/bold.mdx', referenceGroup: 'format', }, + 'format.italic': { + memberPath: 'format.italic', + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: true, + possibleFailureCodes: ['INVALID_TARGET'], + throws: T_NOT_FOUND_COMMAND_TRACKED, + }), + referenceDocPath: 'format/italic.mdx', + referenceGroup: 'format', + }, + 'format.underline': { + memberPath: 'format.underline', + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: true, + possibleFailureCodes: ['INVALID_TARGET'], + throws: T_NOT_FOUND_COMMAND_TRACKED, + }), + referenceDocPath: 'format/underline.mdx', + referenceGroup: 'format', + }, + 'format.strikethrough': { + memberPath: 'format.strikethrough', + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: true, + possibleFailureCodes: ['INVALID_TARGET'], + throws: T_NOT_FOUND_COMMAND_TRACKED, + }), + referenceDocPath: 'format/strikethrough.mdx', + referenceGroup: 'format', + }, 'create.paragraph': { memberPath: 'create.paragraph', diff --git a/packages/document-api/src/contract/operation-registry.ts b/packages/document-api/src/contract/operation-registry.ts index c86834fea..cf0ca59fe 100644 --- a/packages/document-api/src/contract/operation-registry.ts +++ b/packages/document-api/src/contract/operation-registry.ts @@ -21,7 +21,12 @@ import type { InsertInput } from '../insert/insert.js'; import type { ReplaceInput } from '../replace/replace.js'; import type { DeleteInput } from '../delete/delete.js'; import type { MutationOptions } from '../write/write.js'; -import type { FormatBoldInput } from '../format/format.js'; +import type { + FormatBoldInput, + FormatItalicInput, + FormatUnderlineInput, + FormatStrikethroughInput, +} from '../format/format.js'; import type { AddCommentInput, EditCommentInput, @@ -73,6 +78,9 @@ export interface OperationRegistry { // --- format.* --- 'format.bold': { input: FormatBoldInput; options: MutationOptions; output: TextMutationReceipt }; + 'format.italic': { input: FormatItalicInput; options: MutationOptions; output: TextMutationReceipt }; + 'format.underline': { input: FormatUnderlineInput; options: MutationOptions; output: TextMutationReceipt }; + 'format.strikethrough': { input: FormatStrikethroughInput; options: MutationOptions; output: TextMutationReceipt }; // --- create.* --- 'create.paragraph': { input: CreateParagraphInput; options: MutationOptions; output: CreateParagraphResult }; diff --git a/packages/document-api/src/contract/schemas.ts b/packages/document-api/src/contract/schemas.ts index 9a3defcf8..156ce9e3f 100644 --- a/packages/document-api/src/contract/schemas.ts +++ b/packages/document-api/src/contract/schemas.ts @@ -655,6 +655,39 @@ const operationSchemas: Record = { success: textMutationSuccessSchema, failure: textMutationFailureSchemaFor('format.bold'), }, + 'format.italic': { + input: objectSchema( + { + target: textAddressSchema, + }, + ['target'], + ), + output: textMutationResultSchemaFor('format.italic'), + success: textMutationSuccessSchema, + failure: textMutationFailureSchemaFor('format.italic'), + }, + 'format.underline': { + input: objectSchema( + { + target: textAddressSchema, + }, + ['target'], + ), + output: textMutationResultSchemaFor('format.underline'), + success: textMutationSuccessSchema, + failure: textMutationFailureSchemaFor('format.underline'), + }, + 'format.strikethrough': { + input: objectSchema( + { + target: textAddressSchema, + }, + ['target'], + ), + output: textMutationResultSchemaFor('format.strikethrough'), + success: textMutationSuccessSchema, + failure: textMutationFailureSchemaFor('format.strikethrough'), + }, 'create.paragraph': { input: objectSchema({ at: { diff --git a/packages/document-api/src/format/format.ts b/packages/document-api/src/format/format.ts index 688871408..2b9f02da9 100644 --- a/packages/document-api/src/format/format.ts +++ b/packages/document-api/src/format/format.ts @@ -1,17 +1,63 @@ import { normalizeMutationOptions, type MutationOptions } from '../write/write.js'; import type { TextAddress, TextMutationReceipt } from '../types/index.js'; +/** + * Input payload for `format.bold`. + */ export interface FormatBoldInput { target: TextAddress; } +/** + * Input payload for `format.italic`. + */ +export interface FormatItalicInput { + target: TextAddress; +} + +/** + * Input payload for `format.underline`. + */ +export interface FormatUnderlineInput { + target: TextAddress; +} + +/** + * Input payload for `format.strikethrough`. + */ +export interface FormatStrikethroughInput { + target: TextAddress; +} + export interface FormatAdapter { /** Apply or toggle bold formatting on the target text range. */ bold(input: FormatBoldInput, options?: MutationOptions): TextMutationReceipt; + /** Apply or toggle italic formatting on the target text range. */ + italic(input: FormatItalicInput, options?: MutationOptions): TextMutationReceipt; + /** Apply or toggle underline formatting on the target text range. */ + underline(input: FormatUnderlineInput, options?: MutationOptions): TextMutationReceipt; + /** Apply or toggle strikethrough formatting on the target text range. */ + strikethrough(input: FormatStrikethroughInput, options?: MutationOptions): TextMutationReceipt; } export type FormatApi = FormatAdapter; +/** + * Executes `format.bold` using the provided adapter. + * + * @param adapter - Adapter implementation that performs format mutations. + * @param input - Text target payload for the bold mutation. + * @param options - Optional mutation execution options. + * @returns The mutation receipt produced by the adapter. + * @throws {Error} Propagates adapter errors when the target or capabilities are invalid. + * + * @example + * ```ts + * const receipt = executeFormatBold(adapter, { + * target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } }, + * }); + * ``` + */ export function executeFormatBold( adapter: FormatAdapter, input: FormatBoldInput, @@ -19,3 +65,75 @@ export function executeFormatBold( ): TextMutationReceipt { return adapter.bold(input, normalizeMutationOptions(options)); } + +/** + * Executes `format.italic` using the provided adapter. + * + * @param adapter - Adapter implementation that performs format mutations. + * @param input - Text target payload for the italic mutation. + * @param options - Optional mutation execution options. + * @returns The mutation receipt produced by the adapter. + * @throws {Error} Propagates adapter errors when the target or capabilities are invalid. + * + * @example + * ```ts + * const receipt = executeFormatItalic(adapter, { + * target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } }, + * }); + * ``` + */ +export function executeFormatItalic( + adapter: FormatAdapter, + input: FormatItalicInput, + options?: MutationOptions, +): TextMutationReceipt { + return adapter.italic(input, normalizeMutationOptions(options)); +} + +/** + * Executes `format.underline` using the provided adapter. + * + * @param adapter - Adapter implementation that performs format mutations. + * @param input - Text target payload for the underline mutation. + * @param options - Optional mutation execution options. + * @returns The mutation receipt produced by the adapter. + * @throws {Error} Propagates adapter errors when the target or capabilities are invalid. + * + * @example + * ```ts + * const receipt = executeFormatUnderline(adapter, { + * target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } }, + * }); + * ``` + */ +export function executeFormatUnderline( + adapter: FormatAdapter, + input: FormatUnderlineInput, + options?: MutationOptions, +): TextMutationReceipt { + return adapter.underline(input, normalizeMutationOptions(options)); +} + +/** + * Executes `format.strikethrough` using the provided adapter. + * + * @param adapter - Adapter implementation that performs format mutations. + * @param input - Text target payload for the strikethrough mutation. + * @param options - Optional mutation execution options. + * @returns The mutation receipt produced by the adapter. + * @throws {Error} Propagates adapter errors when the target or capabilities are invalid. + * + * @example + * ```ts + * const receipt = executeFormatStrikethrough(adapter, { + * target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } }, + * }); + * ``` + */ +export function executeFormatStrikethrough( + adapter: FormatAdapter, + input: FormatStrikethroughInput, + options?: MutationOptions, +): TextMutationReceipt { + return adapter.strikethrough(input, normalizeMutationOptions(options)); +} diff --git a/packages/document-api/src/index.test.ts b/packages/document-api/src/index.test.ts index 6c90cfcc4..99f03554d 100644 --- a/packages/document-api/src/index.test.ts +++ b/packages/document-api/src/index.test.ts @@ -109,16 +109,23 @@ function makeWriteAdapter(): WriteAdapter { }; } +function makeFormatReceipt() { + return { + success: true as const, + resolution: { + target: { kind: 'text' as const, blockId: 'p1', range: { start: 0, end: 2 } }, + range: { from: 1, to: 3 }, + text: 'Hi', + }, + }; +} + function makeFormatAdapter(): FormatAdapter { return { - bold: vi.fn(() => ({ - success: true as const, - resolution: { - target: { kind: 'text' as const, blockId: 'p1', range: { start: 0, end: 2 } }, - range: { from: 1, to: 3 }, - text: 'Hi', - }, - })), + bold: vi.fn(() => makeFormatReceipt()), + italic: vi.fn(() => makeFormatReceipt()), + underline: vi.fn(() => makeFormatReceipt()), + strikethrough: vi.fn(() => makeFormatReceipt()), }; } @@ -495,6 +502,66 @@ describe('createDocumentApi', () => { expect(formatAdpt.bold).toHaveBeenCalledWith({ target }, { changeMode: 'tracked', dryRun: false }); }); + it('delegates format.italic to the format adapter', () => { + 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.italic({ target }, { changeMode: 'direct' }); + expect(formatAdpt.italic).toHaveBeenCalledWith({ target }, { changeMode: 'direct', dryRun: false }); + }); + + it('delegates format.underline to the format adapter', () => { + 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.underline({ target }, { changeMode: 'direct' }); + expect(formatAdpt.underline).toHaveBeenCalledWith({ target }, { changeMode: 'direct', dryRun: false }); + }); + + it('delegates format.strikethrough to the format adapter', () => { + 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.strikethrough({ target }, { changeMode: 'tracked' }); + expect(formatAdpt.strikethrough).toHaveBeenCalledWith({ target }, { changeMode: 'tracked', dryRun: false }); + }); + it('delegates trackChanges namespace operations', () => { const trackAdpt = makeTrackChangesAdapter(); const api = createDocumentApi({ diff --git a/packages/document-api/src/index.ts b/packages/document-api/src/index.ts index 6d8507654..eab8de602 100644 --- a/packages/document-api/src/index.ts +++ b/packages/document-api/src/index.ts @@ -50,8 +50,20 @@ import { } from './comments/comments.js'; import type { DeleteInput } from './delete/delete.js'; import { executeFind, type FindAdapter, type FindOptions } from './find/find.js'; -import type { FormatAdapter, FormatApi, FormatBoldInput } from './format/format.js'; -import { executeFormatBold } from './format/format.js'; +import type { + FormatAdapter, + FormatApi, + FormatBoldInput, + FormatItalicInput, + FormatUnderlineInput, + FormatStrikethroughInput, +} from './format/format.js'; +import { + executeFormatBold, + executeFormatItalic, + executeFormatUnderline, + executeFormatStrikethrough, +} from './format/format.js'; import type { GetNodeAdapter, GetNodeByIdInput } from './get-node/get-node.js'; import { executeGetNode, executeGetNodeById } from './get-node/get-node.js'; import { executeGetText, type GetTextAdapter, type GetTextInput } from './get-text/get-text.js'; @@ -118,7 +130,13 @@ export type { GetNodeAdapter, GetNodeByIdInput } from './get-node/get-node.js'; export type { GetTextAdapter, GetTextInput } from './get-text/get-text.js'; export type { InfoAdapter, InfoInput } from './info/info.js'; export type { MutationOptions, WriteAdapter, WriteRequest } from './write/write.js'; -export type { FormatAdapter, FormatBoldInput } from './format/format.js'; +export type { + FormatAdapter, + FormatBoldInput, + FormatItalicInput, + FormatUnderlineInput, + FormatStrikethroughInput, +} from './format/format.js'; export type { CreateAdapter } from './create/create.js'; export type { TrackChangesAcceptAllInput, @@ -359,6 +377,15 @@ export function createDocumentApi(adapters: DocumentApiAdapters): DocumentApi { bold(input: FormatBoldInput, options?: MutationOptions): TextMutationReceipt { return executeFormatBold(adapters.format, input, options); }, + italic(input: FormatItalicInput, options?: MutationOptions): TextMutationReceipt { + return executeFormatItalic(adapters.format, input, options); + }, + underline(input: FormatUnderlineInput, options?: MutationOptions): TextMutationReceipt { + return executeFormatUnderline(adapters.format, input, options); + }, + strikethrough(input: FormatStrikethroughInput, options?: MutationOptions): TextMutationReceipt { + return executeFormatStrikethrough(adapters.format, input, options); + }, }, trackChanges: { list(input?: TrackChangesListInput): TrackChangesListResult { diff --git a/packages/document-api/src/invoke/invoke.test.ts b/packages/document-api/src/invoke/invoke.test.ts index 93493f234..25e572a17 100644 --- a/packages/document-api/src/invoke/invoke.test.ts +++ b/packages/document-api/src/invoke/invoke.test.ts @@ -66,15 +66,19 @@ function makeAdapters() { }, })), }; + const formatReceipt = () => ({ + success: true as const, + resolution: { + target: { kind: 'text' as const, blockId: 'p1', range: { start: 0, end: 2 } }, + range: { from: 1, to: 3 }, + text: 'Hi', + }, + }); const formatAdapter: FormatAdapter = { - bold: vi.fn(() => ({ - success: true as const, - resolution: { - target: { kind: 'text' as const, blockId: 'p1', range: { start: 0, end: 2 } }, - range: { from: 1, to: 3 }, - text: 'Hi', - }, - })), + bold: vi.fn(formatReceipt), + italic: vi.fn(formatReceipt), + underline: vi.fn(formatReceipt), + strikethrough: vi.fn(formatReceipt), }; const trackChangesAdapter: TrackChangesAdapter = { list: vi.fn(() => ({ matches: [], total: 0 })), @@ -229,6 +233,33 @@ describe('invoke', () => { const invoked = api.invoke({ operationId: 'lists.get', input }); expect(invoked).toEqual(direct); }); + + it('format.italic: 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 } } }; + const direct = api.format.italic(input); + const invoked = api.invoke({ operationId: 'format.italic', input }); + expect(invoked).toEqual(direct); + }); + + it('format.underline: 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 } } }; + const direct = api.format.underline(input); + const invoked = api.invoke({ operationId: 'format.underline', input }); + expect(invoked).toEqual(direct); + }); + + it('format.strikethrough: 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 } } }; + const direct = api.format.strikethrough(input); + const invoked = api.invoke({ operationId: 'format.strikethrough', input }); + expect(invoked).toEqual(direct); + }); }); describe('error handling', () => { diff --git a/packages/document-api/src/invoke/invoke.ts b/packages/document-api/src/invoke/invoke.ts index ef54fbb24..ec11c056f 100644 --- a/packages/document-api/src/invoke/invoke.ts +++ b/packages/document-api/src/invoke/invoke.ts @@ -46,6 +46,9 @@ export function buildDispatchTable(api: DocumentApi): TypedDispatchTable { // --- format.* --- 'format.bold': (input, options) => api.format.bold(input, options), + 'format.italic': (input, options) => api.format.italic(input, options), + 'format.underline': (input, options) => api.format.underline(input, options), + 'format.strikethrough': (input, options) => api.format.strikethrough(input, options), // --- create.* --- 'create.paragraph': (input, options) => api.create.paragraph(input, options), 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 269ad4ed9..dd345121a 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 @@ -16,7 +16,12 @@ import { import { ListHelpers } from '../../core/helpers/list-numbering-helpers.js'; import { createCommentsAdapter } from '../comments-adapter.js'; import { createParagraphAdapter } from '../create-adapter.js'; -import { formatBoldAdapter } from '../format-adapter.js'; +import { + formatBoldAdapter, + formatItalicAdapter, + formatUnderlineAdapter, + formatStrikethroughAdapter, +} from '../format-adapter.js'; import { getDocumentApiCapabilities } from '../capabilities-adapter.js'; import { listsExitAdapter, @@ -182,6 +187,15 @@ function makeTextEditor( bold: { create: vi.fn(() => ({ type: 'bold' })), }, + italic: { + create: vi.fn(() => ({ type: 'italic' })), + }, + underline: { + create: vi.fn(() => ({ type: 'underline' })), + }, + strike: { + create: vi.fn(() => ({ type: 'strike' })), + }, [TrackFormatMarkName]: { create: vi.fn(() => ({ type: TrackFormatMarkName })), }, @@ -498,6 +512,84 @@ const mutationVectors: Partial> = { ); }, }, + 'format.italic': { + throwCase: () => { + const { editor } = makeTextEditor(); + return formatItalicAdapter( + editor, + { target: { kind: 'text', blockId: 'missing', range: { start: 0, end: 1 } } }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const { editor } = makeTextEditor(); + return formatItalicAdapter( + editor, + { target: { kind: 'text', blockId: 'p1', range: { start: 2, end: 2 } } }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const { editor } = makeTextEditor(); + return formatItalicAdapter( + editor, + { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } }, + { changeMode: 'direct' }, + ); + }, + }, + 'format.underline': { + throwCase: () => { + const { editor } = makeTextEditor(); + return formatUnderlineAdapter( + editor, + { target: { kind: 'text', blockId: 'missing', range: { start: 0, end: 1 } } }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const { editor } = makeTextEditor(); + return formatUnderlineAdapter( + editor, + { target: { kind: 'text', blockId: 'p1', range: { start: 2, end: 2 } } }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const { editor } = makeTextEditor(); + return formatUnderlineAdapter( + editor, + { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } }, + { changeMode: 'direct' }, + ); + }, + }, + 'format.strikethrough': { + throwCase: () => { + const { editor } = makeTextEditor(); + return formatStrikethroughAdapter( + editor, + { target: { kind: 'text', blockId: 'missing', range: { start: 0, end: 1 } } }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const { editor } = makeTextEditor(); + return formatStrikethroughAdapter( + editor, + { target: { kind: 'text', blockId: 'p1', range: { start: 2, end: 2 } } }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const { editor } = makeTextEditor(); + return formatStrikethroughAdapter( + editor, + { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } }, + { changeMode: 'direct' }, + ); + }, + }, 'create.paragraph': { throwCase: () => { const { editor } = makeTextEditor('Hello', { commands: { insertParagraphAt: undefined } }); @@ -917,6 +1009,39 @@ const dryRunVectors: Partial unknown>> = { expect(tr.addMark).not.toHaveBeenCalled(); return result; }, + 'format.italic': () => { + const { editor, dispatch, tr } = makeTextEditor(); + const result = formatItalicAdapter( + editor, + { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + expect(tr.addMark).not.toHaveBeenCalled(); + return result; + }, + 'format.underline': () => { + const { editor, dispatch, tr } = makeTextEditor(); + const result = formatUnderlineAdapter( + editor, + { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + expect(tr.addMark).not.toHaveBeenCalled(); + return result; + }, + 'format.strikethrough': () => { + const { editor, dispatch, tr } = makeTextEditor(); + const result = formatStrikethroughAdapter( + editor, + { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + expect(tr.addMark).not.toHaveBeenCalled(); + return result; + }, 'create.paragraph': () => { const insertParagraphAt = vi.fn(() => true); const { editor } = makeTextEditor('Hello', { commands: { insertParagraphAt } }); 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 bacb4660b..9a2e7ce32 100644 --- a/packages/super-editor/src/document-api-adapters/assemble-adapters.ts +++ b/packages/super-editor/src/document-api-adapters/assemble-adapters.ts @@ -7,7 +7,12 @@ import { infoAdapter } from './info-adapter.js'; import { getDocumentApiCapabilities } from './capabilities-adapter.js'; import { createCommentsAdapter } from './comments-adapter.js'; import { writeAdapter } from './write-adapter.js'; -import { formatBoldAdapter } from './format-adapter.js'; +import { + formatBoldAdapter, + formatItalicAdapter, + formatUnderlineAdapter, + formatStrikethroughAdapter, +} from './format-adapter.js'; import { trackChangesListAdapter, trackChangesGetAdapter, @@ -58,6 +63,9 @@ export function assembleDocumentApiAdapters(editor: Editor): DocumentApiAdapters }, format: { bold: (input, options) => formatBoldAdapter(editor, input, options), + italic: (input, options) => formatItalicAdapter(editor, input, options), + underline: (input, options) => formatUnderlineAdapter(editor, input, options), + strikethrough: (input, options) => formatStrikethroughAdapter(editor, input, options), }, trackChanges: { list: (input) => trackChangesListAdapter(editor, input), diff --git a/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts b/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts index 5ebfa8976..dd8b7427f 100644 --- a/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts +++ b/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts @@ -50,17 +50,25 @@ function hasAllCommands(editor: Editor, operationId: OperationId): boolean { return required.every((command) => hasCommand(editor, command)); } -function hasBoldCapability(editor: Editor): boolean { - return Boolean(editor.schema?.marks?.bold); +function hasMarkCapability(editor: Editor, markName: string): boolean { + return Boolean(editor.schema?.marks?.[markName]); } +/** Map from format operation IDs to their editor mark names. */ +const FORMAT_MARK_MAP: Partial> = { + 'format.bold': 'bold', + 'format.italic': 'italic', + 'format.underline': 'underline', + 'format.strikethrough': 'strike', +}; + function hasTrackedModeCapability(editor: Editor, operationId: OperationId): boolean { if (!hasCommand(editor, 'insertTrackedChange')) return false; // ensureTrackedCapability (mutation-helpers.ts) requires editor.options.user; // 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; - if (operationId === 'format.bold') { + if (FORMAT_MARK_MAP[operationId]) { return Boolean(editor.schema?.marks?.[TrackFormatMarkName]); } return true; @@ -98,15 +106,16 @@ function pushReason(reasons: CapabilityReasonCode[], reason: CapabilityReasonCod } function isOperationAvailable(editor: Editor, operationId: OperationId): boolean { - if (operationId === 'format.bold') { - return hasBoldCapability(editor); + const markName = FORMAT_MARK_MAP[operationId]; + if (markName) { + return hasMarkCapability(editor, markName); } return hasAllCommands(editor, operationId); } function isCommandBackedAvailability(operationId: OperationId): boolean { - return operationId !== 'format.bold'; + return !FORMAT_MARK_MAP[operationId]; } function buildOperationCapabilities(editor: Editor): DocumentApiCapabilities['operations'] { diff --git a/packages/super-editor/src/document-api-adapters/format-adapter.test.ts b/packages/super-editor/src/document-api-adapters/format-adapter.test.ts index d3ed0518f..1a01af912 100644 --- a/packages/super-editor/src/document-api-adapters/format-adapter.test.ts +++ b/packages/super-editor/src/document-api-adapters/format-adapter.test.ts @@ -2,7 +2,12 @@ import { describe, expect, it, vi } from 'vitest'; import type { Node as ProseMirrorNode } from 'prosemirror-model'; import type { Editor } from '../core/Editor.js'; import { TrackFormatMarkName } from '../extensions/track-changes/constants.js'; -import { formatBoldAdapter } from './format-adapter.js'; +import { + formatBoldAdapter, + formatItalicAdapter, + formatUnderlineAdapter, + formatStrikethroughAdapter, +} from './format-adapter.js'; type NodeOptions = { attrs?: Record; @@ -100,6 +105,15 @@ function makeEditor( bold: { create: vi.fn(() => ({ type: 'bold' })), }, + italic: { + create: vi.fn(() => ({ type: 'italic' })), + }, + underline: { + create: vi.fn(() => ({ type: 'underline' })), + }, + strike: { + create: vi.fn(() => ({ type: 'strike' })), + }, [TrackFormatMarkName]: { create: vi.fn(() => ({ type: TrackFormatMarkName })), }, @@ -115,18 +129,18 @@ function makeEditor( return { editor, dispatch, insertTrackedChange, textBetween, tr }; } +const TARGET = { kind: 'text' as const, blockId: 'p1', range: { start: 0, end: 5 } }; +const COLLAPSED_TARGET = { kind: 'text' as const, blockId: 'p1', range: { start: 2, end: 2 } }; +const MISSING_TARGET = { kind: 'text' as const, blockId: 'missing', range: { start: 0, end: 5 } }; + describe('formatBoldAdapter', () => { it('applies direct bold formatting', () => { const { editor, dispatch, tr } = makeEditor(); - const receipt = formatBoldAdapter( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } }, - { changeMode: 'direct' }, - ); + const receipt = formatBoldAdapter(editor, { target: TARGET }, { changeMode: 'direct' }); expect(receipt.success).toBe(true); expect(receipt.resolution).toMatchObject({ - target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } }, + target: TARGET, range: { from: 1, to: 6 }, text: 'Hello', }); @@ -137,11 +151,7 @@ describe('formatBoldAdapter', () => { it('sets skipTrackChanges meta in direct mode to preserve operation-scoped semantics', () => { const { editor, tr } = makeEditor(); - const receipt = formatBoldAdapter( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } }, - { changeMode: 'direct' }, - ); + const receipt = formatBoldAdapter(editor, { target: TARGET }, { changeMode: 'direct' }); expect(receipt.success).toBe(true); expect(tr.setMeta).toHaveBeenCalledWith('skipTrackChanges', true); @@ -150,11 +160,7 @@ describe('formatBoldAdapter', () => { it('sets forceTrackChanges meta in tracked mode', () => { const { editor, tr } = makeEditor('Hello', { user: { name: 'Test' } }); - const receipt = formatBoldAdapter( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } }, - { changeMode: 'tracked' }, - ); + const receipt = formatBoldAdapter(editor, { target: TARGET }, { changeMode: 'tracked' }); expect(receipt.success).toBe(true); expect(tr.setMeta).toHaveBeenCalledWith('forceTrackChanges', true); @@ -162,27 +168,17 @@ describe('formatBoldAdapter', () => { it('throws when target cannot be resolved', () => { const { editor } = makeEditor(); - expect(() => - formatBoldAdapter( - editor, - { target: { kind: 'text', blockId: 'missing', range: { start: 0, end: 5 } } }, - { changeMode: 'direct' }, - ), - ).toThrow('Format target could not be resolved.'); + expect(() => formatBoldAdapter(editor, { target: MISSING_TARGET }, { changeMode: 'direct' })).toThrow( + 'Format target could not be resolved.', + ); }); it('returns INVALID_TARGET for collapsed target ranges', () => { const { editor } = makeEditor(); - const receipt = formatBoldAdapter( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 2, end: 2 } } }, - { changeMode: 'direct' }, - ); + const receipt = formatBoldAdapter(editor, { target: COLLAPSED_TARGET }, { changeMode: 'direct' }); expect(receipt.success).toBe(false); - expect(receipt.failure).toMatchObject({ - code: 'INVALID_TARGET', - }); + expect(receipt.failure).toMatchObject({ code: 'INVALID_TARGET' }); expect(receipt.resolution.range).toEqual({ from: 3, to: 3 }); }); @@ -190,35 +186,23 @@ describe('formatBoldAdapter', () => { const { editor } = makeEditor(); delete (editor.schema?.marks as Record)?.bold; - expect(() => - formatBoldAdapter( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } }, - { changeMode: 'direct' }, - ), - ).toThrow('requires the "bold" mark'); + expect(() => formatBoldAdapter(editor, { target: TARGET }, { changeMode: 'direct' })).toThrow( + 'requires the "bold" mark', + ); }); it('throws when tracked format capability is unavailable', () => { const { editor } = makeEditor(); delete (editor.commands as Record)?.insertTrackedChange; - expect(() => - formatBoldAdapter( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } }, - { changeMode: 'tracked' }, - ), - ).toThrow('requires the insertTrackedChange command'); + expect(() => formatBoldAdapter(editor, { target: TARGET }, { changeMode: 'tracked' })).toThrow( + 'requires the insertTrackedChange command', + ); }); it('supports direct dry-run without building a transaction', () => { const { editor, dispatch, tr } = makeEditor(); - const receipt = formatBoldAdapter( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } }, - { changeMode: 'direct', dryRun: true }, - ); + const receipt = formatBoldAdapter(editor, { target: TARGET }, { changeMode: 'direct', dryRun: true }); expect(receipt.success).toBe(true); expect(receipt.resolution.range).toEqual({ from: 1, to: 6 }); @@ -228,11 +212,7 @@ describe('formatBoldAdapter', () => { it('supports tracked dry-run without building a transaction', () => { const { editor, dispatch, tr } = makeEditor('Hello', { user: { name: 'Test' } }); - const receipt = formatBoldAdapter( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } }, - { changeMode: 'tracked', dryRun: true }, - ); + const receipt = formatBoldAdapter(editor, { target: TARGET }, { changeMode: 'tracked', dryRun: true }); expect(receipt.success).toBe(true); expect(receipt.resolution.range).toEqual({ from: 1, to: 6 }); @@ -244,18 +224,10 @@ describe('formatBoldAdapter', () => { it('keeps direct and tracked bold operations deterministic for the same target', () => { const { editor, tr } = makeEditor('Hello', { user: { name: 'Test' } }); - const direct = formatBoldAdapter( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } }, - { changeMode: 'direct' }, - ); + const direct = formatBoldAdapter(editor, { target: TARGET }, { changeMode: 'direct' }); expect(direct.success).toBe(true); - const tracked = formatBoldAdapter( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } }, - { changeMode: 'tracked' }, - ); + const tracked = formatBoldAdapter(editor, { target: TARGET }, { changeMode: 'tracked' }); expect(tracked.success).toBe(true); expect(tr.setMeta).toHaveBeenCalledWith('forceTrackChanges', true); }); @@ -263,24 +235,154 @@ describe('formatBoldAdapter', () => { it('throws CAPABILITY_UNAVAILABLE for tracked dry-run without a configured user', () => { const { editor } = makeEditor(); - expect(() => - formatBoldAdapter( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } }, - { changeMode: 'tracked', dryRun: true }, - ), - ).toThrow('requires a user to be configured'); + expect(() => formatBoldAdapter(editor, { target: TARGET }, { changeMode: 'tracked', dryRun: true })).toThrow( + 'requires a user to be configured', + ); }); it('throws same error for tracked non-dry-run without a configured user', () => { const { editor } = makeEditor(); - expect(() => - formatBoldAdapter( - editor, - { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } }, - { changeMode: 'tracked' }, - ), - ).toThrow('requires a user to be configured'); + expect(() => formatBoldAdapter(editor, { target: TARGET }, { changeMode: 'tracked' })).toThrow( + 'requires a user to be configured', + ); + }); +}); + +describe('formatItalicAdapter', () => { + it('applies direct italic formatting', () => { + const { editor, dispatch, tr } = makeEditor(); + const receipt = formatItalicAdapter(editor, { target: TARGET }, { changeMode: 'direct' }); + + expect(receipt.success).toBe(true); + expect(receipt.resolution).toMatchObject({ target: TARGET, range: { from: 1, to: 6 }, text: 'Hello' }); + expect(tr.addMark).toHaveBeenCalledTimes(1); + expect(dispatch).toHaveBeenCalledTimes(1); + }); + + it('returns INVALID_TARGET for collapsed target ranges', () => { + const { editor } = makeEditor(); + const receipt = formatItalicAdapter(editor, { target: COLLAPSED_TARGET }, { changeMode: 'direct' }); + + expect(receipt.success).toBe(false); + expect(receipt.failure).toMatchObject({ code: 'INVALID_TARGET' }); + }); + + it('throws when italic mark is unavailable', () => { + const { editor } = makeEditor(); + delete (editor.schema?.marks as Record)?.italic; + + expect(() => formatItalicAdapter(editor, { target: TARGET }, { changeMode: 'direct' })).toThrow( + 'requires the "italic" mark', + ); + }); + + it('supports dry-run without building a transaction', () => { + const { editor, dispatch, tr } = makeEditor(); + const receipt = formatItalicAdapter(editor, { target: TARGET }, { changeMode: 'direct', dryRun: true }); + + expect(receipt.success).toBe(true); + expect(tr.addMark).not.toHaveBeenCalled(); + expect(dispatch).not.toHaveBeenCalled(); + }); + + it('supports tracked mode', () => { + const { editor, tr } = makeEditor('Hello', { user: { name: 'Test' } }); + const receipt = formatItalicAdapter(editor, { target: TARGET }, { changeMode: 'tracked' }); + + expect(receipt.success).toBe(true); + expect(tr.setMeta).toHaveBeenCalledWith('forceTrackChanges', true); + }); +}); + +describe('formatUnderlineAdapter', () => { + it('applies direct underline formatting', () => { + const { editor, dispatch, tr } = makeEditor(); + const receipt = formatUnderlineAdapter(editor, { target: TARGET }, { changeMode: 'direct' }); + + expect(receipt.success).toBe(true); + expect(receipt.resolution).toMatchObject({ target: TARGET, range: { from: 1, to: 6 }, text: 'Hello' }); + expect(tr.addMark).toHaveBeenCalledTimes(1); + expect(dispatch).toHaveBeenCalledTimes(1); + }); + + it('returns INVALID_TARGET for collapsed target ranges', () => { + const { editor } = makeEditor(); + const receipt = formatUnderlineAdapter(editor, { target: COLLAPSED_TARGET }, { changeMode: 'direct' }); + + expect(receipt.success).toBe(false); + expect(receipt.failure).toMatchObject({ code: 'INVALID_TARGET' }); + }); + + it('throws when underline mark is unavailable', () => { + const { editor } = makeEditor(); + delete (editor.schema?.marks as Record)?.underline; + + expect(() => formatUnderlineAdapter(editor, { target: TARGET }, { changeMode: 'direct' })).toThrow( + 'requires the "underline" mark', + ); + }); + + it('supports dry-run without building a transaction', () => { + const { editor, dispatch, tr } = makeEditor(); + const receipt = formatUnderlineAdapter(editor, { target: TARGET }, { changeMode: 'direct', dryRun: true }); + + expect(receipt.success).toBe(true); + expect(tr.addMark).not.toHaveBeenCalled(); + expect(dispatch).not.toHaveBeenCalled(); + }); + + it('supports tracked mode', () => { + const { editor, tr } = makeEditor('Hello', { user: { name: 'Test' } }); + const receipt = formatUnderlineAdapter(editor, { target: TARGET }, { changeMode: 'tracked' }); + + expect(receipt.success).toBe(true); + expect(tr.setMeta).toHaveBeenCalledWith('forceTrackChanges', true); + }); +}); + +describe('formatStrikethroughAdapter', () => { + it('applies direct strikethrough formatting using the "strike" mark', () => { + const { editor, dispatch, tr } = makeEditor(); + const receipt = formatStrikethroughAdapter(editor, { target: TARGET }, { changeMode: 'direct' }); + + expect(receipt.success).toBe(true); + expect(receipt.resolution).toMatchObject({ target: TARGET, range: { from: 1, to: 6 }, text: 'Hello' }); + expect(tr.addMark).toHaveBeenCalledTimes(1); + expect(dispatch).toHaveBeenCalledTimes(1); + }); + + it('returns INVALID_TARGET for collapsed target ranges', () => { + const { editor } = makeEditor(); + const receipt = formatStrikethroughAdapter(editor, { target: COLLAPSED_TARGET }, { changeMode: 'direct' }); + + expect(receipt.success).toBe(false); + expect(receipt.failure).toMatchObject({ code: 'INVALID_TARGET' }); + }); + + it('throws when strike mark is unavailable', () => { + const { editor } = makeEditor(); + delete (editor.schema?.marks as Record)?.strike; + + expect(() => formatStrikethroughAdapter(editor, { target: TARGET }, { changeMode: 'direct' })).toThrow( + 'requires the "strike" mark', + ); + }); + + it('supports dry-run without building a transaction', () => { + const { editor, dispatch, tr } = makeEditor(); + const receipt = formatStrikethroughAdapter(editor, { target: TARGET }, { changeMode: 'direct', dryRun: true }); + + expect(receipt.success).toBe(true); + expect(tr.addMark).not.toHaveBeenCalled(); + expect(dispatch).not.toHaveBeenCalled(); + }); + + it('supports tracked mode', () => { + const { editor, tr } = makeEditor('Hello', { user: { name: 'Test' } }); + const receipt = formatStrikethroughAdapter(editor, { target: TARGET }, { changeMode: 'tracked' }); + + expect(receipt.success).toBe(true); + expect(tr.setMeta).toHaveBeenCalledWith('forceTrackChanges', true); }); }); diff --git a/packages/super-editor/src/document-api-adapters/format-adapter.ts b/packages/super-editor/src/document-api-adapters/format-adapter.ts index 45673ef16..e0af67786 100644 --- a/packages/super-editor/src/document-api-adapters/format-adapter.ts +++ b/packages/super-editor/src/document-api-adapters/format-adapter.ts @@ -1,5 +1,13 @@ import type { Editor } from '../core/Editor.js'; -import type { FormatBoldInput, MutationOptions, TextMutationReceipt } from '@superdoc/document-api'; +import type { + FormatBoldInput, + FormatItalicInput, + FormatUnderlineInput, + FormatStrikethroughInput, + MutationOptions, + TextAddress, + TextMutationReceipt, +} from '@superdoc/document-api'; import { TrackFormatMarkName } from '../extensions/track-changes/constants.js'; import { DocumentApiAdapterError } from './errors.js'; import { requireSchemaMark, ensureTrackedCapability } from './helpers/mutation-helpers.js'; @@ -7,9 +15,32 @@ import { applyDirectMutationMeta, applyTrackedMutationMeta } from './helpers/tra import { resolveTextTarget } from './helpers/adapter-utils.js'; import { buildTextMutationResolution, readTextAtResolvedRange } from './helpers/text-mutation-resolution.js'; -export function formatBoldAdapter( +/** Maps each format operation to the display label used in failure messages. */ +const FORMAT_OPERATION_LABEL = { + 'format.bold': 'Bold', + 'format.italic': 'Italic', + 'format.underline': 'Underline', + 'format.strikethrough': 'Strikethrough', +} as const; + +type FormatOperationId = keyof typeof FORMAT_OPERATION_LABEL; +type FormatMarkName = 'bold' | 'italic' | 'underline' | 'strike'; +type FormatOperationInput = { target: TextAddress }; + +/** + * Shared adapter logic for toggle-mark format operations. + * + * Every format.* operation (bold, italic, underline, strikethrough) follows the + * same sequence: resolve target, build resolution, validate non-collapsed range, + * look up mark, check tracked capability, short-circuit on dryRun, dispatch. + * + * The only thing that varies is the editor mark name and the operation ID. + */ +function formatMarkAdapter( editor: Editor, - input: FormatBoldInput, + markName: FormatMarkName, + operationId: FormatOperationId, + input: FormatOperationInput, options?: MutationOptions, ): TextMutationReceipt { const range = resolveTextTarget(editor, input.target); @@ -27,30 +58,63 @@ export function formatBoldAdapter( }); if (range.from === range.to) { + const label = FORMAT_OPERATION_LABEL[operationId]; return { success: false, resolution, failure: { code: 'INVALID_TARGET', - message: 'Bold formatting requires a non-collapsed target range.', + message: `${label} formatting requires a non-collapsed target range.`, }, }; } - const boldMark = requireSchemaMark(editor, 'bold', 'format.bold'); + const mark = requireSchemaMark(editor, markName, operationId); const mode = options?.changeMode ?? 'direct'; if (mode === 'tracked') - ensureTrackedCapability(editor, { operation: 'format.bold', requireMarks: [TrackFormatMarkName] }); + ensureTrackedCapability(editor, { operation: operationId, requireMarks: [TrackFormatMarkName] }); if (options?.dryRun) { return { success: true, resolution }; } - const tr = editor.state.tr.addMark(range.from, range.to, boldMark.create()); + const tr = editor.state.tr.addMark(range.from, range.to, mark.create()); if (mode === 'tracked') applyTrackedMutationMeta(tr); else applyDirectMutationMeta(tr); editor.dispatch(tr); return { success: true, resolution }; } + +export function formatBoldAdapter( + editor: Editor, + input: FormatBoldInput, + options?: MutationOptions, +): TextMutationReceipt { + return formatMarkAdapter(editor, 'bold', 'format.bold', input, options); +} + +export function formatItalicAdapter( + editor: Editor, + input: FormatItalicInput, + options?: MutationOptions, +): TextMutationReceipt { + return formatMarkAdapter(editor, 'italic', 'format.italic', input, options); +} + +export function formatUnderlineAdapter( + editor: Editor, + input: FormatUnderlineInput, + options?: MutationOptions, +): TextMutationReceipt { + return formatMarkAdapter(editor, 'underline', 'format.underline', input, options); +} + +export function formatStrikethroughAdapter( + editor: Editor, + input: FormatStrikethroughInput, + options?: MutationOptions, +): TextMutationReceipt { + return formatMarkAdapter(editor, 'strike', 'format.strikethrough', input, options); +} diff --git a/packages/super-editor/src/document-api-adapters/index.ts b/packages/super-editor/src/document-api-adapters/index.ts index 753c919c5..118952ab3 100644 --- a/packages/super-editor/src/document-api-adapters/index.ts +++ b/packages/super-editor/src/document-api-adapters/index.ts @@ -16,7 +16,12 @@ import { getDocumentApiCapabilities } from './capabilities-adapter.js'; import { createCommentsAdapter } from './comments-adapter.js'; import { createParagraphAdapter } from './create-adapter.js'; import { findAdapter } from './find-adapter.js'; -import { formatBoldAdapter } from './format-adapter.js'; +import { + formatBoldAdapter, + formatItalicAdapter, + formatUnderlineAdapter, + formatStrikethroughAdapter, +} from './format-adapter.js'; import { getNodeAdapter, getNodeByIdAdapter } from './get-node-adapter.js'; import { getTextAdapter } from './get-text-adapter.js'; import { infoAdapter } from './info-adapter.js'; @@ -71,6 +76,9 @@ export function getDocumentApiAdapters(editor: Editor): DocumentApiAdapters { }, format: { bold: (input, options) => formatBoldAdapter(editor, input, options), + italic: (input, options) => formatItalicAdapter(editor, input, options), + underline: (input, options) => formatUnderlineAdapter(editor, input, options), + strikethrough: (input, options) => formatStrikethroughAdapter(editor, input, options), }, trackChanges: { list: (query) => trackChangesListAdapter(editor, query), From 39ff6acf4ec3c8dfa00f79ffaee17e53503cabed Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Tue, 17 Feb 2026 19:40:47 -0800 Subject: [PATCH 2/5] chore: disable telemetry in doc test runner --- apps/docs/__tests__/doctest.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/docs/__tests__/doctest.test.ts b/apps/docs/__tests__/doctest.test.ts index 92ef2ff9b..dc96d2e9e 100644 --- a/apps/docs/__tests__/doctest.test.ts +++ b/apps/docs/__tests__/doctest.test.ts @@ -65,6 +65,7 @@ for (const [file, fileExamples] of byFile) { const editor = await Editor.open(Buffer.from(fixtureBuffer), { extensions: getStarterExtensions(), suppressDefaultDocxStyles: true, + telemetry: { enabled: false }, }); try { From 376b405a7b1e3971324d558fd25bbfd613c18c0c Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Tue, 17 Feb 2026 21:16:18 -0800 Subject: [PATCH 3/5] feat(document-api): add create.heading operation --- apps/docs/document-api/overview.mdx | 3 +- .../reference/_generated-manifest.json | 5 +- .../reference/capabilities/get.mdx | 33 ++ .../document-api/reference/create/heading.mdx | 427 ++++++++++++++++++ .../document-api/reference/create/index.mdx | 1 + apps/docs/document-api/reference/index.mdx | 3 +- .../generated/agent/compatibility-hints.json | 11 +- .../generated/agent/remediation-map.json | 12 +- .../generated/agent/workflow-playbooks.json | 2 +- .../manifests/document-api-tools.json | 334 +++++++++++++- .../schemas/document-api-contract.json | 336 +++++++++++++- .../src/contract/operation-definitions.ts | 12 + .../src/contract/operation-registry.ts | 8 +- packages/document-api/src/contract/schemas.ts | 69 +++ packages/document-api/src/create/create.ts | 30 +- packages/document-api/src/index.test.ts | 40 ++ packages/document-api/src/index.ts | 6 +- .../document-api/src/invoke/invoke.test.ts | 14 + packages/document-api/src/invoke/invoke.ts | 1 + .../document-api/src/types/create.types.ts | 24 + .../src/core/commands/core-command-map.d.ts | 1 + .../super-editor/src/core/commands/index.js | 1 + .../src/core/commands/insertHeadingAt.js | 50 ++ .../src/core/commands/insertHeadingAt.test.js | 217 +++++++++ .../contract-conformance.test.ts | 41 +- .../assemble-adapters.test.ts | 2 + .../assemble-adapters.ts | 3 +- .../capabilities-adapter.test.ts | 20 + .../capabilities-adapter.ts | 1 + .../create-adapter.test.ts | 302 ++++++++++++- .../document-api-adapters/create-adapter.ts | 161 +++++++ .../src/document-api-adapters/index.ts | 3 +- 32 files changed, 2157 insertions(+), 16 deletions(-) create mode 100644 apps/docs/document-api/reference/create/heading.mdx create mode 100644 packages/super-editor/src/core/commands/insertHeadingAt.js create mode 100644 packages/super-editor/src/core/commands/insertHeadingAt.test.js diff --git a/apps/docs/document-api/overview.mdx b/apps/docs/document-api/overview.mdx index 820d4a5e0..3de0fc33a 100644 --- a/apps/docs/document-api/overview.mdx +++ b/apps/docs/document-api/overview.mdx @@ -31,7 +31,7 @@ Use the tables below to see what operations are available and where each one is | --- | --- | --- | | Core | 8 | [Reference](/document-api/reference/core/index) | | Capabilities | 1 | [Reference](/document-api/reference/capabilities/index) | -| Create | 1 | [Reference](/document-api/reference/create/index) | +| Create | 2 | [Reference](/document-api/reference/create/index) | | Format | 4 | [Reference](/document-api/reference/format/index) | | Lists | 8 | [Reference](/document-api/reference/lists/index) | | Comments | 11 | [Reference](/document-api/reference/comments/index) | @@ -52,6 +52,7 @@ Use the tables below to see what operations are available and where each one is | `editor.doc.format.underline(...)` | [`format.underline`](/document-api/reference/format/underline) | | `editor.doc.format.strikethrough(...)` | [`format.strikethrough`](/document-api/reference/format/strikethrough) | | `editor.doc.create.paragraph(...)` | [`create.paragraph`](/document-api/reference/create/paragraph) | +| `editor.doc.create.heading(...)` | [`create.heading`](/document-api/reference/create/heading) | | `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 4d73ade2a..59d186bdd 100644 --- a/apps/docs/document-api/reference/_generated-manifest.json +++ b/apps/docs/document-api/reference/_generated-manifest.json @@ -16,6 +16,7 @@ "apps/docs/document-api/reference/comments/set-active.mdx", "apps/docs/document-api/reference/comments/set-internal.mdx", "apps/docs/document-api/reference/core/index.mdx", + "apps/docs/document-api/reference/create/heading.mdx", "apps/docs/document-api/reference/create/index.mdx", "apps/docs/document-api/reference/create/paragraph.mdx", "apps/docs/document-api/reference/delete.mdx", @@ -65,7 +66,7 @@ }, { "key": "create", - "operationIds": ["create.paragraph"], + "operationIds": ["create.paragraph", "create.heading"], "pagePath": "apps/docs/document-api/reference/create/index.mdx", "title": "Create" }, @@ -123,5 +124,5 @@ } ], "marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}", - "sourceHash": "9f6796429b2a5ee8e11fe0cf9e482e59be289d0f0efed98f1c74766a6d1199d1" + "sourceHash": "3e7edc551feb80b42bfa59f928a0ce1f34ecb7cf71952d5a489107089105d543" } diff --git a/apps/docs/document-api/reference/capabilities/get.mdx b/apps/docs/document-api/reference/capabilities/get.mdx index 08931d97c..623e69313 100644 --- a/apps/docs/document-api/reference/capabilities/get.mdx +++ b/apps/docs/document-api/reference/capabilities/get.mdx @@ -537,6 +537,38 @@ description: Generated reference for capabilities.get ], "type": "object" }, + "create.heading": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, "create.paragraph": { "additionalProperties": false, "properties": { @@ -1416,6 +1448,7 @@ description: Generated reference for capabilities.get "format.underline", "format.strikethrough", "create.paragraph", + "create.heading", "lists.list", "lists.get", "lists.insert", diff --git a/apps/docs/document-api/reference/create/heading.mdx b/apps/docs/document-api/reference/create/heading.mdx new file mode 100644 index 000000000..44c315bac --- /dev/null +++ b/apps/docs/document-api/reference/create/heading.mdx @@ -0,0 +1,427 @@ +--- +title: create.heading +sidebarTitle: create.heading +description: Generated reference for create.heading +--- + +{/* 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: `create.heading` +- API member path: `editor.doc.create.heading(...)` +- Mutates document: `yes` +- Idempotency: `non-idempotent` +- Supports tracked mode: `yes` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `COMMAND_UNAVAILABLE` +- `TRACK_CHANGE_COMMAND_UNAVAILABLE` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- `INVALID_TARGET` + +## Input schema + +```json +{ + "additionalProperties": false, + "properties": { + "at": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "documentStart" + } + }, + "required": [ + "kind" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "documentEnd" + } + }, + "required": [ + "kind" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "before" + }, + "target": { + "additionalProperties": false, + "properties": { + "kind": { + "const": "block" + }, + "nodeId": { + "type": "string" + }, + "nodeType": { + "enum": [ + "paragraph", + "heading", + "listItem", + "table", + "tableRow", + "tableCell", + "image", + "sdt" + ] + } + }, + "required": [ + "kind", + "nodeType", + "nodeId" + ], + "type": "object" + } + }, + "required": [ + "kind", + "target" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "after" + }, + "target": { + "additionalProperties": false, + "properties": { + "kind": { + "const": "block" + }, + "nodeId": { + "type": "string" + }, + "nodeType": { + "enum": [ + "paragraph", + "heading", + "listItem", + "table", + "tableRow", + "tableCell", + "image", + "sdt" + ] + } + }, + "required": [ + "kind", + "nodeType", + "nodeId" + ], + "type": "object" + } + }, + "required": [ + "kind", + "target" + ], + "type": "object" + } + ] + }, + "level": { + "maximum": 6, + "minimum": 1, + "type": "integer" + }, + "text": { + "type": "string" + } + }, + "required": [ + "level" + ], + "type": "object" +} +``` + +## Output schema + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "heading": { + "additionalProperties": false, + "properties": { + "kind": { + "const": "block" + }, + "nodeId": { + "type": "string" + }, + "nodeType": { + "const": "heading" + } + }, + "required": [ + "kind", + "nodeType", + "nodeId" + ], + "type": "object" + }, + "insertionPoint": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "success": { + "const": true + }, + "trackedChangeRefs": { + "items": { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "success", + "heading", + "insertionPoint" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + +## Success schema + +```json +{ + "additionalProperties": false, + "properties": { + "heading": { + "additionalProperties": false, + "properties": { + "kind": { + "const": "block" + }, + "nodeId": { + "type": "string" + }, + "nodeType": { + "const": "heading" + } + }, + "required": [ + "kind", + "nodeType", + "nodeId" + ], + "type": "object" + }, + "insertionPoint": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } + }, + "required": [ + "kind", + "blockId", + "range" + ], + "type": "object" + }, + "success": { + "const": true + }, + "trackedChangeRefs": { + "items": { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": [ + "kind", + "entityType", + "entityId" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "success", + "heading", + "insertionPoint" + ], + "type": "object" +} +``` + +## Failure schema + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "INVALID_TARGET" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` diff --git a/apps/docs/document-api/reference/create/index.mdx b/apps/docs/document-api/reference/create/index.mdx index e93391383..d58ea4067 100644 --- a/apps/docs/document-api/reference/create/index.mdx +++ b/apps/docs/document-api/reference/create/index.mdx @@ -15,3 +15,4 @@ Structured creation helpers. | Operation | Member path | Mutates | Idempotency | Tracked | Dry run | | --- | --- | --- | --- | --- | --- | | [`create.paragraph`](./paragraph) | `create.paragraph` | Yes | `non-idempotent` | Yes | Yes | +| [`create.heading`](./heading) | `create.heading` | Yes | `non-idempotent` | Yes | Yes | diff --git a/apps/docs/document-api/reference/index.mdx b/apps/docs/document-api/reference/index.mdx index bcb00c2f8..3bd978c15 100644 --- a/apps/docs/document-api/reference/index.mdx +++ b/apps/docs/document-api/reference/index.mdx @@ -15,7 +15,7 @@ Document API is currently alpha and subject to breaking changes. | --- | --- | --- | | Core | 8 | [Open](./core/index) | | Capabilities | 1 | [Open](./capabilities/index) | -| Create | 1 | [Open](./create/index) | +| Create | 2 | [Open](./create/index) | | Format | 4 | [Open](./format/index) | | Lists | 8 | [Open](./lists/index) | | Comments | 11 | [Open](./comments/index) | @@ -38,6 +38,7 @@ Document API is currently alpha and subject to breaking changes. | [`format.underline`](./format/underline) | Format | `format.underline` | Yes | `conditional` | Yes | Yes | | [`format.strikethrough`](./format/strikethrough) | Format | `format.strikethrough` | Yes | `conditional` | Yes | Yes | | [`create.paragraph`](./create/paragraph) | Create | `create.paragraph` | Yes | `non-idempotent` | Yes | Yes | +| [`create.heading`](./create/heading) | Create | `create.heading` | Yes | `non-idempotent` | Yes | Yes | | [`lists.list`](./lists/list) | Lists | `lists.list` | No | `idempotent` | No | No | | [`lists.get`](./lists/get) | Lists | `lists.get` | No | `idempotent` | No | No | | [`lists.insert`](./lists/insert) | Lists | `lists.insert` | Yes | `non-idempotent` | Yes | Yes | diff --git a/packages/document-api/generated/agent/compatibility-hints.json b/packages/document-api/generated/agent/compatibility-hints.json index e1ad770bc..9bc2c2058 100644 --- a/packages/document-api/generated/agent/compatibility-hints.json +++ b/packages/document-api/generated/agent/compatibility-hints.json @@ -109,6 +109,15 @@ "supportsDryRun": false, "supportsTrackedMode": false }, + "create.heading": { + "deterministicTargetResolution": true, + "memberPath": "create.heading", + "mutates": true, + "postApplyThrowForbidden": true, + "requiresPreflightCapabilitiesCheck": true, + "supportsDryRun": true, + "supportsTrackedMode": true + }, "create.paragraph": { "deterministicTargetResolution": true, "memberPath": "create.paragraph", @@ -353,5 +362,5 @@ "supportsTrackedMode": false } }, - "sourceHash": "9f6796429b2a5ee8e11fe0cf9e482e59be289d0f0efed98f1c74766a6d1199d1" + "sourceHash": "3e7edc551feb80b42bfa59f928a0ce1f34ecb7cf71952d5a489107089105d543" } diff --git a/packages/document-api/generated/agent/remediation-map.json b/packages/document-api/generated/agent/remediation-map.json index b735bb04e..fa77e59dc 100644 --- a/packages/document-api/generated/agent/remediation-map.json +++ b/packages/document-api/generated/agent/remediation-map.json @@ -15,6 +15,7 @@ "comments.resolve", "comments.setActive", "comments.setInternal", + "create.heading", "create.paragraph", "delete", "format.bold", @@ -44,6 +45,7 @@ "comments.resolve", "comments.setActive", "comments.setInternal", + "create.heading", "create.paragraph", "delete", "format.bold", @@ -78,6 +80,7 @@ "comments.resolve", "comments.setActive", "comments.setInternal", + "create.heading", "create.paragraph", "format.bold", "format.italic", @@ -104,6 +107,7 @@ "comments.resolve", "comments.setActive", "comments.setInternal", + "create.heading", "create.paragraph", "format.bold", "format.italic", @@ -130,6 +134,7 @@ "comments.reply", "comments.setActive", "comments.setInternal", + "create.heading", "create.paragraph", "format.bold", "format.italic", @@ -150,6 +155,7 @@ "comments.reply", "comments.setActive", "comments.setInternal", + "create.heading", "create.paragraph", "format.bold", "format.italic", @@ -224,6 +230,7 @@ "comments.resolve", "comments.setActive", "comments.setInternal", + "create.heading", "create.paragraph", "delete", "format.bold", @@ -257,6 +264,7 @@ "comments.resolve", "comments.setActive", "comments.setInternal", + "create.heading", "create.paragraph", "delete", "format.bold", @@ -285,6 +293,7 @@ "message": "Verify track-changes support via capabilities.get before requesting tracked mode.", "nonAppliedOperations": [], "operations": [ + "create.heading", "create.paragraph", "delete", "format.bold", @@ -301,6 +310,7 @@ "replace" ], "preApplyOperations": [ + "create.heading", "create.paragraph", "delete", "format.bold", @@ -318,5 +328,5 @@ ] } ], - "sourceHash": "9f6796429b2a5ee8e11fe0cf9e482e59be289d0f0efed98f1c74766a6d1199d1" + "sourceHash": "3e7edc551feb80b42bfa59f928a0ce1f34ecb7cf71952d5a489107089105d543" } diff --git a/packages/document-api/generated/agent/workflow-playbooks.json b/packages/document-api/generated/agent/workflow-playbooks.json index 9a80a3aa6..55237d4e4 100644 --- a/packages/document-api/generated/agent/workflow-playbooks.json +++ b/packages/document-api/generated/agent/workflow-playbooks.json @@ -1,6 +1,6 @@ { "contractVersion": "0.1.0", - "sourceHash": "9f6796429b2a5ee8e11fe0cf9e482e59be289d0f0efed98f1c74766a6d1199d1", + "sourceHash": "3e7edc551feb80b42bfa59f928a0ce1f34ecb7cf71952d5a489107089105d543", "workflows": [ { "id": "find-mutate", diff --git a/packages/document-api/generated/manifests/document-api-tools.json b/packages/document-api/generated/manifests/document-api-tools.json index 44c1d20d7..a73c68238 100644 --- a/packages/document-api/generated/manifests/document-api-tools.json +++ b/packages/document-api/generated/manifests/document-api-tools.json @@ -2,7 +2,7 @@ "contractVersion": "0.1.0", "generatedAt": null, "sourceCommit": null, - "sourceHash": "9f6796429b2a5ee8e11fe0cf9e482e59be289d0f0efed98f1c74766a6d1199d1", + "sourceHash": "3e7edc551feb80b42bfa59f928a0ce1f34ecb7cf71952d5a489107089105d543", "tools": [ { "description": "Read Document API data via `find`.", @@ -5795,6 +5795,309 @@ "supportsDryRun": true, "supportsTrackedMode": true }, + { + "description": "Apply Document API mutation `create.heading`.", + "deterministicTargetResolution": true, + "failureSchema": { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": ["INVALID_TARGET"] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": ["success", "failure"], + "type": "object" + }, + "idempotency": "non-idempotent", + "inputSchema": { + "additionalProperties": false, + "properties": { + "at": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "documentStart" + } + }, + "required": ["kind"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "documentEnd" + } + }, + "required": ["kind"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "before" + }, + "target": { + "additionalProperties": false, + "properties": { + "kind": { + "const": "block" + }, + "nodeId": { + "type": "string" + }, + "nodeType": { + "enum": ["paragraph", "heading", "listItem", "table", "tableRow", "tableCell", "image", "sdt"] + } + }, + "required": ["kind", "nodeType", "nodeId"], + "type": "object" + } + }, + "required": ["kind", "target"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "after" + }, + "target": { + "additionalProperties": false, + "properties": { + "kind": { + "const": "block" + }, + "nodeId": { + "type": "string" + }, + "nodeType": { + "enum": ["paragraph", "heading", "listItem", "table", "tableRow", "tableCell", "image", "sdt"] + } + }, + "required": ["kind", "nodeType", "nodeId"], + "type": "object" + } + }, + "required": ["kind", "target"], + "type": "object" + } + ] + }, + "level": { + "maximum": 6, + "minimum": 1, + "type": "integer" + }, + "text": { + "type": "string" + } + }, + "required": ["level"], + "type": "object" + }, + "memberPath": "create.heading", + "mutates": true, + "name": "create.heading", + "outputSchema": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "heading": { + "additionalProperties": false, + "properties": { + "kind": { + "const": "block" + }, + "nodeId": { + "type": "string" + }, + "nodeType": { + "const": "heading" + } + }, + "required": ["kind", "nodeType", "nodeId"], + "type": "object" + }, + "insertionPoint": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "success": { + "const": true + }, + "trackedChangeRefs": { + "items": { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + "type": "array" + } + }, + "required": ["success", "heading", "insertionPoint"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": ["INVALID_TARGET"] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": ["success", "failure"], + "type": "object" + } + ] + }, + "possibleFailureCodes": ["INVALID_TARGET"], + "preApplyThrows": [ + "TARGET_NOT_FOUND", + "COMMAND_UNAVAILABLE", + "TRACK_CHANGE_COMMAND_UNAVAILABLE", + "CAPABILITY_UNAVAILABLE" + ], + "remediationHints": [], + "successSchema": { + "additionalProperties": false, + "properties": { + "heading": { + "additionalProperties": false, + "properties": { + "kind": { + "const": "block" + }, + "nodeId": { + "type": "string" + }, + "nodeType": { + "const": "heading" + } + }, + "required": ["kind", "nodeType", "nodeId"], + "type": "object" + }, + "insertionPoint": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "success": { + "const": true + }, + "trackedChangeRefs": { + "items": { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + "type": "array" + } + }, + "required": ["success", "heading", "insertionPoint"], + "type": "object" + }, + "supportsDryRun": true, + "supportsTrackedMode": true + }, { "description": "Read Document API data via `lists.list`.", "deterministicTargetResolution": true, @@ -11949,6 +12252,34 @@ "required": ["available", "tracked", "dryRun"], "type": "object" }, + "create.heading": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": ["available", "tracked", "dryRun"], + "type": "object" + }, "create.paragraph": { "additionalProperties": false, "properties": { @@ -12720,6 +13051,7 @@ "format.underline", "format.strikethrough", "create.paragraph", + "create.heading", "lists.list", "lists.get", "lists.insert", diff --git a/packages/document-api/generated/schemas/document-api-contract.json b/packages/document-api/generated/schemas/document-api-contract.json index 1fcec28ad..faf16ed93 100644 --- a/packages/document-api/generated/schemas/document-api-contract.json +++ b/packages/document-api/generated/schemas/document-api-contract.json @@ -459,6 +459,34 @@ "required": ["available", "tracked", "dryRun"], "type": "object" }, + "create.heading": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": ["available", "tracked", "dryRun"], + "type": "object" + }, "create.paragraph": { "additionalProperties": false, "properties": { @@ -1230,6 +1258,7 @@ "format.underline", "format.strikethrough", "create.paragraph", + "create.heading", "lists.list", "lists.get", "lists.insert", @@ -4339,6 +4368,311 @@ "type": "object" } }, + "create.heading": { + "failureSchema": { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": ["INVALID_TARGET"] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": ["success", "failure"], + "type": "object" + }, + "inputSchema": { + "additionalProperties": false, + "properties": { + "at": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "documentStart" + } + }, + "required": ["kind"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "documentEnd" + } + }, + "required": ["kind"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "before" + }, + "target": { + "additionalProperties": false, + "properties": { + "kind": { + "const": "block" + }, + "nodeId": { + "type": "string" + }, + "nodeType": { + "enum": ["paragraph", "heading", "listItem", "table", "tableRow", "tableCell", "image", "sdt"] + } + }, + "required": ["kind", "nodeType", "nodeId"], + "type": "object" + } + }, + "required": ["kind", "target"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "after" + }, + "target": { + "additionalProperties": false, + "properties": { + "kind": { + "const": "block" + }, + "nodeId": { + "type": "string" + }, + "nodeType": { + "enum": ["paragraph", "heading", "listItem", "table", "tableRow", "tableCell", "image", "sdt"] + } + }, + "required": ["kind", "nodeType", "nodeId"], + "type": "object" + } + }, + "required": ["kind", "target"], + "type": "object" + } + ] + }, + "level": { + "maximum": 6, + "minimum": 1, + "type": "integer" + }, + "text": { + "type": "string" + } + }, + "required": ["level"], + "type": "object" + }, + "memberPath": "create.heading", + "metadata": { + "deterministicTargetResolution": true, + "idempotency": "non-idempotent", + "mutates": true, + "possibleFailureCodes": ["INVALID_TARGET"], + "supportsDryRun": true, + "supportsTrackedMode": true, + "throws": { + "postApplyForbidden": true, + "preApply": [ + "TARGET_NOT_FOUND", + "COMMAND_UNAVAILABLE", + "TRACK_CHANGE_COMMAND_UNAVAILABLE", + "CAPABILITY_UNAVAILABLE" + ] + } + }, + "outputSchema": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "heading": { + "additionalProperties": false, + "properties": { + "kind": { + "const": "block" + }, + "nodeId": { + "type": "string" + }, + "nodeType": { + "const": "heading" + } + }, + "required": ["kind", "nodeType", "nodeId"], + "type": "object" + }, + "insertionPoint": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "success": { + "const": true + }, + "trackedChangeRefs": { + "items": { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + "type": "array" + } + }, + "required": ["success", "heading", "insertionPoint"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": ["INVALID_TARGET"] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": ["success", "failure"], + "type": "object" + } + ] + }, + "successSchema": { + "additionalProperties": false, + "properties": { + "heading": { + "additionalProperties": false, + "properties": { + "kind": { + "const": "block" + }, + "nodeId": { + "type": "string" + }, + "nodeType": { + "const": "heading" + } + }, + "required": ["kind", "nodeType", "nodeId"], + "type": "object" + }, + "insertionPoint": { + "additionalProperties": false, + "properties": { + "blockId": { + "type": "string" + }, + "kind": { + "const": "text" + }, + "range": { + "additionalProperties": false, + "properties": { + "end": { + "type": "integer" + }, + "start": { + "type": "integer" + } + }, + "required": ["start", "end"], + "type": "object" + } + }, + "required": ["kind", "blockId", "range"], + "type": "object" + }, + "success": { + "const": true + }, + "trackedChangeRefs": { + "items": { + "additionalProperties": false, + "properties": { + "entityId": { + "type": "string" + }, + "entityType": { + "const": "trackedChange" + }, + "kind": { + "const": "entity" + } + }, + "required": ["kind", "entityType", "entityId"], + "type": "object" + }, + "type": "array" + } + }, + "required": ["success", "heading", "insertionPoint"], + "type": "object" + } + }, "create.paragraph": { "failureSchema": { "additionalProperties": false, @@ -12838,5 +13172,5 @@ } }, "sourceCommit": null, - "sourceHash": "9f6796429b2a5ee8e11fe0cf9e482e59be289d0f0efed98f1c74766a6d1199d1" + "sourceHash": "3e7edc551feb80b42bfa59f928a0ce1f34ecb7cf71952d5a489107089105d543" } diff --git a/packages/document-api/src/contract/operation-definitions.ts b/packages/document-api/src/contract/operation-definitions.ts index 89714e48e..87d54c73c 100644 --- a/packages/document-api/src/contract/operation-definitions.ts +++ b/packages/document-api/src/contract/operation-definitions.ts @@ -253,6 +253,18 @@ export const OPERATION_DEFINITIONS = { referenceDocPath: 'create/paragraph.mdx', referenceGroup: 'create', }, + 'create.heading': { + memberPath: 'create.heading', + metadata: mutationOperation({ + idempotency: 'non-idempotent', + supportsDryRun: true, + supportsTrackedMode: true, + possibleFailureCodes: ['INVALID_TARGET'], + throws: T_NOT_FOUND_COMMAND_TRACKED, + }), + referenceDocPath: 'create/heading.mdx', + referenceGroup: 'create', + }, 'lists.list': { memberPath: 'lists.list', diff --git a/packages/document-api/src/contract/operation-registry.ts b/packages/document-api/src/contract/operation-registry.ts index cf0ca59fe..05c5d5efa 100644 --- a/packages/document-api/src/contract/operation-registry.ts +++ b/packages/document-api/src/contract/operation-registry.ts @@ -11,7 +11,12 @@ import type { OperationId } from './types.js'; import type { NodeAddress, NodeInfo, QueryResult, Selector, Query } from '../types/index.js'; import type { TextMutationReceipt, Receipt } from '../types/receipt.js'; import type { DocumentInfo } from '../types/info.types.js'; -import type { CreateParagraphInput, CreateParagraphResult } from '../types/create.types.js'; +import type { + CreateParagraphInput, + CreateParagraphResult, + CreateHeadingInput, + CreateHeadingResult, +} from '../types/create.types.js'; import type { FindOptions } from '../find/find.js'; import type { GetNodeByIdInput } from '../get-node/get-node.js'; @@ -84,6 +89,7 @@ export interface OperationRegistry { // --- create.* --- 'create.paragraph': { input: CreateParagraphInput; options: MutationOptions; output: CreateParagraphResult }; + 'create.heading': { input: CreateHeadingInput; options: MutationOptions; output: CreateHeadingResult }; // --- lists.* --- 'lists.list': { input: ListsListQuery | undefined; options: never; output: ListsListResult }; diff --git a/packages/document-api/src/contract/schemas.ts b/packages/document-api/src/contract/schemas.ts index 156ce9e3f..b643f4233 100644 --- a/packages/document-api/src/contract/schemas.ts +++ b/packages/document-api/src/contract/schemas.ts @@ -100,6 +100,15 @@ const paragraphAddressSchema = objectSchema( ['kind', 'nodeType', 'nodeId'], ); +const headingAddressSchema = objectSchema( + { + kind: { const: 'block' }, + nodeType: { const: 'heading' }, + nodeId: { type: 'string' }, + }, + ['kind', 'nodeType', 'nodeId'], +); + const listItemAddressSchema = objectSchema( { kind: { const: 'block' }, @@ -266,6 +275,34 @@ function createParagraphResultSchemaFor(operationId: OperationId): JsonSchema { }; } +const createHeadingSuccessSchema = objectSchema( + { + success: { const: true }, + heading: headingAddressSchema, + insertionPoint: textAddressSchema, + trackedChangeRefs: arraySchema(trackChangeRefSchema), + }, + ['success', 'heading', 'insertionPoint'], +); + +function createHeadingFailureSchemaFor(operationId: OperationId): JsonSchema { + return objectSchema( + { + success: { const: false }, + failure: receiptFailureSchemaFor(operationId), + }, + ['success', 'failure'], + ); +} + +function createHeadingResultSchemaFor(operationId: OperationId): JsonSchema { + return { + oneOf: [createHeadingSuccessSchema, createHeadingFailureSchemaFor(operationId)], + }; +} + +const headingLevelSchema: JsonSchema = { type: 'integer', minimum: 1, maximum: 6 }; + const listsInsertSuccessSchema = objectSchema( { success: { const: true }, @@ -716,6 +753,38 @@ const operationSchemas: Record = { success: createParagraphSuccessSchema, failure: createParagraphFailureSchemaFor('create.paragraph'), }, + 'create.heading': { + input: objectSchema( + { + level: headingLevelSchema, + at: { + oneOf: [ + objectSchema({ kind: { const: 'documentStart' } }, ['kind']), + objectSchema({ kind: { const: 'documentEnd' } }, ['kind']), + objectSchema( + { + kind: { const: 'before' }, + target: blockNodeAddressSchema, + }, + ['kind', 'target'], + ), + objectSchema( + { + kind: { const: 'after' }, + target: blockNodeAddressSchema, + }, + ['kind', 'target'], + ), + ], + }, + text: { type: 'string' }, + }, + ['level'], + ), + output: createHeadingResultSchemaFor('create.heading'), + success: createHeadingSuccessSchema, + failure: createHeadingFailureSchemaFor('create.heading'), + }, 'lists.list': { input: objectSchema({ within: blockNodeAddressSchema, diff --git a/packages/document-api/src/create/create.ts b/packages/document-api/src/create/create.ts index 7645c982c..5a73665f2 100644 --- a/packages/document-api/src/create/create.ts +++ b/packages/document-api/src/create/create.ts @@ -1,9 +1,17 @@ import type { MutationOptions } from '../write/write.js'; import { normalizeMutationOptions } from '../write/write.js'; -import type { CreateParagraphInput, CreateParagraphResult, ParagraphCreateLocation } from '../types/create.types.js'; +import type { + CreateParagraphInput, + CreateParagraphResult, + ParagraphCreateLocation, + CreateHeadingInput, + CreateHeadingResult, + HeadingCreateLocation, +} from '../types/create.types.js'; export interface CreateApi { paragraph(input: CreateParagraphInput, options?: MutationOptions): CreateParagraphResult; + heading(input: CreateHeadingInput, options?: MutationOptions): CreateHeadingResult; } export type CreateAdapter = CreateApi; @@ -26,3 +34,23 @@ export function executeCreateParagraph( ): CreateParagraphResult { return adapter.paragraph(normalizeCreateParagraphInput(input), normalizeMutationOptions(options)); } + +function normalizeHeadingCreateLocation(location?: HeadingCreateLocation): HeadingCreateLocation { + return location ?? { kind: 'documentEnd' }; +} + +export function normalizeCreateHeadingInput(input: CreateHeadingInput): CreateHeadingInput { + return { + level: input.level, + at: normalizeHeadingCreateLocation(input.at), + text: input.text ?? '', + }; +} + +export function executeCreateHeading( + adapter: CreateAdapter, + input: CreateHeadingInput, + options?: MutationOptions, +): CreateHeadingResult { + return adapter.heading(normalizeCreateHeadingInput(input), normalizeMutationOptions(options)); +} diff --git a/packages/document-api/src/index.test.ts b/packages/document-api/src/index.test.ts index 99f03554d..81082bae3 100644 --- a/packages/document-api/src/index.test.ts +++ b/packages/document-api/src/index.test.ts @@ -151,6 +151,11 @@ function makeCreateAdapter(): CreateAdapter { paragraph: { kind: 'block' as const, nodeType: 'paragraph' as const, nodeId: 'new-p' }, insertionPoint: { kind: 'text' as const, blockId: 'new-p', range: { start: 0, end: 0 } }, })), + heading: vi.fn(() => ({ + success: true as const, + heading: { kind: 'block' as const, nodeType: 'heading' as const, nodeId: 'new-h' }, + insertionPoint: { kind: 'text' as const, blockId: 'new-h', range: { start: 0, end: 0 } }, + })), }; } @@ -627,6 +632,41 @@ describe('createDocumentApi', () => { ); }); + it('delegates create.heading to the create adapter', () => { + const createAdpt = makeCreateAdapter(); + const api = createDocumentApi({ + find: makeFindAdapter(QUERY_RESULT), + getNode: makeGetNodeAdapter(PARAGRAPH_INFO), + getText: makeGetTextAdapter(), + info: makeInfoAdapter(), + comments: makeCommentsAdapter(), + write: makeWriteAdapter(), + format: makeFormatAdapter(), + trackChanges: makeTrackChangesAdapter(), + create: createAdpt, + lists: makeListsAdapter(), + }); + + const result = api.create.heading( + { + level: 2, + at: { kind: 'documentEnd' }, + text: 'Created heading', + }, + { changeMode: 'tracked' }, + ); + + expect(result.success).toBe(true); + expect(createAdpt.heading).toHaveBeenCalledWith( + { + level: 2, + at: { kind: 'documentEnd' }, + text: 'Created heading', + }, + { changeMode: 'tracked', dryRun: false }, + ); + }); + it('delegates lists namespace operations', () => { const listsAdpt = makeListsAdapter(); const api = createDocumentApi({ diff --git a/packages/document-api/src/index.ts b/packages/document-api/src/index.ts index eab8de602..0b9eb3de9 100644 --- a/packages/document-api/src/index.ts +++ b/packages/document-api/src/index.ts @@ -96,7 +96,8 @@ import { } from './lists/lists.js'; import { executeReplace, type ReplaceInput } from './replace/replace.js'; import type { CreateAdapter, CreateApi } from './create/create.js'; -import { executeCreateParagraph } from './create/create.js'; +import { executeCreateParagraph, executeCreateHeading } from './create/create.js'; +import type { CreateHeadingInput, CreateHeadingResult } from './types/create.types.js'; import type { TrackChangesAcceptAllInput, TrackChangesAcceptInput, @@ -411,6 +412,9 @@ export function createDocumentApi(adapters: DocumentApiAdapters): DocumentApi { paragraph(input: CreateParagraphInput, options?: MutationOptions): CreateParagraphResult { return executeCreateParagraph(adapters.create, input, options); }, + heading(input: CreateHeadingInput, options?: MutationOptions): CreateHeadingResult { + return executeCreateHeading(adapters.create, input, options); + }, }, capabilities, lists: { diff --git a/packages/document-api/src/invoke/invoke.test.ts b/packages/document-api/src/invoke/invoke.test.ts index 25e572a17..73e9fd97f 100644 --- a/packages/document-api/src/invoke/invoke.test.ts +++ b/packages/document-api/src/invoke/invoke.test.ts @@ -98,6 +98,11 @@ function makeAdapters() { paragraph: { kind: 'block' as const, nodeType: 'paragraph' as const, nodeId: 'new-p' }, insertionPoint: { kind: 'text' as const, blockId: 'new-p', range: { start: 0, end: 0 } }, })), + heading: vi.fn(() => ({ + success: true as const, + heading: { kind: 'block' as const, nodeType: 'heading' as const, nodeId: 'new-h' }, + insertionPoint: { kind: 'text' as const, blockId: 'new-h', range: { start: 0, end: 0 } }, + })), }; const listsAdapter: ListsAdapter = { list: vi.fn(() => ({ matches: [], total: 0, items: [] })), @@ -260,6 +265,15 @@ describe('invoke', () => { const invoked = api.invoke({ operationId: 'format.strikethrough', input }); expect(invoked).toEqual(direct); }); + + it('create.heading: invoke returns same result as direct call', () => { + const { adapters } = makeAdapters(); + const api = createDocumentApi(adapters); + const input = { level: 1 as const, at: { kind: 'documentEnd' as const }, text: 'Title' }; + const direct = api.create.heading(input); + const invoked = api.invoke({ operationId: 'create.heading', input }); + expect(invoked).toEqual(direct); + }); }); describe('error handling', () => { diff --git a/packages/document-api/src/invoke/invoke.ts b/packages/document-api/src/invoke/invoke.ts index ec11c056f..e64cb1cf6 100644 --- a/packages/document-api/src/invoke/invoke.ts +++ b/packages/document-api/src/invoke/invoke.ts @@ -52,6 +52,7 @@ export function buildDispatchTable(api: DocumentApi): TypedDispatchTable { // --- create.* --- 'create.paragraph': (input, options) => api.create.paragraph(input, options), + 'create.heading': (input, options) => api.create.heading(input, options), // --- lists.* --- 'lists.list': (input) => api.lists.list(input), diff --git a/packages/document-api/src/types/create.types.ts b/packages/document-api/src/types/create.types.ts index 3d1ce3132..797418115 100644 --- a/packages/document-api/src/types/create.types.ts +++ b/packages/document-api/src/types/create.types.ts @@ -26,3 +26,27 @@ export interface CreateParagraphFailureResult { } export type CreateParagraphResult = CreateParagraphSuccessResult | CreateParagraphFailureResult; + +export type HeadingCreateLocation = ParagraphCreateLocation; + +export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6; + +export interface CreateHeadingInput { + level: HeadingLevel; + at?: HeadingCreateLocation; + text?: string; +} + +export interface CreateHeadingSuccessResult { + success: true; + heading: BlockNodeAddress; + insertionPoint: TextAddress; + trackedChangeRefs?: ReceiptInsert[]; +} + +export interface CreateHeadingFailureResult { + success: false; + failure: ReceiptFailure; +} + +export type CreateHeadingResult = CreateHeadingSuccessResult | CreateHeadingFailureResult; diff --git a/packages/super-editor/src/core/commands/core-command-map.d.ts b/packages/super-editor/src/core/commands/core-command-map.d.ts index b71184b19..f6a9c939e 100644 --- a/packages/super-editor/src/core/commands/core-command-map.d.ts +++ b/packages/super-editor/src/core/commands/core-command-map.d.ts @@ -39,6 +39,7 @@ type CoreCommandNames = | 'insertContent' | 'insertContentAt' | 'insertParagraphAt' + | 'insertHeadingAt' | 'undoInputRule' | 'setSectionPageMarginsAtSelection' | 'toggleList' diff --git a/packages/super-editor/src/core/commands/index.js b/packages/super-editor/src/core/commands/index.js index 8b63ba863..a6ecff1a0 100644 --- a/packages/super-editor/src/core/commands/index.js +++ b/packages/super-editor/src/core/commands/index.js @@ -34,6 +34,7 @@ export * from './selectTextblockEnd.js'; export * from './insertContent.js'; export * from './insertContentAt.js'; export * from './insertParagraphAt.js'; +export * from './insertHeadingAt.js'; export * from './undoInputRule.js'; export * from './setBodyHeaderFooter.js'; export * from './setSectionHeaderFooterAtSelection.js'; diff --git a/packages/super-editor/src/core/commands/insertHeadingAt.js b/packages/super-editor/src/core/commands/insertHeadingAt.js new file mode 100644 index 000000000..7e3b956fc --- /dev/null +++ b/packages/super-editor/src/core/commands/insertHeadingAt.js @@ -0,0 +1,50 @@ +/** + * Insert a heading node at an absolute document position. + * + * Internally, headings are paragraph nodes with a heading styleId + * (`Heading1` through `Heading6`) set on `paragraphProperties`. + * + * Supports optional seed text, deterministic block id assignment, and + * operation-scoped tracked-change conversion via transaction meta. + * + * @param {{ pos: number; level: number; text?: string; sdBlockId?: string; tracked?: boolean }} options + * @returns {import('./types/index.js').Command} + */ +export const insertHeadingAt = + ({ pos, level, text = '', sdBlockId, tracked }) => + ({ state, dispatch }) => { + const paragraphType = state.schema.nodes.paragraph; + if (!paragraphType) return false; + if (!Number.isInteger(pos) || pos < 0 || pos > state.doc.content.size) return false; + if (!Number.isInteger(level) || level < 1 || level > 6) return false; + + const attrs = { + ...(sdBlockId ? { sdBlockId } : undefined), + paragraphProperties: { styleId: `Heading${level}` }, + }; + const normalizedText = typeof text === 'string' ? text : ''; + const textNode = normalizedText.length > 0 ? state.schema.text(normalizedText) : null; + + let paragraphNode; + try { + paragraphNode = + paragraphType.createAndFill(attrs, textNode ?? undefined) ?? + paragraphType.create(attrs, textNode ? [textNode] : undefined); + } catch { + return false; + } + + if (!paragraphNode) return false; + + try { + const tr = state.tr.insert(pos, paragraphNode); + if (!dispatch) return true; + tr.setMeta('inputType', 'programmatic'); + if (tracked === true) tr.setMeta('forceTrackChanges', true); + else if (tracked === false) tr.setMeta('skipTrackChanges', true); + dispatch(tr); + return true; + } catch { + return false; + } + }; diff --git a/packages/super-editor/src/core/commands/insertHeadingAt.test.js b/packages/super-editor/src/core/commands/insertHeadingAt.test.js new file mode 100644 index 000000000..958285aed --- /dev/null +++ b/packages/super-editor/src/core/commands/insertHeadingAt.test.js @@ -0,0 +1,217 @@ +// @ts-check +import { describe, it, expect, vi } from 'vitest'; +import { insertHeadingAt } from './insertHeadingAt.js'; + +/** + * @param {{ size?: number }} [options] + */ +function createMockState(options = {}) { + const { size = 100 } = options; + + const paragraphType = { + createAndFill: vi.fn(), + create: vi.fn(), + }; + + const schema = { + nodes: { paragraph: paragraphType }, + text: vi.fn((text) => ({ type: { name: 'text' }, text, nodeSize: text.length })), + }; + + const mockTr = { + insert: vi.fn().mockReturnThis(), + setMeta: vi.fn().mockReturnThis(), + }; + + return { + state: { + doc: { content: { size } }, + schema, + tr: mockTr, + }, + tr: mockTr, + paragraphType, + dispatch: vi.fn(), + }; +} + +describe('insertHeadingAt', () => { + // --- position validation --- + + it('returns false when pos is negative', () => { + const { state, dispatch } = createMockState(); + const result = insertHeadingAt({ pos: -1, level: 1 })({ state, dispatch }); + expect(result).toBe(false); + }); + + it('returns false when pos exceeds document size', () => { + const { state, dispatch } = createMockState({ size: 10 }); + const result = insertHeadingAt({ pos: 11, level: 1 })({ state, dispatch }); + expect(result).toBe(false); + }); + + it('returns false when pos is not an integer', () => { + const { state, dispatch } = createMockState(); + const result = insertHeadingAt({ pos: 1.5, level: 1 })({ state, dispatch }); + expect(result).toBe(false); + }); + + // --- level validation --- + + it('returns false when level is less than 1', () => { + const { state, dispatch } = createMockState(); + const result = insertHeadingAt({ pos: 0, level: 0 })({ state, dispatch }); + expect(result).toBe(false); + }); + + it('returns false when level is greater than 6', () => { + const { state, dispatch } = createMockState(); + const result = insertHeadingAt({ pos: 0, level: 7 })({ state, dispatch }); + expect(result).toBe(false); + }); + + it('returns false when level is not an integer', () => { + const { state, dispatch } = createMockState(); + const result = insertHeadingAt({ pos: 0, level: 1.5 })({ state, dispatch }); + expect(result).toBe(false); + }); + + // --- schema guard --- + + it('returns false when paragraph type is not in schema', () => { + const { state, dispatch } = createMockState(); + state.schema.nodes.paragraph = undefined; + const result = insertHeadingAt({ pos: 0, level: 1 })({ state, dispatch }); + expect(result).toBe(false); + }); + + // --- dry run --- + + it('returns true without dispatching when dispatch is not provided (dry run)', () => { + const { state, paragraphType } = createMockState(); + const mockNode = { type: { name: 'paragraph' } }; + paragraphType.createAndFill.mockReturnValue(mockNode); + + const result = insertHeadingAt({ pos: 0, level: 1 })({ state, dispatch: undefined }); + expect(result).toBe(true); + }); + + // --- successful insertion --- + + it('inserts a heading at the given position', () => { + const { state, dispatch, paragraphType, tr } = createMockState(); + const mockNode = { type: { name: 'paragraph' } }; + paragraphType.createAndFill.mockReturnValue(mockNode); + + const result = insertHeadingAt({ pos: 5, level: 2 })({ state, dispatch }); + + expect(result).toBe(true); + expect(tr.insert).toHaveBeenCalledWith(5, mockNode); + expect(tr.setMeta).toHaveBeenCalledWith('inputType', 'programmatic'); + expect(dispatch).toHaveBeenCalledWith(tr); + }); + + it('sets paragraphProperties.styleId based on level', () => { + const { state, dispatch, paragraphType } = createMockState(); + const mockNode = { type: { name: 'paragraph' } }; + paragraphType.createAndFill.mockReturnValue(mockNode); + + for (let level = 1; level <= 6; level++) { + paragraphType.createAndFill.mockClear(); + insertHeadingAt({ pos: 0, level })({ state, dispatch }); + + const attrs = paragraphType.createAndFill.mock.calls[0]?.[0]; + expect(attrs.paragraphProperties).toEqual({ styleId: `Heading${level}` }); + } + }); + + it('passes sdBlockId as attrs when provided', () => { + const { state, dispatch, paragraphType } = createMockState(); + const mockNode = { type: { name: 'paragraph' } }; + paragraphType.createAndFill.mockReturnValue(mockNode); + + insertHeadingAt({ pos: 0, level: 1, sdBlockId: 'block-1' })({ state, dispatch }); + + const attrs = paragraphType.createAndFill.mock.calls[0]?.[0]; + expect(attrs.sdBlockId).toBe('block-1'); + expect(attrs.paragraphProperties).toEqual({ styleId: 'Heading1' }); + }); + + it('creates a text node when text is provided', () => { + const { state, dispatch, paragraphType } = createMockState(); + const mockNode = { type: { name: 'paragraph' } }; + paragraphType.createAndFill.mockReturnValue(mockNode); + + insertHeadingAt({ pos: 0, level: 1, text: 'Hello' })({ state, dispatch }); + + expect(state.schema.text).toHaveBeenCalledWith('Hello'); + }); + + // --- tracked mode --- + + it('sets forceTrackChanges meta when tracked is true', () => { + const { state, dispatch, paragraphType, tr } = createMockState(); + const mockNode = { type: { name: 'paragraph' } }; + paragraphType.createAndFill.mockReturnValue(mockNode); + + insertHeadingAt({ pos: 0, level: 1, tracked: true })({ state, dispatch }); + + expect(tr.setMeta).toHaveBeenCalledWith('forceTrackChanges', true); + }); + + it('does not set forceTrackChanges when tracked is false', () => { + const { state, dispatch, paragraphType, tr } = createMockState(); + const mockNode = { type: { name: 'paragraph' } }; + paragraphType.createAndFill.mockReturnValue(mockNode); + + insertHeadingAt({ pos: 0, level: 1, tracked: false })({ state, dispatch }); + + const metaCalls = tr.setMeta.mock.calls.map((call) => call[0]); + expect(metaCalls).not.toContain('forceTrackChanges'); + }); + + it('sets skipTrackChanges meta when tracked is false to preserve direct mode semantics', () => { + const { state, dispatch, paragraphType, tr } = createMockState(); + const mockNode = { type: { name: 'paragraph' } }; + paragraphType.createAndFill.mockReturnValue(mockNode); + + insertHeadingAt({ pos: 0, level: 1, tracked: false })({ state, dispatch }); + + expect(tr.setMeta).toHaveBeenCalledWith('skipTrackChanges', true); + }); + + // --- error resilience --- + + it('falls back to paragraphType.create when createAndFill returns null', () => { + const { state, dispatch, paragraphType } = createMockState(); + const mockNode = { type: { name: 'paragraph' } }; + paragraphType.createAndFill.mockReturnValue(null); + paragraphType.create.mockReturnValue(mockNode); + + const result = insertHeadingAt({ pos: 0, level: 1 })({ state, dispatch }); + expect(result).toBe(true); + expect(paragraphType.create).toHaveBeenCalled(); + }); + + it('returns false when both createAndFill and create throw', () => { + const { state, dispatch, paragraphType } = createMockState(); + paragraphType.createAndFill.mockImplementation(() => { + throw new Error('invalid'); + }); + + const result = insertHeadingAt({ pos: 0, level: 1 })({ state, dispatch }); + expect(result).toBe(false); + }); + + it('returns false when tr.insert throws', () => { + const { state, dispatch, paragraphType, tr } = createMockState(); + const mockNode = { type: { name: 'paragraph' } }; + paragraphType.createAndFill.mockReturnValue(mockNode); + tr.insert.mockImplementation(() => { + throw new Error('Position out of range'); + }); + + const result = insertHeadingAt({ pos: 0, level: 1 })({ state, dispatch }); + expect(result).toBe(false); + }); +}); 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 dd345121a..89953c6e7 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 @@ -15,7 +15,7 @@ import { } from '../../extensions/track-changes/constants.js'; import { ListHelpers } from '../../core/helpers/list-numbering-helpers.js'; import { createCommentsAdapter } from '../comments-adapter.js'; -import { createParagraphAdapter } from '../create-adapter.js'; +import { createParagraphAdapter, createHeadingAdapter } from '../create-adapter.js'; import { formatBoldAdapter, formatItalicAdapter, @@ -174,6 +174,7 @@ function makeTextEditor( acceptAllTrackedChanges: vi.fn(() => true), rejectAllTrackedChanges: vi.fn(() => true), insertParagraphAt: vi.fn(() => true), + insertHeadingAt: vi.fn(() => true), insertListItemAt: vi.fn(() => true), setListTypeAt: vi.fn(() => true), increaseListIndent: vi.fn(() => true), @@ -216,6 +217,7 @@ function makeTextEditor( }, can: vi.fn(() => ({ insertParagraphAt: vi.fn(() => true), + insertHeadingAt: vi.fn(() => true), insertListItemAt: vi.fn(() => true), setListTypeAt: vi.fn(() => true), increaseListIndent: vi.fn(() => true), @@ -604,6 +606,32 @@ const mutationVectors: Partial> = { return createParagraphAdapter(editor, { at: { kind: 'documentEnd' }, text: 'X' }, { changeMode: 'direct' }); }, }, + 'create.heading': { + throwCase: () => { + const { editor } = makeTextEditor('Hello', { commands: { insertHeadingAt: undefined } }); + return createHeadingAdapter( + editor, + { level: 1, at: { kind: 'documentEnd' }, text: 'X' }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const { editor } = makeTextEditor('Hello', { commands: { insertHeadingAt: vi.fn(() => false) } }); + return createHeadingAdapter( + editor, + { level: 1, at: { kind: 'documentEnd' }, text: 'X' }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const { editor } = makeTextEditor('Hello', { commands: { insertHeadingAt: vi.fn(() => true) } }); + return createHeadingAdapter( + editor, + { level: 2, at: { kind: 'documentEnd' }, text: 'X' }, + { changeMode: 'direct' }, + ); + }, + }, 'lists.insert': { throwCase: () => { const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, numberingType: 'decimal' })]); @@ -1053,6 +1081,17 @@ const dryRunVectors: Partial unknown>> = { expect(insertParagraphAt).not.toHaveBeenCalled(); return result; }, + 'create.heading': () => { + const insertHeadingAt = vi.fn(() => true); + const { editor } = makeTextEditor('Hello', { commands: { insertHeadingAt } }); + const result = createHeadingAdapter( + editor, + { level: 1, at: { kind: 'documentEnd' }, text: 'Dry run heading' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(insertHeadingAt).not.toHaveBeenCalled(); + return result; + }, 'lists.insert': () => { const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, numberingType: 'decimal' })]); const insertListItemAt = editor.commands!.insertListItemAt as ReturnType; diff --git a/packages/super-editor/src/document-api-adapters/assemble-adapters.test.ts b/packages/super-editor/src/document-api-adapters/assemble-adapters.test.ts index 273468043..714fc50cd 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 @@ -30,6 +30,7 @@ describe('assembleDocumentApiAdapters', () => { expect(adapters).toHaveProperty('trackChanges.acceptAll'); expect(adapters).toHaveProperty('trackChanges.rejectAll'); expect(adapters).toHaveProperty('create.paragraph'); + expect(adapters).toHaveProperty('create.heading'); expect(adapters).toHaveProperty('lists.list'); expect(adapters).toHaveProperty('lists.get'); expect(adapters).toHaveProperty('lists.insert'); @@ -47,6 +48,7 @@ describe('assembleDocumentApiAdapters', () => { expect(typeof adapters.write.write).toBe('function'); expect(typeof adapters.format.bold).toBe('function'); expect(typeof adapters.create.paragraph).toBe('function'); + expect(typeof adapters.create.heading).toBe('function'); expect(typeof adapters.lists.insert).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 9a2e7ce32..4de0d3559 100644 --- a/packages/super-editor/src/document-api-adapters/assemble-adapters.ts +++ b/packages/super-editor/src/document-api-adapters/assemble-adapters.ts @@ -21,7 +21,7 @@ import { trackChangesAcceptAllAdapter, trackChangesRejectAllAdapter, } from './track-changes-adapter.js'; -import { createParagraphAdapter } from './create-adapter.js'; +import { createParagraphAdapter, createHeadingAdapter } from './create-adapter.js'; import { listsListAdapter, listsGetAdapter, @@ -77,6 +77,7 @@ export function assembleDocumentApiAdapters(editor: Editor): DocumentApiAdapters }, create: { paragraph: (input, options) => createParagraphAdapter(editor, input, options), + heading: (input, options) => createHeadingAdapter(editor, input, options), }, lists: { list: (query) => listsListAdapter(editor, query), 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 0515f3070..20c592be9 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 @@ -7,6 +7,7 @@ import { getDocumentApiCapabilities } from './capabilities-adapter.js'; function makeEditor(overrides: Partial = {}): Editor { const defaultCommands = { insertParagraphAt: vi.fn(() => true), + insertHeadingAt: vi.fn(() => true), insertListItemAt: vi.fn(() => true), setListTypeAt: vi.fn(() => true), setTextSelection: vi.fn(() => true), @@ -116,6 +117,9 @@ describe('getDocumentApiCapabilities', () => { expect(capabilities.operations['lists.setType'].dryRun).toBe(true); expect(capabilities.operations['trackChanges.accept'].dryRun).toBe(false); expect(capabilities.operations['create.paragraph'].dryRun).toBe(true); + expect(capabilities.operations['create.heading'].available).toBe(true); + expect(capabilities.operations['create.heading'].tracked).toBe(true); + expect(capabilities.operations['create.heading'].dryRun).toBe(true); }); it('advertises dryRun for list mutators that implement dry-run behavior', () => { @@ -146,6 +150,8 @@ describe('getDocumentApiCapabilities', () => { expect(capabilities.operations.insert.reasons).toContain('TRACKED_MODE_UNAVAILABLE'); expect(capabilities.operations['create.paragraph'].tracked).toBe(false); expect(capabilities.operations['create.paragraph'].reasons).toContain('TRACKED_MODE_UNAVAILABLE'); + expect(capabilities.operations['create.heading'].tracked).toBe(false); + expect(capabilities.operations['create.heading'].reasons).toContain('TRACKED_MODE_UNAVAILABLE'); }); it('never reports tracked=true when the operation is unavailable', () => { @@ -162,6 +168,20 @@ describe('getDocumentApiCapabilities', () => { expect(capabilities.operations['create.paragraph'].tracked).toBe(false); }); + it('marks create.heading as unavailable when insertHeadingAt command is missing', () => { + const capabilities = getDocumentApiCapabilities( + makeEditor({ + commands: { + insertHeadingAt: undefined, + } as unknown as Editor['commands'], + }), + ); + + expect(capabilities.operations['create.heading'].available).toBe(false); + expect(capabilities.operations['create.heading'].tracked).toBe(false); + expect(capabilities.operations['create.heading'].reasons).toContain('COMMAND_UNAVAILABLE'); + }); + it('does not emit unavailable reasons for modes that are unsupported by design', () => { const capabilities = getDocumentApiCapabilities(makeEditor()); const setTypeReasons = capabilities.operations['lists.setType'].reasons ?? []; 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 dd8b7427f..e19617f58 100644 --- a/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts +++ b/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts @@ -16,6 +16,7 @@ type EditorCommandName = string; // Read-only operations (find, getNode, getText, info, etc.) similarly need no commands. const REQUIRED_COMMANDS: Partial> = { 'create.paragraph': ['insertParagraphAt'], + 'create.heading': ['insertHeadingAt'], 'lists.insert': ['insertListItemAt'], 'lists.setType': ['setListTypeAt'], 'lists.indent': ['setTextSelection', 'increaseListIndent'], diff --git a/packages/super-editor/src/document-api-adapters/create-adapter.test.ts b/packages/super-editor/src/document-api-adapters/create-adapter.test.ts index 0cfba9300..a003830b8 100644 --- a/packages/super-editor/src/document-api-adapters/create-adapter.test.ts +++ b/packages/super-editor/src/document-api-adapters/create-adapter.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; import type { Node as ProseMirrorNode } from 'prosemirror-model'; import type { Editor } from '../core/Editor.js'; -import { createParagraphAdapter } from './create-adapter.js'; +import { createParagraphAdapter, createHeadingAdapter } from './create-adapter.js'; import * as trackedChangeResolver from './helpers/tracked-change-resolver.js'; type MockNode = ProseMirrorNode & { @@ -385,3 +385,303 @@ describe('createParagraphAdapter', () => { ); }); }); + +// --------------------------------------------------------------------------- +// createHeadingAdapter +// --------------------------------------------------------------------------- + +function createHeadingNode( + id: string, + level: number, + text = '', + tracked = false, + extraAttrs: Record = {}, +): MockNode { + const marks = + tracked && text.length > 0 + ? [ + { + type: { name: 'trackInsert' }, + attrs: { id: `tc-${id}` }, + }, + ] + : []; + const children = text.length > 0 ? [createTextNode(text, marks)] : []; + const contentSize = children.reduce((sum, child) => sum + child.nodeSize, 0); + + return { + type: { name: 'paragraph' }, + attrs: { + sdBlockId: id, + paragraphProperties: { styleId: `Heading${level}` }, + ...extraAttrs, + }, + _children: children, + nodeSize: contentSize + 2, + isText: false, + isInline: false, + isBlock: true, + isLeaf: false, + inlineContent: true, + isTextblock: true, + childCount: children.length, + child(index: number) { + return children[index] as unknown as ProseMirrorNode; + }, + descendants(callback: (node: ProseMirrorNode, pos: number) => void) { + let offset = 1; + for (const child of children) { + callback(child as unknown as ProseMirrorNode, offset); + offset += child.nodeSize; + } + return undefined; + }, + } as unknown as MockNode; +} + +function makeHeadingEditor({ + withTrackedCommand = true, + insertReturns = true, + insertedHeadingAttrs, + user, +}: { + withTrackedCommand?: boolean; + insertReturns?: boolean; + insertedHeadingAttrs?: Record; + user?: { name: string }; +} = {}): { + editor: Editor; + insertHeadingAt: ReturnType; +} { + const doc = createDocNode([createParagraphNode('p1', 'Hello')]); + + const insertHeadingAt = vi.fn( + (options: { pos: number; level: number; text?: string; sdBlockId?: string; tracked?: boolean }) => { + if (!insertReturns) return false; + const nodeId = options.sdBlockId ?? 'new-heading'; + const heading = createHeadingNode( + nodeId, + options.level, + options.text ?? '', + options.tracked === true, + insertedHeadingAttrs, + ); + return insertChildAtPos(doc, heading, options.pos); + }, + ); + + const editor = { + state: { + doc, + }, + commands: { + insertHeadingAt, + insertTrackedChange: withTrackedCommand ? vi.fn(() => true) : undefined, + }, + can: () => ({ + insertHeadingAt: (opts: { pos: number; level: number }) => { + if (!insertReturns) return false; + return opts.level >= 1 && opts.level <= 6; + }, + }), + options: { user }, + } as unknown as Editor; + + return { editor, insertHeadingAt }; +} + +describe('createHeadingAdapter', () => { + it('creates a heading at the document end by default', () => { + const { editor, insertHeadingAt } = makeHeadingEditor(); + + const result = createHeadingAdapter(editor, { level: 2, text: 'New heading' }, { changeMode: 'direct' }); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.heading.kind).toBe('block'); + expect(result.heading.nodeType).toBe('heading'); + expect(result.insertionPoint.kind).toBe('text'); + expect(result.insertionPoint.range).toEqual({ start: 0, end: 0 }); + } + + expect(insertHeadingAt).toHaveBeenCalledTimes(1); + expect(insertHeadingAt.mock.calls[0]?.[0]).toMatchObject({ + level: 2, + text: 'New heading', + tracked: false, + }); + }); + + it('creates a heading before a target block', () => { + const { editor, insertHeadingAt } = makeHeadingEditor(); + + const result = createHeadingAdapter( + editor, + { + level: 1, + at: { + kind: 'before', + target: { kind: 'block', nodeType: 'paragraph', nodeId: 'p1' }, + }, + }, + { changeMode: 'direct' }, + ); + + expect(result.success).toBe(true); + expect(insertHeadingAt.mock.calls[0]?.[0]?.pos).toBe(0); + }); + + it('throws TARGET_NOT_FOUND when a before/after target cannot be resolved', () => { + const { editor } = makeHeadingEditor(); + + expect(() => + createHeadingAdapter( + editor, + { + level: 1, + at: { + kind: 'after', + target: { kind: 'block', nodeType: 'paragraph', nodeId: 'missing' }, + }, + }, + { changeMode: 'direct' }, + ), + ).toThrow('target block was not found'); + }); + + it('throws CAPABILITY_UNAVAILABLE when tracked create is requested without tracked capability', () => { + const { editor } = makeHeadingEditor({ withTrackedCommand: false }); + + expect(() => createHeadingAdapter(editor, { level: 1, text: 'Tracked' }, { changeMode: 'tracked' })).toThrow( + 'requires the insertTrackedChange command', + ); + }); + + it('creates tracked headings and returns trackedChangeRefs', () => { + const resolverSpy = vi.spyOn(trackedChangeResolver, 'buildTrackedChangeCanonicalIdMap').mockReturnValue(new Map()); + + const { editor } = makeHeadingEditor({ user: { name: 'Test' } }); + + const result = createHeadingAdapter(editor, { level: 1, text: 'Tracked heading' }, { changeMode: 'tracked' }); + + expect(result.success).toBe(true); + if (!result.success) return; + expect(result.trackedChangeRefs?.length).toBeGreaterThan(0); + expect(result.trackedChangeRefs?.[0]).toMatchObject({ + kind: 'entity', + entityType: 'trackedChange', + }); + expect(resolverSpy).toHaveBeenCalledTimes(1); + resolverSpy.mockRestore(); + }); + + it('returns INVALID_TARGET failure when command cannot apply the insertion', () => { + const { editor } = makeHeadingEditor({ insertReturns: false }); + + const result = createHeadingAdapter(editor, { level: 1, text: 'No-op' }, { changeMode: 'direct' }); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.failure.code).toBe('INVALID_TARGET'); + } + }); + + it('dry-run returns placeholder success without mutating the document', () => { + const { editor, insertHeadingAt } = makeHeadingEditor(); + + const result = createHeadingAdapter( + editor, + { level: 1, text: 'Dry run text' }, + { changeMode: 'direct', dryRun: true }, + ); + + expect(result.success).toBe(true); + if (!result.success) return; + expect(result.heading).toEqual({ kind: 'block', nodeType: 'heading', nodeId: '(dry-run)' }); + expect(result.insertionPoint).toEqual({ kind: 'text', blockId: '(dry-run)', range: { start: 0, end: 0 } }); + expect(insertHeadingAt).not.toHaveBeenCalled(); + }); + + it('dry-run returns INVALID_TARGET when insertion cannot be applied', () => { + const { editor } = makeHeadingEditor({ insertReturns: false }); + + const result = createHeadingAdapter( + editor, + { level: 1, text: 'Dry run text' }, + { changeMode: 'direct', dryRun: true }, + ); + + expect(result.success).toBe(false); + if (result.success) return; + expect(result.failure.code).toBe('INVALID_TARGET'); + }); + + it('dry-run still throws TARGET_NOT_FOUND when target block does not exist', () => { + const { editor } = makeHeadingEditor(); + + expect(() => + createHeadingAdapter( + editor, + { + level: 1, + at: { + kind: 'before', + target: { kind: 'block', nodeType: 'paragraph', nodeId: 'missing' }, + }, + }, + { changeMode: 'direct', dryRun: true }, + ), + ).toThrow('target block was not found'); + }); + + it('dry-run still throws CAPABILITY_UNAVAILABLE when tracked capability is missing', () => { + const { editor } = makeHeadingEditor({ withTrackedCommand: false }); + + expect(() => + createHeadingAdapter(editor, { level: 1, text: 'Tracked dry run' }, { changeMode: 'tracked', dryRun: true }), + ).toThrow('requires the insertTrackedChange command'); + }); + + it('returns success with generated ID when post-apply heading resolution fails', () => { + const { editor } = makeHeadingEditor({ + insertedHeadingAttrs: { + sdBlockId: undefined, + paragraphProperties: {}, + }, + }); + + const result = createHeadingAdapter(editor, { level: 1, text: 'Inserted heading' }, { changeMode: 'direct' }); + + // Contract: success:false means no mutation was applied. + // The mutation DID apply, so we must return success with the generated ID. + expect(result.success).toBe(true); + if (!result.success) return; + expect(result.heading.nodeType).toBe('heading'); + expect(typeof result.heading.nodeId).toBe('string'); + expect(result.heading.nodeId).not.toBe('(dry-run)'); + }); + + it('throws CAPABILITY_UNAVAILABLE for tracked dry-run without a configured user', () => { + const { editor } = makeHeadingEditor(); + + expect(() => + createHeadingAdapter(editor, { level: 1, text: 'Tracked' }, { changeMode: 'tracked', dryRun: true }), + ).toThrow('requires a user to be configured'); + }); + + it('throws same error for tracked non-dry-run without a configured user', () => { + const { editor } = makeHeadingEditor(); + + expect(() => createHeadingAdapter(editor, { level: 1, text: 'Tracked' }, { changeMode: 'tracked' })).toThrow( + 'requires a user to be configured', + ); + }); + + it('passes level through to the insertHeadingAt command', () => { + const { editor, insertHeadingAt } = makeHeadingEditor(); + + createHeadingAdapter(editor, { level: 3 }, { changeMode: 'direct' }); + + expect(insertHeadingAt.mock.calls[0]?.[0]).toMatchObject({ level: 3 }); + }); +}); diff --git a/packages/super-editor/src/document-api-adapters/create-adapter.ts b/packages/super-editor/src/document-api-adapters/create-adapter.ts index ec081a0d0..501ee8378 100644 --- a/packages/super-editor/src/document-api-adapters/create-adapter.ts +++ b/packages/super-editor/src/document-api-adapters/create-adapter.ts @@ -4,6 +4,9 @@ import type { CreateParagraphInput, CreateParagraphResult, CreateParagraphSuccessResult, + CreateHeadingInput, + CreateHeadingResult, + CreateHeadingSuccessResult, MutationOptions, } from '@superdoc/document-api'; import { clearIndexCache, getBlockIndex } from './helpers/index-cache.js'; @@ -21,6 +24,16 @@ type InsertParagraphAtCommandOptions = { type InsertParagraphAtCommand = (options: InsertParagraphAtCommandOptions) => boolean; +type InsertHeadingAtCommandOptions = { + pos: number; + level: number; + text?: string; + sdBlockId?: string; + tracked?: boolean; +}; + +type InsertHeadingAtCommand = (options: InsertHeadingAtCommandOptions) => boolean; + function resolveParagraphInsertPosition(editor: Editor, input: CreateParagraphInput): number { const location = input.at ?? { kind: 'documentEnd' }; @@ -166,3 +179,151 @@ export function createParagraphAdapter( return buildParagraphCreateSuccess(paragraphId); } } + +// --------------------------------------------------------------------------- +// create.heading +// --------------------------------------------------------------------------- + +function resolveHeadingInsertPosition(editor: Editor, input: CreateHeadingInput): number { + const location = input.at ?? { kind: 'documentEnd' }; + + if (location.kind === 'documentStart') return 0; + if (location.kind === 'documentEnd') return editor.state.doc.content.size; + + const index = getBlockIndex(editor); + const target = findBlockById(index, location.target); + if (!target) { + throw new DocumentApiAdapterError('TARGET_NOT_FOUND', 'Create heading target block was not found.', { + target: location.target, + }); + } + + return location.kind === 'before' ? target.pos : target.end; +} + +function resolveCreatedHeading(editor: Editor, headingId: string): BlockCandidate { + const index = getBlockIndex(editor); + const resolved = index.byId.get(`heading:${headingId}`); + + if (resolved) return resolved; + + const bySdBlockId = index.candidates.find((candidate) => { + if (candidate.nodeType !== 'heading') return false; + const attrs = (candidate.node as { attrs?: { sdBlockId?: unknown } }).attrs; + return typeof attrs?.sdBlockId === 'string' && attrs.sdBlockId === headingId; + }); + if (bySdBlockId) return bySdBlockId; + + const fallback = index.candidates.find( + (candidate) => candidate.nodeType === 'heading' && candidate.nodeId === headingId, + ); + if (fallback) return fallback; + + throw new DocumentApiAdapterError('TARGET_NOT_FOUND', 'Created heading could not be resolved after insertion.', { + headingId, + }); +} + +function buildHeadingCreateSuccess( + headingNodeId: string, + trackedChangeRefs?: CreateHeadingSuccessResult['trackedChangeRefs'], +): CreateHeadingSuccessResult { + return { + success: true, + heading: { + kind: 'block', + nodeType: 'heading', + nodeId: headingNodeId, + }, + insertionPoint: { + kind: 'text', + blockId: headingNodeId, + range: { start: 0, end: 0 }, + }, + trackedChangeRefs, + }; +} + +export function createHeadingAdapter( + editor: Editor, + input: CreateHeadingInput, + options?: MutationOptions, +): CreateHeadingResult { + const insertHeadingAt = requireEditorCommand( + editor.commands?.insertHeadingAt, + 'create.heading', + ) as InsertHeadingAtCommand; + const mode = options?.changeMode ?? 'direct'; + + if (mode === 'tracked') { + ensureTrackedCapability(editor, { operation: 'create.heading' }); + } + + const insertAt = resolveHeadingInsertPosition(editor, input); + + if (options?.dryRun) { + const canInsert = editor.can().insertHeadingAt?.({ + pos: insertAt, + level: input.level, + text: input.text, + tracked: mode === 'tracked', + }); + + if (!canInsert) { + return { + success: false, + failure: { + code: 'INVALID_TARGET', + message: 'Heading creation could not be applied at the requested location.', + }, + }; + } + + return { + success: true, + heading: { + kind: 'block', + nodeType: 'heading', + nodeId: '(dry-run)', + }, + insertionPoint: { + kind: 'text', + blockId: '(dry-run)', + range: { start: 0, end: 0 }, + }, + }; + } + + const headingId = uuidv4(); + + const didApply = insertHeadingAt({ + pos: insertAt, + level: input.level, + text: input.text, + sdBlockId: headingId, + tracked: mode === 'tracked', + }); + + if (!didApply) { + return { + success: false, + failure: { + code: 'INVALID_TARGET', + message: 'Heading creation could not be applied at the requested location.', + }, + }; + } + + clearIndexCache(editor); + try { + const heading = resolveCreatedHeading(editor, headingId); + const trackedChangeRefs = + mode === 'tracked' ? collectTrackInsertRefsInRange(editor, heading.pos, heading.end) : undefined; + + return buildHeadingCreateSuccess(heading.nodeId, trackedChangeRefs); + } catch { + // Mutation already applied — contract requires success: true. + // Fall back to the generated ID we assigned to the command. + return buildHeadingCreateSuccess(headingId); + } +} diff --git a/packages/super-editor/src/document-api-adapters/index.ts b/packages/super-editor/src/document-api-adapters/index.ts index 118952ab3..81ac053cd 100644 --- a/packages/super-editor/src/document-api-adapters/index.ts +++ b/packages/super-editor/src/document-api-adapters/index.ts @@ -14,7 +14,7 @@ import type { import type { Editor } from '../core/Editor.js'; import { getDocumentApiCapabilities } from './capabilities-adapter.js'; import { createCommentsAdapter } from './comments-adapter.js'; -import { createParagraphAdapter } from './create-adapter.js'; +import { createParagraphAdapter, createHeadingAdapter } from './create-adapter.js'; import { findAdapter } from './find-adapter.js'; import { formatBoldAdapter, @@ -90,6 +90,7 @@ export function getDocumentApiAdapters(editor: Editor): DocumentApiAdapters { }, create: { paragraph: (input, options) => createParagraphAdapter(editor, input, options), + heading: (input, options) => createHeadingAdapter(editor, input, options), }, lists: { list: (query) => listsListAdapter(editor, query), From 7a78a82243b48daebc7741a7dbf11beb8d68d2af Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Tue, 17 Feb 2026 21:33:03 -0800 Subject: [PATCH 4/5] feat(document-api): add docs, workflow examples, and overview example tests --- apps/docs/document-api/overview.mdx | 73 +++ packages/document-api/README.md | 11 + packages/document-api/scripts/README.md | 4 + packages/document-api/src/README.md | 499 +++++++++++++++++- .../src/overview-examples.test.ts | 449 ++++++++++++++++ 5 files changed, 1035 insertions(+), 1 deletion(-) create mode 100644 packages/document-api/src/overview-examples.test.ts diff --git a/apps/docs/document-api/overview.mdx b/apps/docs/document-api/overview.mdx index 3de0fc33a..e37201285 100644 --- a/apps/docs/document-api/overview.mdx +++ b/apps/docs/document-api/overview.mdx @@ -80,3 +80,76 @@ Use the tables below to see what operations are available and where each one is | `editor.doc.trackChanges.rejectAll(...)` | [`trackChanges.rejectAll`](/document-api/reference/track-changes/reject-all) | | `editor.doc.capabilities()` | [`capabilities.get`](/document-api/reference/capabilities/get) | {/* DOC_API_OPERATIONS_END */} + +## Common workflows + +These patterns show how operations compose to cover typical tasks. + +### Find and mutate + +Locate text in the document, then replace it: + +```ts +const result = editor.doc.find({ type: 'text', text: 'foo' }); +const target = result.context?.[0]?.textRanges?.[0]; +if (target) { + editor.doc.replace({ target, text: 'bar' }); +} +``` + +### Tracked-mode insert + +Insert text as a tracked change so reviewers can accept or reject it: + +```ts +const receipt = editor.doc.insert( + { text: 'new content' }, + { changeMode: 'tracked' }, +); +``` + +The receipt includes a `resolution` with the resolved insertion point and `inserted` entries with tracked-change IDs. + +### Check capabilities before acting + +Use `capabilities()` to branch on what the editor supports: + +```ts +const caps = editor.doc.capabilities(); + +if (caps.operations['format.bold'].available) { + editor.doc.format.bold({ target }); +} + +if (caps.global.trackChanges.enabled) { + editor.doc.insert({ text: 'tracked' }, { changeMode: 'tracked' }); +} +``` + +### Dry-run preview + +Pass `dryRun: true` to validate an operation without applying it: + +```ts +const preview = editor.doc.insert( + { target, text: 'hello' }, + { dryRun: true }, +); +// preview.success tells you whether the insert would succeed +// preview.resolution shows the resolved target range +``` + +## Programmatic invocation + +Every operation is also available through a dynamic `invoke` method on the Document API instance. It accepts an `operationId` string and dispatches to the corresponding method. This is useful when the operation to run is determined at runtime (for example, from a tool-use payload). + +```ts +// doc is the DocumentApi instance (editor.doc) +const result = doc.invoke({ + operationId: 'insert', + input: { text: 'hello' }, + options: { changeMode: 'tracked' }, +}); +``` + +`invoke` delegates to the same direct methods — there is no separate execution path. Type safety is available through `InvokeRequest` for callers who know the operation at compile time, and `DynamicInvokeRequest` for dynamic dispatch. diff --git a/packages/document-api/README.md b/packages/document-api/README.md index 4bf0e42cc..dcd43317d 100644 --- a/packages/document-api/README.md +++ b/packages/document-api/README.md @@ -60,6 +60,17 @@ operation-definitions.ts types.ts (re-exports + CommandCatalog, guards) - `operation-registry.ts` is the single source of truth for type signatures (input/options/output per operation). - `TypedDispatchTable` (in `invoke.ts`) validates at compile time that dispatch wiring conforms to the registry. +## OperationRegistry and invoke + +`operation-registry.ts` is the canonical type-level mapping from `OperationId` to `{ input, options, output }`. Bidirectional `Assert` checks guarantee every `OperationId` has a registry entry and vice versa. + +The invoke system (`invoke.ts`) builds a `TypedDispatchTable` that maps each operation to its direct API method. This provides: + +- **`InvokeRequest`** — typed invoke request, narrowed by `operationId`. Use when the operation is known at compile time. +- **`DynamicInvokeRequest`** — loose invoke request for dynamic callers (AI tool-use, runtime dispatch). Adapter-level validation catches invalid inputs. + +`TypedDispatchTable` is a mapped type that fails to compile if any dispatch entry doesn't match the registry. This is a compile-time parity check — there is no separate runtime script for it. + ## Related docs - `packages/document-api/src/README.md` for contract semantics and invariants diff --git a/packages/document-api/scripts/README.md b/packages/document-api/scripts/README.md index 2da578635..5d8a50f2e 100644 --- a/packages/document-api/scripts/README.md +++ b/packages/document-api/scripts/README.md @@ -46,6 +46,10 @@ Do not hand-edit generated output files. Regenerate instead. | `check-contract-parity.ts` | check | Enforce parity between operation IDs, command catalog, maps, and runtime API member paths | `packages/document-api/src/index.js` exports + runtime API shape | None | Contract surface integrity gate | | `generate-internal-schemas.ts` | generate | Generate internal-only operation schema snapshot | Contract snapshot + schema dialect | `packages/document-api/.generated-internal/contract-schemas/index.json` | Local tooling/debugging | +## Compile-time parity checks + +Not all parity checks are runtime scripts. `TypedDispatchTable` in `invoke.ts` is a mapped type that validates at compile time that every `OperationId` has a matching dispatch entry. If you add an operation to `operation-definitions.ts` and `operation-registry.ts` but forget the dispatch entry, `tsc` will fail before any script runs. + ## Recommended usage 1. Change contract/docs sources. diff --git a/packages/document-api/src/README.md b/packages/document-api/src/README.md index 3679fb737..c4f9aae19 100644 --- a/packages/document-api/src/README.md +++ b/packages/document-api/src/README.md @@ -57,7 +57,7 @@ lives in adapter layers that map engine behavior into `QueryResult` and other AP ## Tracked-Change Semantics - Tracking is operation-scoped (`changeMode: 'direct' | 'tracked'`), not global editor-mode state. -- `insert`, `replace`, `delete`, `format.bold`, and `create.paragraph` may run in tracked mode. +- `insert`, `replace`, `delete`, `format.bold`, `format.italic`, `format.underline`, `format.strikethrough`, and `create.paragraph`, `create.heading` may run in tracked mode. - `trackChanges.*` (`list`, `get`, `accept`, `reject`, `acceptAll`, `rejectAll`) is the review lifecycle namespace. - `lists.insert` may run in tracked mode; `lists.setType|indent|outdent|restart|exit` are direct-only in v1. @@ -77,3 +77,500 @@ Deterministic outcomes: - Text/format targets that cannot be resolved after remote edits must fail deterministically (`TARGET_NOT_FOUND` / `NO_OP`), never silently mutate the wrong range. - Tracked entity IDs returned by mutation receipts (`insert` / `replace` / `delete`) and `create.paragraph.trackedChangeRefs` must match canonical IDs from `trackChanges.list`. - `trackChanges.get` / `accept` / `reject` accept canonical IDs only. + +## Common Workflows + +The following examples show typical multi-step patterns using the Document API. + +### Workflow: Find + Mutate + +Locate text in the document and replace it: + +```ts +const result = editor.doc.find({ type: 'text', text: 'foo' }); +const target = result.context?.[0]?.textRanges?.[0]; +if (target) { + editor.doc.replace({ target, text: 'bar' }); +} +``` + +### Workflow: Tracked-Mode Insert + +Insert text as a tracked change so reviewers can accept or reject it: + +```ts +const receipt = editor.doc.insert( + { text: 'new content' }, + { changeMode: 'tracked' }, +); +// receipt.resolution.target contains the resolved insertion point +// receipt.inserted contains TrackedChangeAddress entries for the new change +``` + +### Workflow: Comment Thread Lifecycle + +Add a comment, reply, then resolve the thread: + +```ts +const target = result.context?.[0]?.textRanges?.[0]; +const addReceipt = editor.doc.comments.add({ target, text: 'Review this section.' }); +// Use the comment ID from the receipt to reply +const comments = editor.doc.comments.list(); +const thread = comments.matches[0]; +editor.doc.comments.reply({ parentCommentId: thread.commentId, text: 'Looks good.' }); +editor.doc.comments.resolve({ commentId: thread.commentId }); +``` + +### Workflow: List Manipulation + +Insert a list item, change its type, then indent it: + +```ts +const lists = editor.doc.lists.list(); +const firstItem = lists.matches[0]; +const insertResult = editor.doc.lists.insert({ target: firstItem, position: 'after', text: 'New item' }); +if (insertResult.success) { + editor.doc.lists.setType({ target: insertResult.item, kind: 'ordered' }); + editor.doc.lists.indent({ target: insertResult.item }); +} +``` + +### Workflow: Capabilities-Aware Branching + +Check what the editor supports before attempting mutations: + +```ts +const caps = editor.doc.capabilities(); +if (caps.operations['format.bold'].available) { + editor.doc.format.bold({ target }); +} +if (caps.global.trackChanges.enabled) { + editor.doc.insert({ text: 'tracked' }, { changeMode: 'tracked' }); +} +if (caps.operations['create.heading'].dryRun) { + const preview = editor.doc.create.heading( + { level: 2, text: 'Preview' }, + { dryRun: true }, + ); +} +``` + +## Operation Reference + +Each operation has a dedicated section below. Grouped by namespace. + +### Core + +### `find` + +Search the document for nodes or text matching a selector. Returns `QueryResult` with `matches` as `NodeAddress[]`. Text selectors include `context[*].textRanges` for precise span targeting. + +- **Input**: `Selector | Query` +- **Output**: `QueryResult` +- **Mutates**: No +- **Idempotency**: idempotent + +### `getNode` + +Resolve a `NodeAddress` to full `NodeInfo` including typed properties (text content, attributes, node type). Throws `TARGET_NOT_FOUND` when the address is invalid. + +- **Input**: `NodeAddress` +- **Output**: `NodeInfo` +- **Mutates**: No +- **Idempotency**: idempotent + +### `getNodeById` + +Resolve a block node by its unique `nodeId`. Optionally constrain by `nodeType`. Throws `TARGET_NOT_FOUND` when the ID is not found. + +- **Input**: `GetNodeByIdInput` (`{ nodeId, nodeType? }`) +- **Output**: `NodeInfo` +- **Mutates**: No +- **Idempotency**: idempotent + +### `getText` + +Return the full plaintext content of the document. + +- **Input**: `GetTextInput` (empty object) +- **Output**: `string` +- **Mutates**: No +- **Idempotency**: idempotent + +### `info` + +Return document summary metadata (block count, word count, character count). + +- **Input**: `InfoInput` (empty object) +- **Output**: `DocumentInfo` +- **Mutates**: No +- **Idempotency**: idempotent + +### `insert` + +Insert text at an optional `TextAddress` target. When `target` is omitted, the adapter resolves a deterministic default insertion point. Supports dry-run and tracked mode. + +- **Input**: `InsertInput` (`{ target?, text }`) +- **Options**: `MutationOptions` (`{ changeMode?, dryRun? }`) +- **Output**: `TextMutationReceipt` +- **Mutates**: Yes +- **Idempotency**: non-idempotent +- **Failure codes**: `INVALID_TARGET`, `NO_OP` + +### `replace` + +Replace text at a `TextAddress` target with new content. The target range must resolve to a valid span. Supports dry-run and tracked mode. + +- **Input**: `ReplaceInput` (`{ target, text }`) +- **Options**: `MutationOptions` (`{ changeMode?, dryRun? }`) +- **Output**: `TextMutationReceipt` +- **Mutates**: Yes +- **Idempotency**: conditional +- **Failure codes**: `INVALID_TARGET`, `NO_OP` + +### `delete` + +Delete the text span covered by a `TextAddress` target. Supports dry-run and tracked mode. + +- **Input**: `DeleteInput` (`{ target }`) +- **Options**: `MutationOptions` (`{ changeMode?, dryRun? }`) +- **Output**: `TextMutationReceipt` +- **Mutates**: Yes +- **Idempotency**: conditional +- **Failure codes**: `NO_OP` + +### Capabilities + +### `capabilities.get` + +Return a runtime capability snapshot describing which operations, namespaces, tracked mode, and dry-run support are available in the current editor configuration. + +- **Input**: `undefined` +- **Output**: `DocumentApiCapabilities` +- **Mutates**: No +- **Idempotency**: idempotent + +### Create + +### `create.paragraph` + +Insert a new paragraph node at a specified location (document start/end, before/after a block). Returns the new paragraph's `BlockNodeAddress` and `insertionPoint`. Supports dry-run and tracked mode. + +- **Input**: `CreateParagraphInput` (`{ at?, text? }`) +- **Options**: `MutationOptions` (`{ changeMode?, dryRun? }`) +- **Output**: `CreateParagraphResult` +- **Mutates**: Yes +- **Idempotency**: non-idempotent +- **Failure codes**: `INVALID_TARGET` + +### `create.heading` + +Insert a new heading node at a specified location with a given level (1-6). Returns the new heading's `BlockNodeAddress` and `insertionPoint`. Supports dry-run and tracked mode. + +- **Input**: `CreateHeadingInput` (`{ level, at?, text? }`) +- **Options**: `MutationOptions` (`{ changeMode?, dryRun? }`) +- **Output**: `CreateHeadingResult` +- **Mutates**: Yes +- **Idempotency**: non-idempotent +- **Failure codes**: `INVALID_TARGET` + +### Format + +### `format.bold` + +Toggle bold formatting on a `TextAddress` range. Supports dry-run and tracked mode. Availability depends on the `bold` mark being registered in the editor schema. + +- **Input**: `FormatBoldInput` (`{ target }`) +- **Options**: `MutationOptions` (`{ changeMode?, dryRun? }`) +- **Output**: `TextMutationReceipt` +- **Mutates**: Yes +- **Idempotency**: conditional +- **Failure codes**: `INVALID_TARGET` + +### `format.italic` + +Toggle italic formatting on a `TextAddress` range. Supports dry-run and tracked mode. Availability depends on the `italic` mark being registered in the editor schema. + +- **Input**: `FormatItalicInput` (`{ target }`) +- **Options**: `MutationOptions` (`{ changeMode?, dryRun? }`) +- **Output**: `TextMutationReceipt` +- **Mutates**: Yes +- **Idempotency**: conditional +- **Failure codes**: `INVALID_TARGET` + +### `format.underline` + +Toggle underline formatting on a `TextAddress` range. Supports dry-run and tracked mode. Availability depends on the `underline` mark being registered in the editor schema. + +- **Input**: `FormatUnderlineInput` (`{ target }`) +- **Options**: `MutationOptions` (`{ changeMode?, dryRun? }`) +- **Output**: `TextMutationReceipt` +- **Mutates**: Yes +- **Idempotency**: conditional +- **Failure codes**: `INVALID_TARGET` + +### `format.strikethrough` + +Toggle strikethrough formatting on a `TextAddress` range. Supports dry-run and tracked mode. Availability depends on the `strike` mark being registered in the editor schema. + +- **Input**: `FormatStrikethroughInput` (`{ target }`) +- **Options**: `MutationOptions` (`{ changeMode?, dryRun? }`) +- **Output**: `TextMutationReceipt` +- **Mutates**: Yes +- **Idempotency**: conditional +- **Failure codes**: `INVALID_TARGET` + +### Lists + +### `lists.list` + +List all list items in the document, optionally filtered by `within`, `kind`, `level`, or `ordinal`. Supports pagination via `limit` and `offset`. + +- **Input**: `ListsListQuery | undefined` +- **Output**: `ListsListResult` (`{ matches, total, items }`) +- **Mutates**: No +- **Idempotency**: idempotent + +### `lists.get` + +Retrieve full information for a single list item by its `ListItemAddress`. Throws `TARGET_NOT_FOUND` when the address is invalid. + +- **Input**: `ListsGetInput` (`{ address }`) +- **Output**: `ListItemInfo` +- **Mutates**: No +- **Idempotency**: idempotent + +### `lists.insert` + +Insert a new list item before or after a target item. Returns the new item's `ListItemAddress` and `insertionPoint`. Supports dry-run and tracked mode. + +- **Input**: `ListInsertInput` (`{ target, position, text? }`) +- **Options**: `MutationOptions` (`{ changeMode?, dryRun? }`) +- **Output**: `ListsInsertResult` +- **Mutates**: Yes +- **Idempotency**: non-idempotent +- **Failure codes**: `INVALID_TARGET` + +### `lists.setType` + +Change a list item's kind (`ordered` or `bullet`). Returns `NO_OP` when the item already has the requested kind. Direct-only (no tracked mode in v1). Supports dry-run. + +- **Input**: `ListSetTypeInput` (`{ target, kind }`) +- **Options**: `MutationOptions` (`{ dryRun? }`) +- **Output**: `ListsMutateItemResult` +- **Mutates**: Yes +- **Idempotency**: conditional +- **Failure codes**: `NO_OP`, `INVALID_TARGET` + +### `lists.indent` + +Increase the indent level of a list item. Returns `NO_OP` when already at maximum depth. Direct-only (no tracked mode in v1). Supports dry-run. + +- **Input**: `ListTargetInput` (`{ target }`) +- **Options**: `MutationOptions` (`{ dryRun? }`) +- **Output**: `ListsMutateItemResult` +- **Mutates**: Yes +- **Idempotency**: conditional +- **Failure codes**: `NO_OP`, `INVALID_TARGET` + +### `lists.outdent` + +Decrease the indent level of a list item. Returns `NO_OP` when already at top level. Direct-only (no tracked mode in v1). Supports dry-run. + +- **Input**: `ListTargetInput` (`{ target }`) +- **Options**: `MutationOptions` (`{ dryRun? }`) +- **Output**: `ListsMutateItemResult` +- **Mutates**: Yes +- **Idempotency**: conditional +- **Failure codes**: `NO_OP`, `INVALID_TARGET` + +### `lists.restart` + +Restart numbering for an ordered list item. Returns `NO_OP` when the item already starts a new numbering sequence. Direct-only. Supports dry-run. + +- **Input**: `ListTargetInput` (`{ target }`) +- **Options**: `MutationOptions` (`{ dryRun? }`) +- **Output**: `ListsMutateItemResult` +- **Mutates**: Yes +- **Idempotency**: conditional +- **Failure codes**: `NO_OP`, `INVALID_TARGET` + +### `lists.exit` + +Convert a list item back into a plain paragraph, exiting the list. Supports dry-run. Direct-only. + +- **Input**: `ListTargetInput` (`{ target }`) +- **Options**: `MutationOptions` (`{ dryRun? }`) +- **Output**: `ListsExitResult` +- **Mutates**: Yes +- **Idempotency**: conditional +- **Failure codes**: `INVALID_TARGET` + +### Comments + +### `comments.add` + +Attach a new comment to a text range. + +- **Input**: `AddCommentInput` (`{ target, text }`) +- **Output**: `Receipt` +- **Mutates**: Yes +- **Idempotency**: non-idempotent +- **Failure codes**: `INVALID_TARGET`, `NO_OP` + +### `comments.edit` + +Update the body text of an existing comment. + +- **Input**: `EditCommentInput` (`{ commentId, text }`) +- **Output**: `Receipt` +- **Mutates**: Yes +- **Idempotency**: conditional +- **Failure codes**: `NO_OP` + +### `comments.reply` + +Add a reply to an existing comment thread. + +- **Input**: `ReplyToCommentInput` (`{ parentCommentId, text }`) +- **Output**: `Receipt` +- **Mutates**: Yes +- **Idempotency**: non-idempotent +- **Failure codes**: `INVALID_TARGET` + +### `comments.move` + +Move a comment to a different text range. + +- **Input**: `MoveCommentInput` (`{ commentId, target }`) +- **Output**: `Receipt` +- **Mutates**: Yes +- **Idempotency**: conditional +- **Failure codes**: `INVALID_TARGET`, `NO_OP` + +### `comments.resolve` + +Resolve an open comment, marking it as addressed. + +- **Input**: `ResolveCommentInput` (`{ commentId }`) +- **Output**: `Receipt` +- **Mutates**: Yes +- **Idempotency**: conditional +- **Failure codes**: `NO_OP` + +### `comments.remove` + +Remove a comment from the document. + +- **Input**: `RemoveCommentInput` (`{ commentId }`) +- **Output**: `Receipt` +- **Mutates**: Yes +- **Idempotency**: conditional +- **Failure codes**: `NO_OP` + +### `comments.setInternal` + +Set or clear the internal/private flag on a comment. + +- **Input**: `SetCommentInternalInput` (`{ commentId, isInternal }`) +- **Output**: `Receipt` +- **Mutates**: Yes +- **Idempotency**: conditional +- **Failure codes**: `NO_OP`, `INVALID_TARGET` + +### `comments.setActive` + +Set which comment is currently active/focused. Pass `null` to clear. + +- **Input**: `SetCommentActiveInput` (`{ commentId }`) +- **Output**: `Receipt` +- **Mutates**: Yes +- **Idempotency**: conditional +- **Failure codes**: `INVALID_TARGET` + +### `comments.goTo` + +Scroll to and focus a comment in the document. + +- **Input**: `GoToCommentInput` (`{ commentId }`) +- **Output**: `Receipt` +- **Mutates**: No +- **Idempotency**: conditional + +### `comments.get` + +Retrieve full information for a single comment by ID. Throws `TARGET_NOT_FOUND` when the comment is not found. + +- **Input**: `GetCommentInput` (`{ commentId }`) +- **Output**: `CommentInfo` +- **Mutates**: No +- **Idempotency**: idempotent + +### `comments.list` + +List all comments in the document. Optionally include resolved comments. + +- **Input**: `CommentsListQuery | undefined` (`{ includeResolved? }`) +- **Output**: `CommentsListResult` (`{ matches, total }`) +- **Mutates**: No +- **Idempotency**: idempotent + +### Track Changes + +### `trackChanges.list` + +List tracked changes in the document. Supports filtering by `type` and pagination via `limit`/`offset`. + +- **Input**: `TrackChangesListInput | undefined` (`{ limit?, offset?, type? }`) +- **Output**: `TrackChangesListResult` (`{ matches, total, changes? }`) +- **Mutates**: No +- **Idempotency**: idempotent + +### `trackChanges.get` + +Retrieve full information for a single tracked change by its canonical ID. Throws `TARGET_NOT_FOUND` when the ID is invalid. + +- **Input**: `TrackChangesGetInput` (`{ id }`) +- **Output**: `TrackChangeInfo` +- **Mutates**: No +- **Idempotency**: idempotent + +### `trackChanges.accept` + +Accept a tracked change, applying it permanently to the document. Returns `NO_OP` when the change has already been accepted. + +- **Input**: `TrackChangesAcceptInput` (`{ id }`) +- **Output**: `Receipt` +- **Mutates**: Yes +- **Idempotency**: conditional +- **Failure codes**: `NO_OP` + +### `trackChanges.reject` + +Reject a tracked change, reverting it from the document. Returns `NO_OP` when the change has already been rejected. + +- **Input**: `TrackChangesRejectInput` (`{ id }`) +- **Output**: `Receipt` +- **Mutates**: Yes +- **Idempotency**: conditional +- **Failure codes**: `NO_OP` + +### `trackChanges.acceptAll` + +Accept all tracked changes in the document. Returns `NO_OP` when there are no pending changes. + +- **Input**: `TrackChangesAcceptAllInput` (empty object) +- **Output**: `Receipt` +- **Mutates**: Yes +- **Idempotency**: conditional +- **Failure codes**: `NO_OP` + +### `trackChanges.rejectAll` + +Reject all tracked changes in the document. Returns `NO_OP` when there are no pending changes. + +- **Input**: `TrackChangesRejectAllInput` (empty object) +- **Output**: `Receipt` +- **Mutates**: Yes +- **Idempotency**: conditional +- **Failure codes**: `NO_OP` diff --git a/packages/document-api/src/overview-examples.test.ts b/packages/document-api/src/overview-examples.test.ts new file mode 100644 index 000000000..8a059cb72 --- /dev/null +++ b/packages/document-api/src/overview-examples.test.ts @@ -0,0 +1,449 @@ +/** + * Tests that exercise the exact code patterns shown in: + * - apps/docs/document-api/overview.mdx (Common workflows + Programmatic invocation) + * - packages/document-api/src/README.md (Workflow examples) + * + * If any of these tests break, the corresponding documentation example is wrong + * and must be updated to match. + */ +import { describe, expect, it, vi } from 'vitest'; +import { createDocumentApi } from './index.js'; +import type { DocumentApiCapabilities } from './capabilities/capabilities.js'; +import type { OperationId } from './contract/types.js'; +import type { TextAddress } from './types/index.js'; + +// --------------------------------------------------------------------------- +// Shared mock-adapter factories (mirrors index.test.ts patterns) +// --------------------------------------------------------------------------- + +const TEXT_TARGET: TextAddress = { kind: 'text', blockId: 'p1', range: { start: 0, end: 3 } }; + +function makeTextMutationReceipt(target = TEXT_TARGET) { + return { + success: true as const, + resolution: { + target, + range: { from: 1, to: 4 }, + text: 'foo', + }, + inserted: [{ kind: 'entity' as const, entityType: 'trackedChange' as const, entityId: 'tc-1' }], + }; +} + +function makeFindAdapter() { + return { + find: vi.fn(() => ({ + matches: [{ kind: 'block' as const, nodeType: 'paragraph' as const, nodeId: 'p1' }], + total: 1, + context: [ + { + textRanges: [TEXT_TARGET], + }, + ], + })), + }; +} + +function makeGetNodeAdapter() { + return { + getNode: vi.fn(() => ({ nodeType: 'paragraph', kind: 'block', properties: {} })), + getNodeById: vi.fn(() => ({ nodeType: 'paragraph', kind: 'block', properties: {} })), + }; +} + +function makeGetTextAdapter() { + return { getText: vi.fn(() => 'hello') }; +} + +function makeInfoAdapter() { + return { + info: vi.fn(() => ({ + counts: { words: 0, paragraphs: 0, headings: 0, tables: 0, images: 0, comments: 0 }, + outline: [], + capabilities: { canFind: true, canGetNode: true, canComment: true, canReplace: true }, + })), + }; +} + +function makeWriteAdapter() { + return { write: vi.fn(() => makeTextMutationReceipt()) }; +} + +function makeFormatAdapter() { + return { + bold: vi.fn(() => makeTextMutationReceipt()), + italic: vi.fn(() => makeTextMutationReceipt()), + underline: vi.fn(() => makeTextMutationReceipt()), + strikethrough: vi.fn(() => makeTextMutationReceipt()), + }; +} + +function makeCommentsAdapter() { + return { + add: vi.fn(() => ({ success: true as const })), + edit: vi.fn(() => ({ success: true as const })), + reply: vi.fn(() => ({ success: true as const })), + move: vi.fn(() => ({ success: true as const })), + resolve: vi.fn(() => ({ success: true as const })), + remove: vi.fn(() => ({ success: true as const })), + setInternal: vi.fn(() => ({ success: true as const })), + setActive: vi.fn(() => ({ success: true as const })), + goTo: vi.fn(() => ({ success: true as const })), + get: vi.fn(() => ({ + address: { kind: 'entity' as const, entityType: 'comment' as const, entityId: 'c1' }, + commentId: 'c1', + status: 'open' as const, + text: 'Review this section.', + })), + list: vi.fn(() => ({ + matches: [ + { + address: { kind: 'entity' as const, entityType: 'comment' as const, entityId: 'c1' }, + commentId: 'c1', + status: 'open' as const, + text: 'Review this section.', + }, + ], + total: 1, + })), + }; +} + +function makeTrackChangesAdapter() { + return { + list: vi.fn(() => ({ matches: [], total: 0 })), + get: vi.fn((input: { id: string }) => ({ + address: { kind: 'entity' as const, entityType: 'trackedChange' as const, entityId: input.id }, + id: input.id, + type: 'insert' as const, + })), + accept: vi.fn(() => ({ success: true as const })), + reject: vi.fn(() => ({ success: true as const })), + acceptAll: vi.fn(() => ({ success: true as const })), + rejectAll: vi.fn(() => ({ success: true as const })), + }; +} + +function makeCreateAdapter() { + return { + paragraph: vi.fn(() => ({ + success: true as const, + paragraph: { kind: 'block' as const, nodeType: 'paragraph' as const, nodeId: 'new-p' }, + insertionPoint: { kind: 'text' as const, blockId: 'new-p', range: { start: 0, end: 0 } }, + })), + heading: vi.fn(() => ({ + success: true as const, + heading: { kind: 'block' as const, nodeType: 'heading' as const, nodeId: 'new-h' }, + insertionPoint: { kind: 'text' as const, blockId: 'new-h', range: { start: 0, end: 0 } }, + })), + }; +} + +function makeListsAdapter() { + return { + list: vi.fn(() => ({ + matches: [{ kind: 'block' as const, nodeType: 'listItem' as const, nodeId: 'li-1' }], + total: 1, + items: [ + { + address: { kind: 'block' as const, nodeType: 'listItem' as const, nodeId: 'li-1' }, + kind: 'ordered' as const, + level: 0, + text: 'List item', + }, + ], + })), + get: vi.fn(() => ({ + address: { kind: 'block' as const, nodeType: 'listItem' as const, nodeId: 'li-1' }, + kind: 'ordered' as const, + level: 0, + text: 'List item', + })), + insert: vi.fn(() => ({ + success: true as const, + item: { kind: 'block' as const, nodeType: 'listItem' as const, nodeId: 'li-2' }, + insertionPoint: { kind: 'text' as const, blockId: 'li-2', range: { start: 0, end: 0 } }, + })), + setType: vi.fn(() => ({ + success: true as const, + item: { kind: 'block' as const, nodeType: 'listItem' as const, nodeId: 'li-2' }, + })), + indent: vi.fn(() => ({ + success: true as const, + item: { kind: 'block' as const, nodeType: 'listItem' as const, nodeId: 'li-2' }, + })), + outdent: vi.fn(() => ({ + success: true as const, + item: { kind: 'block' as const, nodeType: 'listItem' as const, nodeId: 'li-1' }, + })), + restart: vi.fn(() => ({ + success: true as const, + item: { kind: 'block' as const, nodeType: 'listItem' as const, nodeId: 'li-1' }, + })), + exit: vi.fn(() => ({ + success: true as const, + paragraph: { kind: 'block' as const, nodeType: 'paragraph' as const, nodeId: 'p1' }, + })), + }; +} + +function makeCapabilitiesAdapter(): { get: ReturnType } { + const caps: DocumentApiCapabilities = { + global: { + trackChanges: { enabled: true }, + comments: { enabled: true }, + lists: { enabled: true }, + dryRun: { enabled: true }, + }, + operations: Object.fromEntries( + [ + 'find', + 'getNode', + 'getNodeById', + 'getText', + 'info', + 'insert', + 'replace', + 'delete', + 'format.bold', + 'format.italic', + 'format.underline', + 'format.strikethrough', + 'create.paragraph', + 'create.heading', + 'lists.list', + 'lists.get', + 'lists.insert', + 'lists.setType', + 'lists.indent', + 'lists.outdent', + 'lists.restart', + 'lists.exit', + 'comments.add', + 'comments.edit', + 'comments.reply', + 'comments.move', + 'comments.resolve', + 'comments.remove', + 'comments.setInternal', + 'comments.setActive', + 'comments.goTo', + 'comments.get', + 'comments.list', + 'trackChanges.list', + 'trackChanges.get', + 'trackChanges.accept', + 'trackChanges.reject', + 'trackChanges.acceptAll', + 'trackChanges.rejectAll', + 'capabilities.get', + ].map((id) => [id, { available: true, tracked: true, dryRun: true }]), + ) as DocumentApiCapabilities['operations'], + }; + return { get: vi.fn(() => caps) }; +} + +function makeApi() { + return createDocumentApi({ + find: makeFindAdapter(), + getNode: makeGetNodeAdapter(), + getText: makeGetTextAdapter(), + info: makeInfoAdapter(), + capabilities: makeCapabilitiesAdapter(), + comments: makeCommentsAdapter(), + write: makeWriteAdapter(), + format: makeFormatAdapter(), + trackChanges: makeTrackChangesAdapter(), + create: makeCreateAdapter(), + lists: makeListsAdapter(), + }); +} + +// --------------------------------------------------------------------------- +// overview.mdx — "Common workflows" +// --------------------------------------------------------------------------- + +describe('overview.mdx examples', () => { + describe('Find and mutate', () => { + // Mirrors the exact code block from overview.mdx § "Find and mutate" + it('find text then replace it', () => { + const doc = makeApi(); + + const result = doc.find({ type: 'text', text: 'foo' }); + const target = result.context?.[0]?.textRanges?.[0]; + if (target) { + doc.replace({ target, text: 'bar' }); + } + + expect(target).toBeDefined(); + expect(target!.kind).toBe('text'); + }); + }); + + describe('Tracked-mode insert', () => { + // Mirrors the exact code block from overview.mdx § "Tracked-mode insert" + it('insert text with changeMode tracked', () => { + const doc = makeApi(); + + const receipt = doc.insert({ text: 'new content' }, { changeMode: 'tracked' }); + + expect(receipt.resolution).toBeDefined(); + expect(receipt.resolution.target).toBeDefined(); + }); + }); + + describe('Check capabilities before acting', () => { + // Mirrors the exact code block from overview.mdx § "Check capabilities before acting" + it('branch on capabilities', () => { + const doc = makeApi(); + const target = TEXT_TARGET; + + const caps = doc.capabilities(); + + if (caps.operations['format.bold'].available) { + doc.format.bold({ target }); + } + + if (caps.global.trackChanges.enabled) { + doc.insert({ text: 'tracked' }, { changeMode: 'tracked' }); + } + + // Both branches should execute with our fully-capable mock + expect(caps.operations['format.bold'].available).toBe(true); + expect(caps.global.trackChanges.enabled).toBe(true); + }); + }); + + describe('Dry-run preview', () => { + // Mirrors the exact code block from overview.mdx § "Dry-run preview" + it('insert with dryRun true', () => { + const doc = makeApi(); + const target = TEXT_TARGET; + + const preview = doc.insert({ target, text: 'hello' }, { dryRun: true }); + // preview.success tells you whether the insert would succeed + // preview.resolution shows the resolved target range + + expect(preview).toHaveProperty('success'); + expect(preview).toHaveProperty('resolution'); + expect(preview.resolution).toHaveProperty('target'); + expect(preview.resolution).toHaveProperty('range'); + }); + }); + + describe('Programmatic invocation', () => { + // Mirrors the exact code block from overview.mdx § "Programmatic invocation" + it('invoke with operationId string', () => { + const doc = makeApi(); + + const result = doc.invoke({ + operationId: 'insert', + input: { text: 'hello' }, + options: { changeMode: 'tracked' }, + }); + + expect(result).toBeDefined(); + }); + }); +}); + +// --------------------------------------------------------------------------- +// src/README.md — "Workflow:" examples +// --------------------------------------------------------------------------- + +describe('src/README.md workflow examples', () => { + describe('Workflow: Find + Mutate', () => { + // Mirrors the exact code block from src/README.md § "Workflow: Find + Mutate" + it('find then replace', () => { + const doc = makeApi(); + + const result = doc.find({ type: 'text', text: 'foo' }); + const target = result.context?.[0]?.textRanges?.[0]; + if (target) { + doc.replace({ target, text: 'bar' }); + } + + expect(target).toBeDefined(); + }); + }); + + describe('Workflow: Tracked-Mode Insert', () => { + // Mirrors the exact code block from src/README.md § "Workflow: Tracked-Mode Insert" + it('insert in tracked mode and access receipt properties', () => { + const doc = makeApi(); + + const receipt = doc.insert({ text: 'new content' }, { changeMode: 'tracked' }); + // receipt.resolution.target contains the resolved insertion point + // receipt.inserted contains TrackedChangeAddress entries for the new change + + expect(receipt.resolution.target).toBeDefined(); + if (receipt.success) { + expect(receipt.inserted).toBeDefined(); + } + }); + }); + + describe('Workflow: Comment Thread Lifecycle', () => { + // Mirrors the exact code block from src/README.md § "Workflow: Comment Thread Lifecycle" + it('add comment, reply, then resolve', () => { + const doc = makeApi(); + + // Simulate having a find result in scope (the example assumes `result` exists) + const result = doc.find({ type: 'text', text: 'something' }); + const target = result.context?.[0]?.textRanges?.[0]; + const addReceipt = doc.comments.add({ target: target!, text: 'Review this section.' }); + // Use the comment ID from the receipt to reply + const comments = doc.comments.list(); + const thread = comments.matches[0]; + doc.comments.reply({ parentCommentId: thread.commentId, text: 'Looks good.' }); + doc.comments.resolve({ commentId: thread.commentId }); + + expect(addReceipt.success).toBe(true); + expect(thread.commentId).toBeDefined(); + }); + }); + + describe('Workflow: List Manipulation', () => { + // Mirrors the exact code block from src/README.md § "Workflow: List Manipulation" + it('insert list item, set type, indent', () => { + const doc = makeApi(); + + const lists = doc.lists.list(); + const firstItem = lists.matches[0]; + const insertResult = doc.lists.insert({ target: firstItem, position: 'after', text: 'New item' }); + if (insertResult.success) { + doc.lists.setType({ target: insertResult.item, kind: 'ordered' }); + doc.lists.indent({ target: insertResult.item }); + } + + expect(insertResult.success).toBe(true); + if (insertResult.success) { + expect(insertResult.item).toBeDefined(); + } + }); + }); + + describe('Workflow: Capabilities-Aware Branching', () => { + // Mirrors the exact code block from src/README.md § "Workflow: Capabilities-Aware Branching" + it('branch on per-operation capabilities', () => { + const doc = makeApi(); + const target = TEXT_TARGET; + + const caps = doc.capabilities(); + if (caps.operations['format.bold'].available) { + doc.format.bold({ target }); + } + if (caps.global.trackChanges.enabled) { + doc.insert({ text: 'tracked' }, { changeMode: 'tracked' }); + } + if (caps.operations['create.heading'].dryRun) { + const preview = doc.create.heading({ level: 2, text: 'Preview' }, { dryRun: true }); + expect(preview).toBeDefined(); + } + + expect(caps.operations['format.bold'].available).toBe(true); + expect(caps.global.trackChanges.enabled).toBe(true); + expect(caps.operations['create.heading'].dryRun).toBe(true); + }); + }); +}); From 7832a3f1a2c0df2a2f7918bfad0e74e809ac2045 Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Tue, 17 Feb 2026 21:44:02 -0800 Subject: [PATCH 5/5] fix(document-api): pass maxMatches Infinity to uncap text search results --- .../find-adapter.test.ts | 34 +++++++++++++++++-- .../find/text-strategy.ts | 1 + 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/super-editor/src/document-api-adapters/find-adapter.test.ts b/packages/super-editor/src/document-api-adapters/find-adapter.test.ts index 495e9cb47..3e9aaf222 100644 --- a/packages/super-editor/src/document-api-adapters/find-adapter.test.ts +++ b/packages/super-editor/src/document-api-adapters/find-adapter.test.ts @@ -687,7 +687,7 @@ describe('findAdapter — text selectors', () => { expect(capturedOptions!.caseSensitive).toBe(true); }); - it('does not cap maxMatches so pagination and scoping see the full result set', () => { + it('passes maxMatches: Infinity so pagination and scoping see the full result set', () => { let capturedOptions: Record | undefined; const doc = buildDoc({ typeName: 'paragraph', attrs: { sdBlockId: 'p1' }, nodeSize: 50, offset: 0 }); const search: SearchFn = (_pattern, options) => { @@ -700,7 +700,7 @@ describe('findAdapter — text selectors', () => { findAdapter(editor, query); expect(capturedOptions).toBeDefined(); - expect(capturedOptions!.maxMatches).toBeUndefined(); + expect(capturedOptions!.maxMatches).toBe(Infinity); }); it('throws when editor has no search command', () => { @@ -784,6 +784,36 @@ describe('findAdapter — text selectors', () => { expect(result.matches).toHaveLength(2); }); + it('supports paginating beyond 1000 matches when search default max is applied', () => { + const totalMatches = 1205; + const allMatches = Array.from({ length: totalMatches }, (_, index) => ({ + from: 8, + to: 9, + text: `m-${index}`, + })); + const doc = buildDoc( + 'a'.repeat(102), + { typeName: 'paragraph', attrs: { sdBlockId: 'p1' }, nodeSize: 50, offset: 0 }, + { typeName: 'paragraph', attrs: { sdBlockId: 'p2' }, nodeSize: 50, offset: 52 }, + ); + const search: SearchFn = (_pattern, options) => { + // Mirror editor.commands.search default behavior when maxMatches is omitted. + const max = (options as { maxMatches?: number })?.maxMatches ?? 1000; + return allMatches.slice(0, max); + }; + const editor = makeEditor(doc, search); + const query: Query = { + select: { type: 'text', pattern: 'test' }, + offset: 1001, + limit: 2, + }; + + const result = findAdapter(editor, query); + + expect(result.total).toBe(totalMatches); + expect(result.matches).toHaveLength(2); + }); + it('applies within filtering to the full text match set (not only the first 1000)', () => { const outsideScopeMatches = Array.from({ length: 1000 }, (_, index) => ({ from: 92, diff --git a/packages/super-editor/src/document-api-adapters/find/text-strategy.ts b/packages/super-editor/src/document-api-adapters/find/text-strategy.ts index c70357b6e..b3e6bdf41 100644 --- a/packages/super-editor/src/document-api-adapters/find/text-strategy.ts +++ b/packages/super-editor/src/document-api-adapters/find/text-strategy.ts @@ -90,6 +90,7 @@ export function executeTextSelector( const rawResult = search(pattern, { highlight: false, caseSensitive: selector.caseSensitive ?? false, + maxMatches: Infinity, }); if (!Array.isArray(rawResult)) {