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
13 changes: 12 additions & 1 deletion apps/docs/core/superdoc/events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ superdoc.on('editorDestroy', () => {

### `editor-update`

When editor content changes.
When editor content changes. Use this to refresh live UI state like word counts or auto-save.

<CodeGroup>
```javascript Usage
Expand All @@ -167,6 +167,17 @@ superdoc.on('editor-update', ({ editor }) => {
```
</CodeGroup>

**Live counter example:** Read `editor.doc.info()` inside the handler to build a live document-stats panel without polling.

```javascript
superdoc.on('editor-update', ({ editor }) => {
const { counts } = editor.doc.info();
document.getElementById('stats').textContent =
`${counts.words} words, ${counts.characters} characters, ` +
`${counts.trackedChanges} tracked changes, ${counts.lists} lists`;
});
```

### `content-error`

When content processing fails.
Expand Down
44 changes: 44 additions & 0 deletions apps/docs/core/supereditor/events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,50 @@ const editor = await Editor.open(yourFile, {
```
</CodeGroup>

## Subscribing after initialization

Use `editor.on(...)` and `editor.off(...)` to subscribe to events at any time after the editor is created. This is useful for adding listeners from external code that does not control the initial configuration.

<CodeGroup>
```javascript Usage
editor.on('update', ({ editor }) => {
const { counts } = editor.doc.info();
updateDocumentStatsUI({
words: counts.words,
characters: counts.characters,
trackedChanges: counts.trackedChanges,
sdtFields: counts.sdtFields,
lists: counts.lists,
});
});
```

```javascript Full Example
import { Editor } from 'superdoc/super-editor';

const editor = await Editor.open(yourFile, {
element: document.querySelector('#editor'),
});

// Subscribe to updates after creation
const handler = ({ editor }) => {
const { counts } = editor.doc.info();
document.getElementById('stats').textContent =
`${counts.words} words, ${counts.characters} characters, ` +
`${counts.trackedChanges} tracked changes`;
};

editor.on('update', handler);

// Later, unsubscribe
editor.off('update', handler);
```
</CodeGroup>

<Info>
Constructor callbacks like `onUpdate` and runtime subscriptions like `editor.on('update', ...)` both fire on the same events. Use constructor callbacks when the listener is known at creation time, and `editor.on(...)` when adding listeners dynamically.
</Info>

## Features

### `onCommentsUpdate`
Expand Down
67 changes: 67 additions & 0 deletions apps/docs/document-api/common-workflows.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,73 @@ editor2.destroy();
No ID is guaranteed to survive all Microsoft Word round-trips. Re-extract addresses after major external edits or transformations, since Word (or other tools) may rewrite paragraph IDs and SuperDoc may rewrite duplicate IDs on import.
</Warning>

## Read document counts

`doc.info()` returns a snapshot of current document statistics including word, character, paragraph, heading, table, image, comment, tracked-change, SDT-field, and list counts.

```ts
const info = editor.doc.info();

console.log(info.counts.words); // whitespace-delimited word count
console.log(info.counts.characters); // full text projection length (with spaces)
console.log(info.counts.paragraphs); // excludes headings and list items
console.log(info.counts.headings); // style-based heading detection
console.log(info.counts.tables); // top-level table containers
console.log(info.counts.images); // block + inline images
console.log(info.counts.comments); // unique anchored comment IDs
console.log(info.counts.trackedChanges); // grouped tracked-change entities
console.log(info.counts.sdtFields); // field-like SDT/content-control nodes
console.log(info.counts.lists); // unique list sequences
```

All counts reflect the current editor state, not OOXML metadata. They update naturally as the document changes.

### Build a live counter in the browser

`doc.info()` is a snapshot read. To build a live counter, subscribe to document-change events and refresh counts in the handler — do not poll in a render loop.

**SuperEditor (raw editor):**

```ts
editor.on('update', ({ editor }) => {
const { counts } = editor.doc.info();
updateDocumentStatsUI({
words: counts.words,
characters: counts.characters,
trackedChanges: counts.trackedChanges,
sdtFields: counts.sdtFields,
lists: counts.lists,
});
});
```

**SuperDoc (wrapper):**

```ts
superdoc.on('editor-update', ({ editor }) => {
const { counts } = editor.doc.info();
updateDocumentStatsUI({
words: counts.words,
characters: counts.characters,
trackedChanges: counts.trackedChanges,
sdtFields: counts.sdtFields,
lists: counts.lists,
});
});
```

### SDK usage

The SDKs do not expose browser event subscriptions. Call `doc.info()` at workflow boundaries — after opening a document, after a batch of mutations, or before saving.

```ts
const doc = await client.open('./contract.docx');
const info = doc.info();
console.log(
`${info.counts.words} words, ${info.counts.characters} characters, ${info.counts.trackedChanges} tracked changes`,
);
```

## Dry-run preview

Pass `dryRun: true` to validate an operation without applying it:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -962,5 +962,5 @@
}
],
"marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}",
"sourceHash": "bf7e9b493d8ab9e84c2d5875b5aa7fe0e74e8504ec3e258e463af5e529b16e92"
"sourceHash": "9197780d09944c67a656339ee8adc0e2c4473d1dffb09288c9c0b85f68fd34f3"
}
2 changes: 1 addition & 1 deletion apps/docs/document-api/reference/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ The tables below are grouped by namespace.
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><a href="/document-api/reference/get-markdown"><code>getMarkdown</code></a></span> | <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.getMarkdown(...)</code></span> | Extract the document content as a Markdown string. |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><a href="/document-api/reference/get-html"><code>getHtml</code></a></span> | <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.getHtml(...)</code></span> | Extract the document content as an HTML string. |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><a href="/document-api/reference/markdown-to-fragment"><code>markdownToFragment</code></a></span> | <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.markdownToFragment(...)</code></span> | Convert a Markdown string into an SDM/1 structural fragment. |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><a href="/document-api/reference/info"><code>info</code></a></span> | <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.info(...)</code></span> | Return document metadata including revision, node count, and capabilities. |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><a href="/document-api/reference/info"><code>info</code></a></span> | <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.info(...)</code></span> | Return document summary info including word, character, paragraph, heading, table, image, comment, tracked-change, SDT-field, and list counts, plus outline and capabilities. |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><a href="/document-api/reference/clear-content"><code>clearContent</code></a></span> | <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.clearContent(...)</code></span> | Clear all document body content, leaving a single empty paragraph. |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><a href="/document-api/reference/insert"><code>insert</code></a></span> | <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.insert(...)</code></span> | Insert content into the document. Two input shapes: legacy string-based (value + type) inserts inline content at a text position within an existing block; structural SDFragment (content) inserts one or more blocks as siblings relative to a BlockNodeAddress target. When target is omitted, content appends at the end of the document. Legacy 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. |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><a href="/document-api/reference/replace"><code>replace</code></a></span> | <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.replace(...)</code></span> | 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. |
Expand Down
32 changes: 28 additions & 4 deletions apps/docs/document-api/reference/info.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: info
sidebarTitle: info
description: Return document metadata including revision, node count, and capabilities.
description: Return document summary info including word, character, paragraph, heading, table, image, comment, tracked-change, SDT-field, and list counts, plus outline and capabilities.
---

{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
Expand All @@ -10,7 +10,7 @@ description: Return document metadata including revision, node count, and capabi

## Summary

Return document metadata including revision, node count, and capabilities.
Return document summary info including word, character, paragraph, heading, table, image, comment, tracked-change, SDT-field, and list counts, plus outline and capabilities.

- Operation ID: `info`
- API member path: `editor.doc.info(...)`
Expand All @@ -22,7 +22,7 @@ Return document metadata including revision, node count, and capabilities.

## Expected result

Returns a DocumentInfo object with revision, word/paragraph/heading counts, and capability flags.
Returns a DocumentInfo object with counts (words, characters, paragraphs, headings, tables, images, comments, trackedChanges, sdtFields, lists), document outline, capability flags, and revision.

## Input fields

Expand All @@ -44,11 +44,15 @@ _No fields._
| `capabilities.canGetNode` | boolean | yes | |
| `capabilities.canReplace` | boolean | yes | |
| `counts` | object | yes | |
| `counts.characters` | integer | yes | |
| `counts.comments` | integer | yes | |
| `counts.headings` | integer | yes | |
| `counts.images` | integer | yes | |
| `counts.lists` | integer | yes | |
| `counts.paragraphs` | integer | yes | |
| `counts.sdtFields` | integer | yes | |
| `counts.tables` | integer | yes | |
| `counts.trackedChanges` | integer | yes | |
| `counts.words` | integer | yes | |
| `outline` | object[] | yes | |
| `revision` | string | yes | |
Expand All @@ -64,11 +68,15 @@ _No fields._
"canReplace": true
},
"counts": {
"characters": 1,
"comments": 0,
"headings": 3,
"images": 2,
"lists": 1,
"paragraphs": 12,
"sdtFields": 1,
"tables": 1,
"trackedChanges": 1,
"words": 250
},
"outline": [
Expand Down Expand Up @@ -134,6 +142,9 @@ _No fields._
"counts": {
"additionalProperties": false,
"properties": {
"characters": {
"type": "integer"
},
"comments": {
"type": "integer"
},
Expand All @@ -143,23 +154,36 @@ _No fields._
"images": {
"type": "integer"
},
"lists": {
"type": "integer"
},
"paragraphs": {
"type": "integer"
},
"sdtFields": {
"type": "integer"
},
"tables": {
"type": "integer"
},
"trackedChanges": {
"type": "integer"
},
"words": {
"type": "integer"
}
},
"required": [
"words",
"characters",
"paragraphs",
"headings",
"tables",
"images",
"comments"
"comments",
"trackedChanges",
"sdtFields",
"lists"
],
"type": "object"
},
Expand Down
24 changes: 22 additions & 2 deletions apps/docs/document-engine/sdks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p
| `doc.getMarkdown` | `get-markdown` | Extract the document content as a Markdown string. |
| `doc.getHtml` | `get-html` | Extract the document content as an HTML string. |
| `doc.markdownToFragment` | `markdown-to-fragment` | Convert a Markdown string into an SDM/1 structural fragment. |
| `doc.info` | `info` | Return document metadata including revision, node count, and capabilities. |
| `doc.info` | `info` | Return document summary info including word, character, paragraph, heading, table, image, comment, tracked-change, SDT-field, and list counts, plus outline and capabilities. |
| `doc.clearContent` | `clear-content` | Clear all document body content, leaving a single empty paragraph. |
| `doc.insert` | `insert` | Insert content into the document. Two input shapes: legacy string-based (value + type) inserts inline content at a text position within an existing block; structural SDFragment (content) inserts one or more blocks as siblings relative to a BlockNodeAddress target. When target is omitted, content appends at the end of the document. Legacy 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. |
| `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. |
Expand Down Expand Up @@ -818,7 +818,7 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p
| `doc.get_markdown` | `get-markdown` | Extract the document content as a Markdown string. |
| `doc.get_html` | `get-html` | Extract the document content as an HTML string. |
| `doc.markdown_to_fragment` | `markdown-to-fragment` | Convert a Markdown string into an SDM/1 structural fragment. |
| `doc.info` | `info` | Return document metadata including revision, node count, and capabilities. |
| `doc.info` | `info` | Return document summary info including word, character, paragraph, heading, table, image, comment, tracked-change, SDT-field, and list counts, plus outline and capabilities. |
| `doc.clear_content` | `clear-content` | Clear all document body content, leaving a single empty paragraph. |
| `doc.insert` | `insert` | Insert content into the document. Two input shapes: legacy string-based (value + type) inserts inline content at a text position within an existing block; structural SDFragment (content) inserts one or more blocks as siblings relative to a BlockNodeAddress target. When target is omitted, content appends at the end of the document. Legacy 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. |
| `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. |
Expand Down Expand Up @@ -1245,6 +1245,26 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p
</Tabs>
{/* SDK_OPERATIONS_END */}

## SDK vs browser integration model

The SDKs are request/response wrappers around the CLI. They do **not** expose browser event subscriptions like `editor.on('update', ...)` or `superdoc.on('editor-update', ...)`.

- Call `doc.info()` at workflow boundaries — after opening, after edits, or before saving — not in a polling loop.
- If you are building a browser live counter, use the [SuperEditor events](/core/supereditor/events) or [SuperDoc events](/core/superdoc/events) instead.

```ts
const doc = await client.open('./report.docx');
const info = doc.info();
console.log(
`${info.counts.words} words, ` +
`${info.counts.characters} characters, ` +
`${info.counts.trackedChanges} tracked changes, ` +
`${info.counts.sdtFields} SDT fields, ` +
`${info.counts.lists} lists`,
);
await doc.close();
```

## Related

- [Document API](/document-api/overview) — the in-browser API that defines the operation set
Expand Down
14 changes: 13 additions & 1 deletion packages/document-api/scripts/check-contract-parity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,21 @@ function createNoopAdapters(): DocumentApiAdapters {
},
info: {
info: () => ({
counts: { words: 0, paragraphs: 0, headings: 0, tables: 0, images: 0, comments: 0 },
counts: {
words: 0,
characters: 0,
paragraphs: 0,
headings: 0,
tables: 0,
images: 0,
comments: 0,
trackedChanges: 0,
sdtFields: 0,
lists: 0,
},
outline: [],
capabilities: { canFind: true, canGetNode: true, canComment: true, canReplace: true },
revision: '0',
}),
},
capabilities: {
Expand Down
6 changes: 4 additions & 2 deletions packages/document-api/src/contract/operation-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,8 +378,10 @@ export const OPERATION_DEFINITIONS = {
},
info: {
memberPath: 'info',
description: 'Return document metadata including revision, node count, and capabilities.',
expectedResult: 'Returns a DocumentInfo object with revision, word/paragraph/heading counts, and capability flags.',
description:
'Return document summary info including word, character, paragraph, heading, table, image, comment, tracked-change, SDT-field, and list counts, plus outline and capabilities.',
expectedResult:
'Returns a DocumentInfo object with counts (words, characters, paragraphs, headings, tables, images, comments, trackedChanges, sdtFields, lists), document outline, capability flags, and revision.',
requiresDocumentContext: true,
metadata: readOperation(),
referenceDocPath: 'info.mdx',
Expand Down
19 changes: 17 additions & 2 deletions packages/document-api/src/contract/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@
const trackedChangeAddressSchema = ref('TrackedChangeAddress');
const entityAddressSchema = ref('EntityAddress');
const selectionTargetSchema = ref('SelectionTarget');
const targetLocatorSchema = ref('TargetLocator');

Check warning on line 465 in packages/document-api/src/contract/schemas.ts

View workflow job for this annotation

GitHub Actions / validate

'targetLocatorSchema' is assigned a value but never used. Allowed unused vars must match /^_/u
const deleteBehaviorSchema = ref('DeleteBehavior');
const resolvedHandleSchema = ref('ResolvedHandle');
const pageInfoSchema = ref('PageInfo');
Expand Down Expand Up @@ -726,7 +726,7 @@
text: { type: 'string' },
});

const nodeInfoSchema: JsonSchema = {

Check warning on line 729 in packages/document-api/src/contract/schemas.ts

View workflow job for this annotation

GitHub Actions / validate

'nodeInfoSchema' is assigned a value but never used. Allowed unused vars must match /^_/u
type: 'object',
required: ['nodeType', 'kind'],
properties: {
Expand All @@ -742,7 +742,7 @@
additionalProperties: false,
};

const matchContextSchema = objectSchema(

Check warning on line 745 in packages/document-api/src/contract/schemas.ts

View workflow job for this annotation

GitHub Actions / validate

'matchContextSchema' is assigned a value but never used. Allowed unused vars must match /^_/u
{
address: nodeAddressSchema,
snippet: { type: 'string' },
Expand All @@ -753,7 +753,7 @@
['address', 'snippet', 'highlightRange'],
);

const unknownNodeDiagnosticSchema = objectSchema(

Check warning on line 756 in packages/document-api/src/contract/schemas.ts

View workflow job for this annotation

GitHub Actions / validate

'unknownNodeDiagnosticSchema' is assigned a value but never used. Allowed unused vars must match /^_/u
{
message: { type: 'string' },
address: nodeAddressSchema,
Expand Down Expand Up @@ -898,13 +898,28 @@
const documentInfoCountsSchema = objectSchema(
{
words: { type: 'integer' },
characters: { type: 'integer' },
paragraphs: { type: 'integer' },
headings: { type: 'integer' },
tables: { type: 'integer' },
images: { type: 'integer' },
comments: { type: 'integer' },
},
['words', 'paragraphs', 'headings', 'tables', 'images', 'comments'],
trackedChanges: { type: 'integer' },
sdtFields: { type: 'integer' },
lists: { type: 'integer' },
},
[
'words',
'characters',
'paragraphs',
'headings',
'tables',
'images',
'comments',
'trackedChanges',
'sdtFields',
'lists',
],
);

const documentInfoOutlineItemSchema = objectSchema(
Expand Down
Loading
Loading