Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/docs/document-api/reference/_generated-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,5 +119,5 @@
}
],
"marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}",
"sourceHash": "ef0d931b0ccec18b0cc91efee5ed117a0d3328adc8406faaab4119e2f07b2ca5"
"sourceHash": "81ed2589e4d1277639235807ef0028264465e71a2a01ec9f5e86a8c2f5755cef"
}
8 changes: 6 additions & 2 deletions apps/docs/document-api/reference/comments/get.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ description: Reference for comments.get
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `address` | CommentAddress | yes | CommentAddress |
| `anchoredText` | string | no | |
| `commentId` | string | yes | |
| `createdTime` | number | no | |
| `creatorEmail` | string | no | |
Expand All @@ -45,7 +46,7 @@ description: Reference for comments.get
| `isInternal` | boolean | no | |
| `parentCommentId` | string | no | |
| `status` | enum | yes | `"open"`, `"resolved"` |
| `target` | TextAddress | no | TextAddress |
| `target` | TextTarget | no | TextTarget |
| `text` | string | no | |

### Example response
Expand Down Expand Up @@ -99,6 +100,9 @@ description: Reference for comments.get
"address": {
"$ref": "#/$defs/CommentAddress"
},
"anchoredText": {
"type": "string"
},
"commentId": {
"type": "string"
},
Expand Down Expand Up @@ -127,7 +131,7 @@ description: Reference for comments.get
]
},
"target": {
"$ref": "#/$defs/TextAddress"
"$ref": "#/$defs/TextTarget"
},
"text": {
"type": "string"
Expand Down
5 changes: 4 additions & 1 deletion apps/docs/document-api/reference/comments/list.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ description: Reference for comments.list
"address": {
"$ref": "#/$defs/CommentAddress"
},
"anchoredText": {
"type": "string"
},
"createdTime": {
"type": "number"
},
Expand Down Expand Up @@ -152,7 +155,7 @@ description: Reference for comments.list
]
},
"target": {
"$ref": "#/$defs/TextAddress"
"$ref": "#/$defs/TextTarget"
},
"text": {
"type": "string"
Expand Down
8 changes: 5 additions & 3 deletions packages/document-api/src/comments/comments.types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CommentAddress, CommentStatus, TextAddress } from '../types/index.js';
import type { CommentAddress, CommentStatus, TextTarget } from '../types/index.js';
import type { DiscoveryOutput } from '../types/discovery.js';

export type { CommentStatus } from '../types/index.js';
Expand All @@ -11,7 +11,8 @@ export interface CommentInfo {
text?: string;
isInternal?: boolean;
status: CommentStatus;
target?: TextAddress;
target?: TextTarget;
anchoredText?: string;
createdTime?: number;
creatorName?: string;
creatorEmail?: string;
Expand All @@ -36,7 +37,8 @@ export interface CommentDomain {
text?: string;
isInternal?: boolean;
status: CommentStatus;
target?: TextAddress;
target?: TextTarget;
anchoredText?: string;
createdTime?: number;
creatorName?: string;
creatorEmail?: string;
Expand Down
21 changes: 19 additions & 2 deletions packages/document-api/src/contract/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,20 @@ const SHARED_DEFS: Record<string, JsonSchema> = {
},
['kind', 'blockId', 'range'],
),
TextSegment: objectSchema(
{
blockId: { type: 'string' },
range: ref('Range'),
},
['blockId', 'range'],
),
TextTarget: objectSchema(
{
kind: { const: 'text' },
segments: { type: 'array', items: ref('TextSegment'), minItems: 1 },
},
['kind', 'segments'],
),
BlockNodeAddress: objectSchema(
{
kind: { const: 'block' },
Expand Down Expand Up @@ -284,6 +298,7 @@ const positionSchema = ref('Position');
const inlineAnchorSchema = ref('InlineAnchor');
const targetKindSchema = ref('TargetKind');
const textAddressSchema = ref('TextAddress');
const textTargetSchema = ref('TextTarget');
const blockNodeAddressSchema = ref('BlockNodeAddress');
const paragraphAddressSchema = ref('ParagraphAddress');
const headingAddressSchema = ref('HeadingAddress');
Expand Down Expand Up @@ -724,7 +739,8 @@ const commentInfoSchema = objectSchema(
text: { type: 'string' },
isInternal: { type: 'boolean' },
status: { enum: ['open', 'resolved'] },
target: textAddressSchema,
target: textTargetSchema,
anchoredText: { type: 'string' },
createdTime: { type: 'number' },
creatorName: { type: 'string' },
creatorEmail: { type: 'string' },
Expand All @@ -740,7 +756,8 @@ const commentDomainItemSchema = discoveryItemSchema(
text: { type: 'string' },
isInternal: { type: 'boolean' },
status: { enum: ['open', 'resolved'] },
target: textAddressSchema,
target: textTargetSchema,
anchoredText: { type: 'string' },
createdTime: { type: 'number' },
creatorName: { type: 'string' },
creatorEmail: { type: 'string' },
Expand Down
29 changes: 29 additions & 0 deletions packages/document-api/src/types/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,35 @@ export type TextAddress = {
range: Range;
};

/**
* A single anchored text segment within one block.
*
* Unlike {@link TextAddress} (used for mutation inputs), TextSegment is a
* lightweight component of a {@link TextTarget} — it carries no `kind`
* discriminant because the parent TextTarget already provides it.
*/
export type TextSegment = {
blockId: string;
range: Range;
};

/**
* Multi-segment text target returned by comment read operations.
*
* A single comment can span multiple discontinuous text ranges (e.g. when Word
* applies the same comment ID across separate marked runs or across blocks).
* TextTarget faithfully represents all anchored segments in document order.
*
* Invariants:
* - `segments` is non-empty (at least one segment).
* - Segments are sorted in document order.
* - Segment bounds are valid integers (start >= 0, start <= end).
*/
export type TextTarget = {
kind: 'text';
segments: [TextSegment, ...TextSegment[]];
};

export type EntityType = 'comment' | 'trackedChange';

export type CommentAddress = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,11 @@ describe('getDocumentApiCapabilities', () => {
it('does not emit unavailable reasons for modes that are unsupported by design', () => {
const capabilities = getDocumentApiCapabilities(makeEditor());
const setTypeReasons = capabilities.operations['lists.setType'].reasons ?? [];
const decideReasons = capabilities.operations['trackChanges.decide'].reasons ?? [];
const trackChangesDecideReasons = capabilities.operations['trackChanges.decide'].reasons ?? [];

expect(setTypeReasons).not.toContain('TRACKED_MODE_UNAVAILABLE');
expect(setTypeReasons).not.toContain('DRY_RUN_UNAVAILABLE');
expect(decideReasons).not.toContain('DRY_RUN_UNAVAILABLE');
expect(trackChangesDecideReasons).not.toContain('DRY_RUN_UNAVAILABLE');
});

it('handles an editor with undefined schema gracefully', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest';
import type { Editor } from '../../core/Editor.js';
import type { TextTarget } from '@superdoc/document-api';
import {
buildCommentJsonFromText,
extractCommentText,
Expand Down Expand Up @@ -236,8 +237,18 @@ describe('toCommentInfo', () => {
});

it('includes target when provided', () => {
const target = { kind: 'text' as const, blockId: 'p1', range: { start: 0, end: 5 } };
const target: TextTarget = { kind: 'text', segments: [{ blockId: 'p1', range: { start: 0, end: 5 } }] };
const info = toCommentInfo({ commentId: 'c1' }, { target });
expect(info.target).toBe(target);
});

it('includes anchoredText when provided', () => {
const info = toCommentInfo({ commentId: 'c1' }, { anchoredText: 'hello world' });
expect(info.anchoredText).toBe('hello world');
});

it('omits anchoredText when not provided', () => {
const info = toCommentInfo({ commentId: 'c1' });
expect(info.anchoredText).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Editor } from '../../core/Editor.js';
import type { CommentInfo, CommentStatus, TextAddress } from '@superdoc/document-api';
import type { CommentInfo, CommentStatus, TextTarget } from '@superdoc/document-api';

const FALLBACK_STORE_KEY = '__documentApiComments';

Expand Down Expand Up @@ -185,8 +185,9 @@ export function isCommentResolved(entry: CommentEntityRecord): boolean {
export function toCommentInfo(
entry: CommentEntityRecord,
options: {
target?: TextAddress;
target?: TextTarget;
status?: CommentStatus;
anchoredText?: string;
} = {},
): CommentInfo {
const resolvedId = typeof entry.commentId === 'string' ? entry.commentId : String(entry.importedId ?? '');
Expand All @@ -205,6 +206,7 @@ export function toCommentInfo(
isInternal: typeof entry.isInternal === 'boolean' ? entry.isInternal : undefined,
status,
target: options.target,
anchoredText: options.anchoredText,
createdTime: typeof entry.createdTime === 'number' ? entry.createdTime : undefined,
creatorName: typeof entry.creatorName === 'string' ? entry.creatorName : undefined,
creatorEmail: typeof entry.creatorEmail === 'string' ? entry.creatorEmail : undefined,
Expand Down
Loading
Loading