Skip to Content
API Referenceproofread

@stesura/proofread

Spell and grammar checking as a ProseMirror plugin. You supply the checker and the cache; setup, the checker contract and the cache rules are in the Proofread guide. The toolbar, panel and menus are in @stesura/proofread-react.

EntryContents
@stesura/proofreadproofreadPlugin, plugin keys, global flags, commands, helpers, errors, types
@stesura/proofread/language-toolLanguageTool’s language list and the chunk size, for API routes
@stesura/proofread/cache-idbIndexedDB cache
@stesura/proofread/cache-seedPrecomputed cache seeds

Plugin

proofreadPlugin

The proofread plugin; install one per editor. A root editor gets the full plugin (queues, calls the api, writes the cache); a sub-editor gets the read-only subscriber, which paints from the root’s cache.

Parameters:
NameTypeDefault
optionsProofreadPluginFactoryOptions
Returns:
Plugin<ProofreadPluginState>

ProofreadPluginFactoryOptions

What proofreadPlugin takes.

NameTypeDefault
debounceTimeMSnumber

Debounce time in milliseconds for incremental proofread runs.

generateProofreadErrorsGenerateProofreadErrors

Checks a chunk of text — typically a call to a LanguageTool-compatible api.

createCacheCreateProofreadCache

Returns the cache adapter for an editor field. Called on the first view update.

createDictionaryCreateProofreadDictionary

Returns the user’s dictionary adapter. Without it, “add to dictionary” is unavailable.

Applied at render time: cached results stay raw, so dictionary edits repaint without api calls, and a shared cache stays valid for users with different dictionaries.

skipNodeProofreadSkipPredicate

Excludes nodes from proofreading; return true to skip. Omit to proofread everything. See ProofreadSkipPredicate for the rules.

apiTimeoutMSnumber

How long one api call may take before it counts as failed (flagged, backed off, retried). Default 30 s: LanguageTool can take seconds on a full chunk. Without a bound, a hung request keeps the loading state on and Retry disabled.

maxConcurrentRequestsnumber

How many api calls the editor keeps in flight; the rest wait in a queue, in document order, with what was just edited first. Default 2: a first open of a long document is dozens of calls, and over HTTP/1.1 the browser allows six connections per host, so an unbounded burst stalls every other request to the same origin. Raise it for a dedicated checker with spare cores. 1 checks one chunk at a time; Infinity sends everything at once.

chunkSizenumber

Characters per api call. Default 40 000 (PROOFREAD_MAX_CHUNK_SIZE). Blocks longer than this are not checked. A checker’s time is linear in the text, so this sets the latency of one call and how much one failure leaves unchecked, not the total work.

minChunkSizenumber

Floor for the chunk size after timeouts. Default 10 000. A timed-out call (the apiTimeoutMS abort, or a ProofreadApiError with 408/504) halves the chunk size for the rest of the session, down to this, and is re-sent in halves at once; it never grows back. A call that cannot shrink fails and is retried on the backoff. Other failures don’t halve.

maxRetriesnumber

Consecutive failed runs retried on the backoff ladder (5 s up to 60 s) before giving up. Default Infinity. Once exhausted the api-failed flag stays raised and nothing is sent until the next local edit or retryProofread; the unchecked blocks stay queued.

parentEditorIdstring | undefined

The parent editor’s id for a sub-editor (header, footer, note), undefined for a root editor. Required so the choice is explicit: a sub-editor without it would run a second full plugin on the same cache.

ProofreadPluginOptions

Options of the root-editor proofread plugin; proofreadPlugin takes them plus parentEditorId.

NameTypeDefault
debounceTimeMSnumber

Debounce time in milliseconds for incremental proofread runs.

generateProofreadErrorsGenerateProofreadErrors

Checks a chunk of text — typically a call to a LanguageTool-compatible api.

createCacheCreateProofreadCache

Returns the cache adapter for an editor field. Called on the first view update.

createDictionaryCreateProofreadDictionary

Returns the user’s dictionary adapter. Without it, “add to dictionary” is unavailable.

Applied at render time: cached results stay raw, so dictionary edits repaint without api calls, and a shared cache stays valid for users with different dictionaries.

skipNodeProofreadSkipPredicate

Excludes nodes from proofreading; return true to skip. Omit to proofread everything. See ProofreadSkipPredicate for the rules.

apiTimeoutMSnumber

How long one api call may take before it counts as failed (flagged, backed off, retried). Default 30 s: LanguageTool can take seconds on a full chunk. Without a bound, a hung request keeps the loading state on and Retry disabled.

maxConcurrentRequestsnumber

How many api calls the editor keeps in flight; the rest wait in a queue, in document order, with what was just edited first. Default 2: a first open of a long document is dozens of calls, and over HTTP/1.1 the browser allows six connections per host, so an unbounded burst stalls every other request to the same origin. Raise it for a dedicated checker with spare cores. 1 checks one chunk at a time; Infinity sends everything at once.

chunkSizenumber

Characters per api call. Default 40 000 (PROOFREAD_MAX_CHUNK_SIZE). Blocks longer than this are not checked. A checker’s time is linear in the text, so this sets the latency of one call and how much one failure leaves unchecked, not the total work.

minChunkSizenumber

Floor for the chunk size after timeouts. Default 10 000. A timed-out call (the apiTimeoutMS abort, or a ProofreadApiError with 408/504) halves the chunk size for the rest of the session, down to this, and is re-sent in halves at once; it never grows back. A call that cannot shrink fails and is retried on the backoff. Other failures don’t halve.

maxRetriesnumber

Consecutive failed runs retried on the backoff ladder (5 s up to 60 s) before giving up. Default Infinity. Once exhausted the api-failed flag stays raised and nothing is sent until the next local edit or retryProofread; the unchecked blocks stay queued.

ProofreadSkipPredicate

Excludes nodes from proofreading; return true to skip. Called on:

  • blocks: the block and its subtree are never checked or decorated;
  • text and inline nodes inside a proofread block: their text is left out of the api text, the surrounding words are still checked.

Must be pure for a given document. Its result affects the cache hash, so changing the predicate re-checks the affected blocks.

Parameters:
NameTypeDefault
contextProofreadSkipContext
Returns:
boolean

ProofreadSkipContext

What ProofreadSkipPredicate gets: the doc.descendants arguments plus $pos.

NameTypeDefault
nodeNode

A block, inline node or text node.

posnumber

Absolute document position.

parentNode | null

null for a direct child of the doc.

indexnumber
$posResolvedPos

doc.resolve(pos), resolved on first access — for ancestry checks.

State and global flags

Proofreading on/off is one preference shared by every editor on the page; loading, failure and backlog are tracked per root editor. Prefer getProofreadPluginKey(state) over the keys: it picks the root or sub-editor key.

proofreadPluginKey

Key for the full plugin, installed on root editors.

NameTypeDefault
get(state: EditorState) => Plugin<ProofreadPluginState> | undefined

Get the active plugin with this key, if any, from an editor state.

getState(state: EditorState) => ProofreadPluginState | undefined

Get the plugin’s state from an editor state.

proofreadSubscriberPluginKey

Key for the read-only sub-editor plugin. Separate from proofreadPluginKey so core/sub-editor/exclusions can refuse the full plugin inside a sub-editor while the subscriber still installs. Use getProofreadPluginKey(state) to pick the right one.

NameTypeDefault
get(state: EditorState) => Plugin<ProofreadPluginState> | undefined

Get the active plugin with this key, if any, from an editor state.

getState(state: EditorState) => ProofreadPluginState | undefined

Get the plugin’s state from an editor state.

getProofreadEnabled

Whether proofreading is on. A user preference shared by every editor.

Parameters:
This function does not accept any parameters.
Returns:
boolean

setProofreadEnabled

Turns proofreading on or off, for hosts building their own toggle. Every mounted plugin listens and recomputes or wipes itself.

Parameters:
NameTypeDefault
valueboolean
Returns:
void

getProofreadLoading

True while at least one proofread api call is in flight, in any editor.

Parameters:
This function does not accept any parameters.
Returns:
boolean

getProofreadLoadingFor

True while a proofread api call is in flight for editorId.

Parameters:
NameTypeDefault
editorIdstring
Returns:
boolean

getProofreadApiFailed

True while any editor’s api is failing. Cleared by the next run that reaches the api; retries are automatic, retryProofread only skips the wait.

Parameters:
This function does not accept any parameters.
Returns:
boolean

getProofreadApiFailedFor

True while editorId’s api is failing.

Parameters:
NameTypeDefault
editorIdstring
Returns:
boolean

getProofreadBacklogFor

How many blocks editorId’s queued and in-flight api calls carry; 0 when idle.

Parameters:
NameTypeDefault
editorIdstring
Returns:
number

subscribeProofreadBacklog

Notifies on every backlog change, in any editor; returns the unsubscribe.

Parameters:
NameTypeDefault
cb() => void
Returns:
() => void

getSubEditorProofreadDecorVersion

Monotonic counter of sub-editor decoration rebuilds.

Parameters:
This function does not accept any parameters.
Returns:
number

subscribeSubEditorProofreadDecorVersion

Notifies after every sub-editor decoration rebuild; returns the unsubscribe.

Parameters:
NameTypeDefault
cb() => void
Returns:
() => void

Commands

The commands that act on one error take its decoration: use getProofreadDecoInSelection(state), or findProofreadDecorationByKey(state, key) for one captured earlier.

toggleProofread

Toggles the global proofread flag; every plugin on the page listens to it. Dispatches nothing — dispatch only distinguishes a real run from a dry-run probe.

Parameters:
NameTypeDefault
stateEditorState
dispatch(tr: Transaction) => void
viewEditorView
Returns:
boolean

retryProofread

Re-checks the whole document immediately, ignoring the outage backoff. Current cache entries are hits; only what the outage left unchecked reaches the api.

Parameters:
NameTypeDefault
stateEditorState
dispatch(tr: Transaction) => void
viewEditorView
Returns:
boolean

setLanguage

Sets the proofing language attribute for all proofread nodes in the current selection. Triggers the updateCounters meta when a modified node has numbering, or when the document holds a cross-reference.

The cross-reference half is not incidental: an aboveBelow reference renders its “above”/“below” in the TARGET’s language, and an AttrStep carries an empty step map, so the cross-reference plugin’s change detection never sees one. Without the meta an Italian “sopra” stayed “sopra” after the target moved to French (REF-46).

Parameters:
NameTypeDefault
languagestring

The language code (e.g., “en-GB”, “en-US”, “fr-FR”), or null to drop the explicit stamp and follow the style/document language.

Returns:
Command

onProofreadReplace

Replaces the text under a decoration with value, keeping its marks except track-changes ones, and selects the replacement. An empty value deletes the text (the api’s suggestion for a doubled comma, a stray space).

The block’s hash changes, so the plugin re-checks it on its own.

Parameters:
NameTypeDefault
valuestring
decorProofreadDecoration
Returns:
Command

onProofreadIgnore

Marks the text under a decoration as ignored. A mark rather than a cache entry: see the plugin header.

Parameters:
NameTypeDefault
decorDecoration
Returns:
Command

onProofreadIgnoreAll

Marks every whole-word, case-sensitive occurrence of the flagged word as ignored.

Searches each block’s api text, so a word split by a mark boundary (“te” + h) is found as one word, and matches spanning deletions or atoms are dropped, as api matches would be.

Parameters:
NameTypeDefault
decorDecoration
Returns:
Command

onProofreadAddToDictionary

Adds the flagged word to the user’s dictionary, under the primary subtag of the block’s effective language. Unlike “ignore all” it writes nothing to the document: the dictionary follows the user across documents and filters at render time.

Fails when the host configured no createDictionary.

Parameters:
NameTypeDefault
decorDecoration
Returns:
Command

Helpers

getProofreadPluginKey

The subscriber key in a sub-editor, the main key otherwise. Prefer this over importing a key: the wrong one silently reads nothing.

Parameters:
NameTypeDefault
stateEditorState
Returns:
PluginKey<ProofreadPluginState>

getProofreadDecor

Every proofread decoration in the state. Works in sub-editors too.

Parameters:
NameTypeDefault
stateEditorState
Returns:
ProofreadDecoration[]

getProofreadDecoInSelection

The decoration covering the selection, edges included.

Parameters:
NameTypeDefault
stateEditorState
Returns:
ProofreadDecoration | undefined

findProofreadDecorationByKey

Re-finds a decoration in the live state by its key. UI holds decorations captured at render time, and the doc may have moved by the time the user clicks, so commands must act on the live position. Undefined once the error is gone.

Parameters:
NameTypeDefault
stateEditorState
keystring
Returns:
ProofreadDecoration | undefined

getProofreadSuggestions

The decoration’s replacement suggestions; "" suggests deleting the text.

Parameters:
NameTypeDefault
decoProofreadDecoration
Returns:
string[]

getProofreadDictionary

The editor’s dictionary adapter, if the host configured createDictionary. Works in sub-editors too. UI uses it to decide whether to offer “add to dictionary”.

Parameters:
NameTypeDefault
stateEditorState
Returns:
ProofreadDictionaryAdapter | undefined

primaryLanguageSubtag

"en-US" → "en". Dictionary entries are keyed by primary subtag, so a word added once covers every regional variant.

Parameters:
NameTypeDefault
languagestring
Returns:
string | null

parseRetryAfterMS

Parses an HTTP Retry-After header (delta-seconds or HTTP date, RFC 9110) into milliseconds, for building a ProofreadRetryAfterError. Undefined when absent, unparseable or not in the future.

Parameters:
NameTypeDefault
Returns:
number | undefined

retryAfterMSOf

The cooldown carried by a ProofreadRetryAfterError, capped at PROOFREAD_MAX_RETRY_AFTER_MS. Undefined for any other error or a non-finite, non-positive value.

Parameters:
NameTypeDefault
errunknown
Returns:
number | undefined

isProofreadTimeoutError

Whether err is a ProofreadApiError for a timeout (408, 504). The plugin’s own apiTimeoutMS throws one with 408.

Parameters:
NameTypeDefault
errunknown
Returns:
boolean

Errors

Thrown by your generateProofreadErrors so the plugin can tell a rate limit, a timeout and an outage apart. Both set name, which the plugin matches on, so they work across bundles. See Implementing generateProofreadErrors.

ErrorThrow it forPlugin behaviour
ProofreadRetryAfterError(retryAfterMS)A 429 with Retry-After (read it with parseRetryAfterMS)Waits the longer of its own backoff and retryAfterMS, capped at 5 minutes
ProofreadApiError(status)Any other failed response408 and 504 halve the chunk size; any other status stops the language group until the backoff retry

Agent issues

The IssueProvider that hands proofread’s spelling and grammar errors to the agent’s issue tools; pass it to createIssueTools in @stesura/ai.

proofreadIssues

Spelling and grammar errors the proofread plugin has painted, for createIssueTools. Fixes land as tracked suggestions; choice picks among the replacements, the first by default.

NameTypeDefault
sourcestring
collect(state: EditorState) => AiIssue[]

The current findings, in document order.

apply(state: EditorState, fixes: readonly AiIssueFix[]) => AiIssueApplyResult

Applies fixes to issues collect returns. Tracked fixes come back as a plain edit the caller turns into suggestions; direct ones as the change itself. Omitted: nothing is fixable.

Types

GenerateProofreadErrors

Host-supplied checker for a chunk of text, typically a LanguageTool-compatible call. language is the blocks’ effective language (proofingLanguage, else the style’s, else the document’s). Throw on failure: the blocks are retried.

Parameters:
NameTypeDefault
textstring
languagestring
Returns:
GenerateProofreadErrorsResponse | Promise<GenerateProofreadErrorsResponse>

GenerateProofreadErrorsResponse

What GenerateProofreadErrors resolves to: LanguageTool’s response shape.

NameTypeDefault
matchesProofreadError[]

ProofreadError

One match from the checker, in LanguageTool’s shape. Validated, not trusted: matches without a usable offset/length are dropped, other fields fall back.

NameTypeDefault
offsetnumber

Offset into the text that was sent.

lengthnumber
messagestring
shortMessagestring
type{ typeName: string; }
replacements{ value: string; }[]

Capped at five; "" suggests deleting the flagged text.

ProofreadCacheAdapter

Storage abstraction for the proofread cache (memory, redis, localforage…).

Sync contract

get / entries / set / delete are synchronous. Decoration rendering runs inside doc.descendants on every transaction and cannot block on IO. Async-backed adapters (redis, idb) are expected to maintain an in-memory mirror and schedule persistence in the background.

External changes

Adapters whose data can change from outside the plugin (collaborator edits on a y.Map, redis pubsub, another tab via BroadcastChannel) should implement subscribe. The plugin calls it on mount and triggers a decoration refresh when notified.

Pass the changed node ids to the callback when the adapter knows them. The plugin then re-queues only those nodes, and a sub-editor skips the rebuild entirely when none of them is in its document — without the ids, every remote write walks every mounted editor. Omitting them stays correct, just broader.

Cross-instance fan-out

One page creates several instances for the same field: the main editor’s plugin plus one subscriber plugin per sub-editor (headers, footers, notes), all through the same factory. Sub-editors only ever repaint from these notifications, so set and delete MUST notify subscribers registered through every instance backed by the same store, with the changed ids — not just the instance that wrote. Adapters don’t need to filter out the writer’s own notifications, but the notification MUST be delivered synchronously from inside set/delete: the main plugin suppresses its own writes with a flag held only for the duration of the call, so a deferred notification (microtask, network echo) makes it treat every one of its own writes as external — a wasted re-check pass per write.

Readiness

An adapter that fills its mirror from an async source (a hydration GET, an idb read) should expose that as ready. The plugin’s FIRST pass waits for it (bounded — a dead endpoint must not stall proofreading), so a document whose results are already in the shared store is not re-sent to the api on every load (PR-47). Resolve it on failure too; reject never. Adapters with no async source omit it.

NameTypeDefault
get(id: string) => ProofreadCacheItem | undefined
set(id: string, value: ProofreadCacheItem) => void
delete(id: string) => void
entries() => Iterable<[string, ProofreadCacheItem]>
subscribe(cb: (changedIds?: readonly string[] | undefined) => void) => () => void
destroy() => void
readyPromise<void>

CreateProofreadCache

Factory passed to proofreadPlugin; creates one cache adapter per editor field.

Parameters:
NameTypeDefault
fieldstring
Returns:
ProofreadCacheAdapter

ProofreadDictionaryAdapter

Storage abstraction for the user’s proofread dictionary (“add to dictionary”).

The dictionary is identity-scoped (a user and the groups they belong to), never document-scoped: it is applied as a render-time filter over cached problems, so cached results stay dictionary-blind and shareable across collaborators.

Sync contract

Same as ProofreadCacheAdapter: has runs inside decoration builds and cannot block on IO. Async-backed adapters keep an in-memory mirror and persist in the background.

Language

language is always a primary subtag ("en", never "en-US") or null. An entry stored with language: null applies to every language (names, brand terms); has must honour that.

External changes

subscribe should fire on any change to the effective dictionary — including writes from this same page through another adapter instance (sub-editors create their own), other tabs, or the server. The plugin only rebuilds decorations from cache on notification, so over-notifying is cheap and safe.

NameTypeDefault
has(word: string, language?: string | null | undefined) => boolean

Whether word is in the dictionary for language (primary subtag) or globally.

add(word: string, language?: string | null | undefined) => void
remove(word: string, language?: string | null | undefined) => void
subscribe(cb: () => void) => () => void
destroy() => void

CreateProofreadDictionary

Factory passed to proofreadPlugin to create a dictionary adapter. Called once per plugin instance (sub-editors get their own).

Parameters:
This function does not accept any parameters.
Returns:
ProofreadDictionaryAdapter

ProofreadPluginState

State of both the proofread plugin and its sub-editor subscriber.

NameTypeDefault
decorDecorationSet
versionnumber

Bumped on doc changes; a run that finishes on another version drops its repaint. Undo/redo/collab-sync don’t bump it: their repaint comes from cache, and dropping it would leave the doc unsquiggled until the next edit.

ignoreRanges{ from: number; to: number; }[]

Ranges whose decorations are dropped on apply. See setClearDecorationsMeta.

TypeDescription
ProofreadCacheItemOne cache entry: the block’s hash, its language and its Problems.
ProblemOne stored error, with from/to relative to its block’s content.
ProofreadDecorationA proofread decoration, typed so the stored Problem and the decoration’s key can be read off its spec. Use the decoration’s own from/to for document positions.

LanguageTool (/language-tool)

Imports nothing from the plugin, so API routes can use it.

ExportDescription
LANGUAGE_TOOL_SUPPORTED_LANGUAGESEvery language code LanguageTool can check, per its /languages endpoint. SupportedLTLanguage is one of them.
PROOFREAD_MAX_CHUNK_SIZE40000: the characters per call the plugin chunks by, for routes that reject oversized text.

IndexedDB cache (/cache-idb)

createIdbProofreadCacheFactory

createCache factory for an IndexedDB-backed cache scoped to one document, for hosts without a shared cache. Results survive reloads.

The adapter contract is synchronous, so reads and writes hit an in-memory mirror; writes are flushed in debounced batches (and on pagehide / last destroy()), and the mirror is hydrated once per field. No cross-tab sync: a stale mirror only costs a re-check.

Parameters:
NameTypeDefault
configIdbProofreadCacheConfig
Returns:
CreateProofreadCache

IdbProofreadCacheConfig

Options for createIdbProofreadCacheFactory.

NameTypeDefault
docIdstring

Scopes entries to a document: node ids can repeat across documents.

dbNamestring

Override to isolate stores (tests, multi-tenant hosts).

maxDocumentsnumber

Least-recently-opened documents beyond this are dropped.

flushDelayMsnumber

Debounce before writes reach IndexedDB. Test seam.

onError(error: unknown, context: { scope: string; }) => void

Receives hydration and flush failures; defaults to console.warn.

Cache seeds (/cache-seed)

For documents whose results can be computed ahead of time, such as templates: warm a cache offline with warmProofreadCache, ship it as a seed, and serve it with createSeededProofreadCacheFactory, so opening the document sends nothing to your checker. getProofreadSeedStatus checks that a seed still answers for its document.

warmProofreadCache

Proofreads every block of doc into cache through the plugin’s own pipeline, so the hashes and offsets are those a live pass would write. For cache seeds and warmers. Throws when any api call failed; blocks already current in cache are not re-sent.

Parameters:
NameTypeDefault
__0{ doc: Node; cache: ProofreadCacheAdapter; generateProofreadErrors: GenerateProofreadErrors; skipNode?: ProofreadSkipPredicate; apiTimeoutMS?: number; maxConcurrentRequests?: number; chunkSize?: number; minChunkSize?: number; }
Returns:
Promise<void>

createSeededProofreadCacheFactory

createCache factory serving a static seed under a writable overlay, so a document whose results were precomputed isn’t sent to the api on load. Seed entries are verified by hash like any other, so a stale seed only costs api calls.

Parameters:
NameTypeDefault
__0SeededProofreadCacheConfig
Returns:
CreateProofreadCache

SeededProofreadCacheConfig

Options for createSeededProofreadCacheFactory.

NameTypeDefault
seedReadonly<Record<string, ProofreadCacheItem>>
overlayCreateProofreadCache

Receives every write; the seed itself is read-only. Defaults to an in-memory store.

A persisted overlay shadows the seed: when the document reloads to its seeded content but the overlay still holds an entry from an edit, that block is re-checked. For documents that reset on reload, keep the default.

getProofreadSeedStatus

Compares a seed with the document it was generated from, offline, using the plugin’s own hit test. All three lists empty means loading doc sends nothing to the api. skipNode must be the plugin’s.

Parameters:
NameTypeDefault
docNode
seedReadonly<Record<string, ProofreadCacheItem>>
skipNodeProofreadSkipPredicate
Returns:
Promise<ProofreadSeedStatus>

ProofreadSeedStatus

What getProofreadSeedStatus finds, as node ids.

NameTypeDefault
stalestring[]

In the doc, but the seed entry no longer answers for it (content, language, format).

missingstring[]

In the doc, not in the seed.

orphanedstring[]

In the seed, not in the doc.

ProofreadCacheSeed is the seed itself: cache entries by node id.

Last updated on