@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.
| Entry | Contents |
|---|---|
@stesura/proofread | proofreadPlugin, plugin keys, global flags, commands, helpers, errors, types |
@stesura/proofread/language-tool | LanguageTool’s language list and the chunk size, for API routes |
@stesura/proofread/cache-idb | IndexedDB cache |
@stesura/proofread/cache-seed | Precomputed 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.
| Name | Type | Default |
|---|---|---|
options | ProofreadPluginFactoryOptions |
ProofreadPluginFactoryOptions
What proofreadPlugin takes.
ProofreadPluginOptions
Options of the root-editor proofread plugin; proofreadPlugin takes them plus parentEditorId.
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.
| Name | Type | Default |
|---|---|---|
context | ProofreadSkipContext |
ProofreadSkipContext
What ProofreadSkipPredicate gets: the doc.descendants arguments plus $pos.
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.
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.
getProofreadEnabled
Whether proofreading is on. A user preference shared by every editor.
setProofreadEnabled
Turns proofreading on or off, for hosts building their own toggle. Every mounted plugin listens and recomputes or wipes itself.
| Name | Type | Default |
|---|---|---|
value | boolean |
getProofreadLoading
True while at least one proofread api call is in flight, in any editor.
getProofreadLoadingFor
True while a proofread api call is in flight for editorId.
| Name | Type | Default |
|---|---|---|
editorId | string |
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.
getProofreadApiFailedFor
True while editorId’s api is failing.
| Name | Type | Default |
|---|---|---|
editorId | string |
getProofreadBacklogFor
How many blocks editorId’s queued and in-flight api calls carry; 0 when idle.
| Name | Type | Default |
|---|---|---|
editorId | string |
subscribeProofreadBacklog
Notifies on every backlog change, in any editor; returns the unsubscribe.
| Name | Type | Default |
|---|---|---|
cb | () => void |
getSubEditorProofreadDecorVersion
Monotonic counter of sub-editor decoration rebuilds.
subscribeSubEditorProofreadDecorVersion
Notifies after every sub-editor decoration rebuild; returns the unsubscribe.
| Name | Type | Default |
|---|---|---|
cb | () => 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.
| Name | Type | Default |
|---|---|---|
state | EditorState | |
dispatch | (tr: Transaction) => void | |
view | EditorView |
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.
| Name | Type | Default |
|---|---|---|
state | EditorState | |
dispatch | (tr: Transaction) => void | |
view | EditorView |
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).
| Name | Type | Default |
|---|---|---|
language | stringThe language code (e.g., “en-GB”, “en-US”, “fr-FR”), or
|
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.
| Name | Type | Default |
|---|---|---|
value | string | |
decor | ProofreadDecoration |
onProofreadIgnore
Marks the text under a decoration as ignored. A mark rather than a cache entry: see the plugin header.
| Name | Type | Default |
|---|---|---|
decor | Decoration |
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.
| Name | Type | Default |
|---|---|---|
decor | Decoration |
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.
| Name | Type | Default |
|---|---|---|
decor | Decoration |
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.
| Name | Type | Default |
|---|---|---|
state | EditorState |
getProofreadDecor
Every proofread decoration in the state. Works in sub-editors too.
| Name | Type | Default |
|---|---|---|
state | EditorState |
getProofreadDecoInSelection
The decoration covering the selection, edges included.
| Name | Type | Default |
|---|---|---|
state | EditorState |
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.
| Name | Type | Default |
|---|---|---|
state | EditorState | |
key | string |
getProofreadSuggestions
The decoration’s replacement suggestions; "" suggests deleting the text.
| Name | Type | Default |
|---|---|---|
deco | ProofreadDecoration |
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”.
| Name | Type | Default |
|---|---|---|
state | EditorState |
primaryLanguageSubtag
"en-US" → "en". Dictionary entries are keyed by primary subtag, so a word added
once covers every regional variant.
| Name | Type | Default |
|---|---|---|
language | string |
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.
| Name | Type | Default |
|---|---|---|
header | string |
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.
| Name | Type | Default |
|---|---|---|
err | unknown |
isProofreadTimeoutError
Whether err is a ProofreadApiError for a timeout (408, 504). The plugin’s own
apiTimeoutMS throws one with 408.
| Name | Type | Default |
|---|---|---|
err | unknown |
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.
| Error | Throw it for | Plugin 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 response | 408 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.
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.
| Name | Type | Default |
|---|---|---|
text | string | |
language | string |
GenerateProofreadErrorsResponse
What GenerateProofreadErrors resolves to: LanguageTool’s response shape.
| Name | Type | Default |
|---|---|---|
matches | ProofreadError[] |
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.
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.
CreateProofreadCache
Factory passed to proofreadPlugin; creates one cache adapter per editor field.
| Name | Type | Default |
|---|---|---|
field | string |
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.
CreateProofreadDictionary
Factory passed to proofreadPlugin to create a dictionary adapter.
Called once per plugin instance (sub-editors get their own).
ProofreadPluginState
State of both the proofread plugin and its sub-editor subscriber.
| Type | Description |
|---|---|
ProofreadCacheItem | One cache entry: the block’s hash, its language and its Problems. |
Problem | One stored error, with from/to relative to its block’s content. |
ProofreadDecoration | A 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.
| Export | Description |
|---|---|
LANGUAGE_TOOL_SUPPORTED_LANGUAGES | Every language code LanguageTool can check, per its /languages endpoint. SupportedLTLanguage is one of them. |
PROOFREAD_MAX_CHUNK_SIZE | 40000: 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.
| Name | Type | Default |
|---|---|---|
config | IdbProofreadCacheConfig |
IdbProofreadCacheConfig
Options for createIdbProofreadCacheFactory.
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.
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.
| Name | Type | Default |
|---|---|---|
__0 | SeededProofreadCacheConfig |
SeededProofreadCacheConfig
Options for createSeededProofreadCacheFactory.
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.
| Name | Type | Default |
|---|---|---|
doc | Node | |
seed | Readonly<Record<string, ProofreadCacheItem>> | |
skipNode | ProofreadSkipPredicate |
ProofreadSeedStatus
What getProofreadSeedStatus finds, as node ids.
| Name | Type | Default |
|---|---|---|
stale | string[]In the doc, but the seed entry no longer answers for it (content, language, format). | |
missing | string[]In the doc, not in the seed. | |
orphaned | string[]In the seed, not in the doc. |
ProofreadCacheSeed is the seed itself: cache entries by node id.