Skip to content

Commit deff7e6

Browse files
authored
feat(document-api): selection primitives + multi-block comment targets (SD-2668) (#2924)
* feat(document-api): selection primitives + multi-block comment targets (SD-2668) Adds `editor.doc.selection.current()` and `editor.doc.selection.onChange()` so consumers can build custom toolbars, comments sidebars, and selection-driven UIs without reaching into ProseMirror internals. Widens `comments.create` target to accept `TextTarget` so multi-block selections anchor across blocks instead of silently collapsing to the first segment. - Consumers previously had to resolve PM positions through `editor.state.doc`, walk the PM tree for the containing block's `sdBlockId`, and convert to flattened-text offsets — ~30 lines of editor internals. `selection.current()` returns a portable `SelectionInfo { empty, target, activeMarks, text? }` with a multi-segment `TextTarget` ready for `comments.create`. - `comments.list().target` already used multi-segment `TextTarget`; the write side (`comments.create`) only accepted single-block `TextAddress`, so drag-selecting across paragraphs lost data with no warning. - Contract, schemas, dispatch, and adapter wired. Super-editor adapter projects PM selections into the flattened-text model via the existing `computeTextContentLength` helper. Part of the SD-2667 drop-in assessment umbrella. * fix(document-api): address PR review on selection + comments scope 1. Drop the aspirational `in: StoryLocator` parameter from `selection.current`. The adapter always read the live editor selection and merely copied `input.in` into the returned `TextTarget.story`, so a body selection could be mislabeled as a header/footer selection. The operation now documents that it always reflects whichever story holds focus; story-scoped selection reads are out of scope. 2. Narrow `comments.create` multi-segment handling to contiguous ranges. Validation previously accepted any non-empty segment array; the handler spanned `first.start → last.end` as a single PM range, so disjoint or out-of-order segments would silently anchor the comment over text the caller never selected. `addCommentHandler` now resolves every segment, rejects out-of-order pairs, and rejects any pair with non-empty text between them (`INVALID_TARGET`). 3. Fix schema inconsistency: `SelectionInfo.target` is `TextTarget | null` at the type level, but the published schema required it as an `object`. Schema now uses `oneOf: [textTargetSchema, { type: 'null' }]` so empty selections validate against the exported contract. 4. Make `SelectionAdapter` optional on `DocumentApiAdapters`. This is a public exported interface; adding a required member is a source breaking change for external adapter constructors. The factory now throws `SELECTION_ADAPTER_UNAVAILABLE` when selection operations are called without a registered adapter. Tests: add 3 new cases (multi-segment forward, missing-adapter for `current` + `onChange`). 1374 pass, 0 fail. * fix(document-api): correct selection offset mapping + cancel pending flush Two bot findings on PR 2924: - P1: `collectTextSegments` was deriving block-relative offsets with `selStart - blockStart`, treating raw PM positions as flattened text offsets. That's only equivalent when the block contains pure text; it diverges whenever the block has inline wrappers (e.g. `run` marks) or leaf atoms whose PM boundary tokens do not count in the flattened model. A selection inside a run would therefore return a `TextTarget` off by the number of wrapper boundary tokens, and `comments.create` using that target would anchor to the wrong text. Added `pmPositionToTextOffset(blockNode, blockPos, pmPos)` alongside the existing `resolveTextRangeInBlock` in `text-offset-resolver.ts` — it walks the block with the same flattened model (text = length, leaf = 1, block separator = 1, inline wrapper tokens = 0) and returns the correct offset. `collectTextSegments` now uses it for both endpoints. - P2: `subscribeToSelection` scheduled `flush` via `queueMicrotask` but the returned unsubscribe only detached listeners — a microtask already queued before cleanup would still fire, invoking the listener after unsubscribe returned (stale state updates on component unmount). Added a `cancelled` closure flag set by unsubscribe and checked in `flush` and `schedule`. Tests: 5 new cases for `pmPositionToTextOffset` covering plain text, inline wrapper transparency, leaf atoms with `nodeSize > 1`, before- block-start, and past-block-end. 11515 super-editor pass, 0 fail. * test(document-api): positive-path + write-side selection coverage Addresses PR review findings on test coverage: - Adds `selection-info-resolver.test.ts` (11 cases) covering `resolveCurrentSelectionInfo` projection (empty state, single-block selection, multi-block segment-per-touched-block, missing blockId, includeText on/off, active-marks empty path) and `subscribeToSelection` (listener fires once per tick, unsubscribe stops firing, queued-microtask-cancellation on unmount). - Adds 3 write-side cases to `comments-wrappers.test.ts` for the multi-segment path: out-of-order rejection with INVALID_TARGET / "document order" message, non-contiguous-gap rejection with "contiguous" message, and the contiguous-success path verifying the spanned PM range [first.from, last.to] is applied. - Updates `assemble-adapters.test.ts` to assert both `selection.current` and `selection.onChange` are wired on the adapter bag. - Adds a new section in `tests/consumer-typecheck/src/customer-scenario.ts` exercising the exported `editor.doc.selection.current()` surface, `SelectionInfo` destructuring, multi-segment `TextTarget` pass-through to `comments.create`, and the `onChange` subscription shape. Requires threading the new types through `packages/super-editor/src/index.ts` and `packages/superdoc/src/index.js` JSDoc typedefs so they are reachable from the `superdoc` package entrypoint. - Exempts `selection.onChange` from the contract-parity member-path check via `META_MEMBER_PATHS` — it is a subscription primitive, not a request/response operation, so it does not belong in `OPERATION_DEFINITIONS` / schemas / dispatch. Fixes the `validate` CI job that otherwise rejects the new runtime member. Deferred to SD-2671 (follow-up): - doc-api-stories/comments/multi-segment-target.ts (CLI-harness story) - doc-api-stories/selection story - Playwright behavior test that drives selection.current → comments.create Verified: document-api 1374 pass, super-editor 11529 pass, tests/consumer-typecheck compiles clean against the packed tarball. --------- Co-authored-by: Caio Pizzol <caio@superdoc.dev>
1 parent d0a36c2 commit deff7e6

35 files changed

Lines changed: 1546 additions & 33 deletions

apps/docs/document-api/available-operations.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Use the tables below to see what operations are available and where each one is
4141
| Query | 1 | 0 | 1 | [Reference](/document-api/reference/query/index) |
4242
| Ranges | 1 | 0 | 1 | [Reference](/document-api/reference/ranges/index) |
4343
| Sections | 18 | 0 | 18 | [Reference](/document-api/reference/sections/index) |
44+
| Selection | 1 | 0 | 1 | [Reference](/document-api/reference/selection/index) |
4445
| Styles | 1 | 0 | 1 | [Reference](/document-api/reference/styles/index) |
4546
| Table of Authorities | 11 | 0 | 11 | [Reference](/document-api/reference/authorities/index) |
4647
| Table of Contents | 10 | 0 | 10 | [Reference](/document-api/reference/toc/index) |
@@ -366,6 +367,7 @@ Use the tables below to see what operations are available and where each one is
366367
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.sections.setLinkToPrevious(...)</code></span> | [`sections.setLinkToPrevious`](/document-api/reference/sections/set-link-to-previous) |
367368
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.sections.setPageBorders(...)</code></span> | [`sections.setPageBorders`](/document-api/reference/sections/set-page-borders) |
368369
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.sections.clearPageBorders(...)</code></span> | [`sections.clearPageBorders`](/document-api/reference/sections/clear-page-borders) |
370+
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.selection.current(...)</code></span> | [`selection.current`](/document-api/reference/selection/current) |
369371
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.styles.apply(...)</code></span> | [`styles.apply`](/document-api/reference/styles/apply) |
370372
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.authorities.list(...)</code></span> | [`authorities.list`](/document-api/reference/authorities/list) |
371373
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.authorities.get(...)</code></span> | [`authorities.get`](/document-api/reference/authorities/get) |

apps/docs/document-api/reference/_generated-manifest.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,8 @@
355355
"apps/docs/document-api/reference/sections/set-section-direction.mdx",
356356
"apps/docs/document-api/reference/sections/set-title-page.mdx",
357357
"apps/docs/document-api/reference/sections/set-vertical-align.mdx",
358+
"apps/docs/document-api/reference/selection/current.mdx",
359+
"apps/docs/document-api/reference/selection/index.mdx",
358360
"apps/docs/document-api/reference/styles/apply.mdx",
359361
"apps/docs/document-api/reference/styles/index.mdx",
360362
"apps/docs/document-api/reference/styles/paragraph/clear-style.mdx",
@@ -989,6 +991,13 @@
989991
"pagePath": "apps/docs/document-api/reference/ranges/index.mdx",
990992
"title": "Ranges"
991993
},
994+
{
995+
"aliasMemberPaths": [],
996+
"key": "selection",
997+
"operationIds": ["selection.current"],
998+
"pagePath": "apps/docs/document-api/reference/selection/index.mdx",
999+
"title": "Selection"
1000+
},
9921001
{
9931002
"aliasMemberPaths": [],
9941003
"key": "diff",
@@ -1018,5 +1027,5 @@
10181027
}
10191028
],
10201029
"marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}",
1021-
"sourceHash": "c8670fb494b56c19fbd09a7bada35974fbb3c22d938f6a5e01eee6e8467961c0"
1030+
"sourceHash": "f5c0786256e77432e9b9a58bc2009e39e2e832f6b0b2b625ee6dbeb3a762bdd6"
10221031
}

apps/docs/document-api/reference/capabilities/get.mdx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1895,6 +1895,11 @@ _No fields._
18951895
| `operations.sections.setVerticalAlign.dryRun` | boolean | yes | |
18961896
| `operations.sections.setVerticalAlign.reasons` | enum[] | no | |
18971897
| `operations.sections.setVerticalAlign.tracked` | boolean | yes | |
1898+
| `operations.selection.current` | object | yes | |
1899+
| `operations.selection.current.available` | boolean | yes | |
1900+
| `operations.selection.current.dryRun` | boolean | yes | |
1901+
| `operations.selection.current.reasons` | enum[] | no | |
1902+
| `operations.selection.current.tracked` | boolean | yes | |
18981903
| `operations.styles.apply` | object | yes | |
18991904
| `operations.styles.apply.available` | boolean | yes | |
19001905
| `operations.styles.apply.dryRun` | boolean | yes | |
@@ -4116,6 +4121,11 @@ _No fields._
41164121
"dryRun": true,
41174122
"tracked": false
41184123
},
4124+
"selection.current": {
4125+
"available": true,
4126+
"dryRun": false,
4127+
"tracked": false
4128+
},
41194129
"styles.apply": {
41204130
"available": true,
41214131
"dryRun": true,
@@ -17469,6 +17479,41 @@ _No fields._
1746917479
],
1747017480
"type": "object"
1747117481
},
17482+
"selection.current": {
17483+
"additionalProperties": false,
17484+
"properties": {
17485+
"available": {
17486+
"type": "boolean"
17487+
},
17488+
"dryRun": {
17489+
"type": "boolean"
17490+
},
17491+
"reasons": {
17492+
"items": {
17493+
"enum": [
17494+
"COMMAND_UNAVAILABLE",
17495+
"HELPER_UNAVAILABLE",
17496+
"OPERATION_UNAVAILABLE",
17497+
"TRACKED_MODE_UNAVAILABLE",
17498+
"DRY_RUN_UNAVAILABLE",
17499+
"NAMESPACE_UNAVAILABLE",
17500+
"STYLES_PART_MISSING",
17501+
"COLLABORATION_ACTIVE"
17502+
]
17503+
},
17504+
"type": "array"
17505+
},
17506+
"tracked": {
17507+
"type": "boolean"
17508+
}
17509+
},
17510+
"required": [
17511+
"available",
17512+
"tracked",
17513+
"dryRun"
17514+
],
17515+
"type": "object"
17516+
},
1747217517
"styles.apply": {
1747317518
"additionalProperties": false,
1747417519
"properties": {
@@ -19756,6 +19801,7 @@ _No fields._
1975619801
"trackChanges.decide",
1975719802
"query.match",
1975819803
"ranges.resolve",
19804+
"selection.current",
1975919805
"mutations.preview",
1976019806
"mutations.apply",
1976119807
"capabilities.get",

apps/docs/document-api/reference/comments/create.mdx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,7 @@ Returns a Receipt confirming the comment was created; reports NO_OP if the ancho
2727
| Field | Type | Required | Description |
2828
| --- | --- | --- | --- |
2929
| `parentCommentId` | string | no | |
30-
| `target` | TextAddress | no | TextAddress |
31-
| `target.blockId` | string | no | |
32-
| `target.kind` | `"text"` | no | Constant: `"text"` |
33-
| `target.range` | Range | no | Range |
34-
| `target.range.end` | integer | no | |
35-
| `target.range.start` | integer | no | |
30+
| `target` | TextAddress \\| TextTarget | no | One of: TextAddress, TextTarget |
3631
| `text` | string | yes | |
3732

3833
### Example request
@@ -118,8 +113,15 @@ Returns a Receipt confirming the comment was created; reports NO_OP if the ancho
118113
"type": "string"
119114
},
120115
"target": {
121-
"$ref": "#/$defs/TextAddress",
122-
"description": "Text range to anchor the comment: {kind:'text', blockId:'...', range:{start:N, end:N}}."
116+
"description": "Text range to anchor the comment. Accepts either a single-block TextAddress {kind:'text', blockId, range} or a multi-segment TextTarget {kind:'text', segments:[{blockId, range}, ...]} for selections that span blocks.",
117+
"oneOf": [
118+
{
119+
"$ref": "#/$defs/TextAddress"
120+
},
121+
{
122+
"$ref": "#/$defs/TextTarget"
123+
}
124+
]
123125
},
124126
"text": {
125127
"description": "Comment text content.",

apps/docs/document-api/reference/index.mdx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ This reference is sourced from `packages/document-api/src/contract/*`.
4949
| Citations | 15 | 0 | 15 | [Open](/document-api/reference/citations/index) |
5050
| Table of Authorities | 11 | 0 | 11 | [Open](/document-api/reference/authorities/index) |
5151
| Ranges | 1 | 0 | 1 | [Open](/document-api/reference/ranges/index) |
52+
| Selection | 1 | 0 | 1 | [Open](/document-api/reference/selection/index) |
5253
| Diff | 3 | 0 | 3 | [Open](/document-api/reference/diff/index) |
5354
| Protection | 3 | 0 | 3 | [Open](/document-api/reference/protection/index) |
5455
| Permission Ranges | 5 | 0 | 5 | [Open](/document-api/reference/permission-ranges/index) |
@@ -583,6 +584,12 @@ The tables below are grouped by namespace.
583584
| --- | --- | --- |
584585
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><a href="/document-api/reference/ranges/resolve"><code>ranges.resolve</code></a></span> | <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.ranges.resolve(...)</code></span> | Resolve two explicit anchors into a contiguous document range. Returns a transparent SelectionTarget, a mutation-ready ref, and preview metadata. Stateless and deterministic. |
585586

587+
#### Selection
588+
589+
| Operation | API member path | Description |
590+
| --- | --- | --- |
591+
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><a href="/document-api/reference/selection/current"><code>selection.current</code></a></span> | <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.selection.current(...)</code></span> | Read the editor's current selection as a portable SelectionInfo with a text-anchored TextTarget. Primitive for building custom comments UIs, floating toolbars, and other selection-driven components without reaching into ProseMirror internals. |
592+
586593
#### Diff
587594

588595
| Operation | API member path | Description |
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
---
2+
title: selection.current
3+
sidebarTitle: selection.current
4+
description: "Read the editor's current selection as a portable SelectionInfo with a text-anchored TextTarget. Primitive for building custom comments UIs, floating toolbars, and other selection-driven components without reaching into ProseMirror internals."
5+
---
6+
7+
{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
8+
9+
## Summary
10+
11+
Read the editor's current selection as a portable SelectionInfo with a text-anchored TextTarget. Primitive for building custom comments UIs, floating toolbars, and other selection-driven components without reaching into ProseMirror internals.
12+
13+
- Operation ID: `selection.current`
14+
- API member path: `editor.doc.selection.current(...)`
15+
- Mutates document: `no`
16+
- Idempotency: `idempotent`
17+
- Supports tracked mode: `no`
18+
- Supports dry run: `no`
19+
- Deterministic target resolution: `yes`
20+
21+
## Expected result
22+
23+
Returns a SelectionInfo with `empty`, `target` (TextTarget or null), `activeMarks`, and optionally `text` when `includeText: true`.
24+
25+
## Input fields
26+
27+
| Field | Type | Required | Description |
28+
| --- | --- | --- | --- |
29+
| `includeText` | boolean | no | |
30+
31+
### Example request
32+
33+
```json
34+
{
35+
"includeText": true
36+
}
37+
```
38+
39+
## Output fields
40+
41+
| Field | Type | Required | Description |
42+
| --- | --- | --- | --- |
43+
| `activeMarks` | string[] | yes | |
44+
| `empty` | boolean | yes | |
45+
| `target` | TextTarget \\| null | yes | One of: TextTarget, null |
46+
| `text` | string | no | |
47+
48+
### Example response
49+
50+
```json
51+
{
52+
"activeMarks": [
53+
"example"
54+
],
55+
"empty": true,
56+
"target": {
57+
"kind": "text",
58+
"segments": [
59+
{
60+
"blockId": "block-abc123",
61+
"range": {
62+
"end": 10,
63+
"start": 0
64+
}
65+
}
66+
]
67+
},
68+
"text": "Hello, world."
69+
}
70+
```
71+
72+
## Pre-apply throws
73+
74+
- `INVALID_INPUT`
75+
- `INVALID_CONTEXT`
76+
77+
## Non-applied failure codes
78+
79+
- None
80+
81+
## Raw schemas
82+
83+
<Accordion title="Raw input schema">
84+
```json
85+
{
86+
"additionalProperties": false,
87+
"properties": {
88+
"includeText": {
89+
"type": "boolean"
90+
}
91+
},
92+
"type": "object"
93+
}
94+
```
95+
</Accordion>
96+
97+
<Accordion title="Raw output schema">
98+
```json
99+
{
100+
"additionalProperties": false,
101+
"properties": {
102+
"activeMarks": {
103+
"items": {
104+
"type": "string"
105+
},
106+
"type": "array"
107+
},
108+
"empty": {
109+
"type": "boolean"
110+
},
111+
"target": {
112+
"oneOf": [
113+
{
114+
"$ref": "#/$defs/TextTarget"
115+
},
116+
{
117+
"type": "null"
118+
}
119+
]
120+
},
121+
"text": {
122+
"type": "string"
123+
}
124+
},
125+
"required": [
126+
"empty",
127+
"target",
128+
"activeMarks"
129+
],
130+
"type": "object"
131+
}
132+
```
133+
</Accordion>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
title: Selection operations
3+
sidebarTitle: Selection
4+
description: Selection operation reference from the canonical Document API contract.
5+
---
6+
7+
{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
8+
9+
[Back to full reference](../index)
10+
11+
Read the editor's current selection as a portable, addressable target.
12+
13+
| Operation | Member path | Mutates | Idempotency | Tracked | Dry run |
14+
| --- | --- | --- | --- | --- | --- |
15+
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><a href="/document-api/reference/selection/current"><code>selection.current</code></a></span> | `selection.current` | No | `idempotent` | No | No |
16+

apps/docs/document-engine/sdks.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,7 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p
571571
| `doc.getHtml` | `get-html` | Extract the document content as an HTML string. |
572572
| `doc.markdownToFragment` | `markdown-to-fragment` | Convert a Markdown string into an SDM/1 structural fragment. |
573573
| `doc.info` | `info` | Return document summary info including word, character, paragraph, heading, table, image, comment, tracked-change, SDT-field, list, and page counts, plus outline and capabilities. |
574+
| `doc.extract` | `extract` | Extract all document content with stable IDs for RAG pipelines. Returns blocks with full text, comments, and tracked changes — each with an ID compatible with scrollToElement(). |
574575
| `doc.clearContent` | `clear-content` | Clear all document body content, leaving a single empty paragraph. |
575576
| `doc.insert` | `insert` | Insert content into the document. Two input shapes: text-based (value + type) inserts inline content at a SelectionTarget or ref position within an existing block; structural SDFragment (content) inserts one or more blocks as siblings relative to a BlockNodeAddress target. When target/ref is omitted, content appends at the end of the document. Text mode supports text (default), markdown, and html content types via the `type` field. Structural mode uses `placement` (before/after/insideStart/insideEnd) to position relative to the target block. |
576577
| `doc.replace` | `replace` | Replace content at a contiguous document selection. Text path accepts a SelectionTarget or ref plus replacement text. Structural path accepts a BlockNodeAddress (replaces whole block), SelectionTarget (expands to full covered block boundaries), or ref plus SDFragment content. |
@@ -580,6 +581,7 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p
580581
| `doc.blocks.deleteRange` | `blocks delete-range` | Delete a contiguous range of top-level blocks between two endpoints (inclusive). Both endpoints must be direct children of the document node. Supports dry-run preview. |
581582
| `doc.query.match` | `query match` | Deterministic selector-based search returning mutation-grade addresses and text ranges. Use this to discover targets before any mutation. |
582583
| `doc.ranges.resolve` | `ranges resolve` | Resolve two explicit anchors into a contiguous document range. Returns a transparent SelectionTarget, a mutation-ready ref, and preview metadata. Stateless and deterministic. |
584+
| `doc.selection.current` | `selection current` | Read the editor's current selection as a portable SelectionInfo with a text-anchored TextTarget. Primitive for building custom comments UIs, floating toolbars, and other selection-driven components without reaching into ProseMirror internals. |
583585
| `doc.mutations.preview` | `mutations preview` | Dry-run a mutation plan, returning resolved targets without applying changes. |
584586
| `doc.mutations.apply` | `mutations apply` | Execute a mutation plan atomically against the document. |
585587
| `doc.capabilities.get` | `capabilities` | Query runtime capabilities supported by the current document engine. |
@@ -1031,6 +1033,7 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p
10311033
| `doc.get_html` | `get-html` | Extract the document content as an HTML string. |
10321034
| `doc.markdown_to_fragment` | `markdown-to-fragment` | Convert a Markdown string into an SDM/1 structural fragment. |
10331035
| `doc.info` | `info` | Return document summary info including word, character, paragraph, heading, table, image, comment, tracked-change, SDT-field, list, and page counts, plus outline and capabilities. |
1036+
| `doc.extract` | `extract` | Extract all document content with stable IDs for RAG pipelines. Returns blocks with full text, comments, and tracked changes — each with an ID compatible with scrollToElement(). |
10341037
| `doc.clear_content` | `clear-content` | Clear all document body content, leaving a single empty paragraph. |
10351038
| `doc.insert` | `insert` | Insert content into the document. Two input shapes: text-based (value + type) inserts inline content at a SelectionTarget or ref position within an existing block; structural SDFragment (content) inserts one or more blocks as siblings relative to a BlockNodeAddress target. When target/ref is omitted, content appends at the end of the document. Text mode supports text (default), markdown, and html content types via the `type` field. Structural mode uses `placement` (before/after/insideStart/insideEnd) to position relative to the target block. |
10361039
| `doc.replace` | `replace` | Replace content at a contiguous document selection. Text path accepts a SelectionTarget or ref plus replacement text. Structural path accepts a BlockNodeAddress (replaces whole block), SelectionTarget (expands to full covered block boundaries), or ref plus SDFragment content. |
@@ -1040,6 +1043,7 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p
10401043
| `doc.blocks.delete_range` | `blocks delete-range` | Delete a contiguous range of top-level blocks between two endpoints (inclusive). Both endpoints must be direct children of the document node. Supports dry-run preview. |
10411044
| `doc.query.match` | `query match` | Deterministic selector-based search returning mutation-grade addresses and text ranges. Use this to discover targets before any mutation. |
10421045
| `doc.ranges.resolve` | `ranges resolve` | Resolve two explicit anchors into a contiguous document range. Returns a transparent SelectionTarget, a mutation-ready ref, and preview metadata. Stateless and deterministic. |
1046+
| `doc.selection.current` | `selection current` | Read the editor's current selection as a portable SelectionInfo with a text-anchored TextTarget. Primitive for building custom comments UIs, floating toolbars, and other selection-driven components without reaching into ProseMirror internals. |
10431047
| `doc.mutations.preview` | `mutations preview` | Dry-run a mutation plan, returning resolved targets without applying changes. |
10441048
| `doc.mutations.apply` | `mutations apply` | Execute a mutation plan atomically against the document. |
10451049
| `doc.capabilities.get` | `capabilities` | Query runtime capabilities supported by the current document engine. |

packages/document-api/scripts/check-contract-parity.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,17 @@ import { OPERATION_DEFINITIONS } from '../src/contract/operation-definitions.js'
2121
import { OPERATION_REFERENCE_DOC_PATH_MAP } from '../src/contract/reference-doc-map.js';
2222
import { buildDispatchTable } from '../src/invoke/invoke.js';
2323

24-
/** Meta-methods and helper methods on DocumentApi that are not contract operations. */
25-
const META_MEMBER_PATHS = ['invoke', ...REFERENCE_OPERATION_ALIASES.map((alias) => alias.memberPath)];
24+
/**
25+
* Meta-methods and helper methods on DocumentApi that are not contract
26+
* operations. `selection.onChange` is a subscription primitive (push-based,
27+
* no request/response shape) rather than a request-response operation, so
28+
* it is not represented in OPERATION_DEFINITIONS / schemas / dispatch.
29+
*/
30+
const META_MEMBER_PATHS = [
31+
'invoke',
32+
'selection.onChange',
33+
...REFERENCE_OPERATION_ALIASES.map((alias) => alias.memberPath),
34+
];
2635

2736
function collectFunctionMemberPaths(value: unknown, prefix = ''): string[] {
2837
if (!value || typeof value !== 'object') return [];

0 commit comments

Comments
 (0)