@stesura/core/plugins
ProseMirror plugins that make up the editor runtime, and the plugin keys for reading their state.
stesuraPlugins(schema, editorId, configuration?, options?) returns the full set: basePlugins (infrastructure) followed by customPlugins (features). Set an entry to false in options to leave that plugin out, e.g. { history: false } when a collab provider supplies its own undo/redo. configPlugin and viewModeInteractionsPlugin are always included; stesuraKeymapPlugin is not part of the set.
Functions & values
basePlugins
Infrastructure plugins: the config plugin (editor identity), history, gap/drop cursor, table editing, doc styling and math.
Each entry except the config plugin can be switched off by name, e.g.
{ history: false } when a collab provider supplies its own undo/redo.
| Name | Type | Default |
|---|---|---|
schema | Schema<any, any> | |
editorId | string | |
configuration | ConfigPluginProps | |
props | BasePluginsProps |
clipboardSerializerFor
The schema’s default serializer, with text boxes in their clipboard form.
| Name | Type | Default |
|---|---|---|
schema | Schema<any, any> |
closeMathEditor
Closes the source editor, leaving the equation selected.
| Name | Type | Default |
|---|---|---|
state | EditorState | |
dispatch | (tr: Transaction) => void | |
view | EditorView |
codeHighlightPluginKey
State: the code-block syntax highlighting decorations and their cache.
computedStylePastePlugin
Strips the source page’s computed style from a browser copy before the parse.
computedStylePastePluginKey
Identifies the web-copy base-style stripper.
configPlugin
Holds the editor id and config in plugin state, so anything with an
EditorState can resolve editor identity.
Root editors also create their stores and register them on mount, removing
them on unmount. Sub-editors (parentEditorId set) create nothing;
getEditorStores falls through to the parent’s stores.
| Name | Type | Default |
|---|---|---|
__0 | ConfigPluginProps & { editorId: string; } |
configPluginKey
State: the editor id and configuration — how any code holding an EditorState resolves editor identity.
customPlugins
Feature plugins: numbering, TOC, images, stylesheets, spacing, tables, search, paste handling and the rest of the editing behaviour.
Each entry can be switched off by name. Throws without field (the editor id).
| Name | Type | Default |
|---|---|---|
props | CustomPluginsProps |
docStylingPlugin
Mirrors the docBgColor doc attr (and its contrasting “auto” text color) as
CSS variables on .editor-container, since the doc node has no toDOM. Set
above .ProseMirror so footnote and header/footer sub-editors inherit them.
Main editor only.
findFloatingBox
The box with boxId in doc.
| Name | Type | Default |
|---|---|---|
doc | Node | |
boxId | string |
floatingBoxClipboardPlugin
Copies text boxes in a form the HTML parser keeps inside their paragraph, and line heights in a form other apps read.
floatingBoxClipboardPluginKey
floatingBoxEdgeKeymap
Keeps the caret inside the box: arrows stop at its edges, document jumps go to its ends.
floatingBoxEditorBindingPlugin
Pushes main-editor changes into the open text box editor. Mount on the main editor.
floatingBoxEditorBindingPluginKey
MUST keep its label: the sub-editor exclusion list matches on it.
floatingBoxKind
The text box sub-editor kind: the box’s blocks in an editor mounted inside the box’s node view (docs/executed/floating-objects-plan.md, section 3).
The exclusions are the notes’ plus the box itself (no box in a box, D5) and the nodes rendered through node views (D4): the static render could not match their live height. A box holding one of those does not open.
floatingBoxWrapperAttrs
The section attrs a box’s inner wrapper takes, for the sub-editor’s first state.
| Name | Type | Default |
|---|---|---|
state | EditorState | |
boxId | string |
formatPainterPlugin
Holds the formatting picked up by enableFormatPainter and applies it where
the user next clicks, or on Enter (A11Y-20).
A single click on the toolbar button arms it for one use (once), a double
click until dismissed. Escape (with focus anywhere), typing, or an unrelated
toolbar interaction dismisses it. Owns the marker cursor while armed.
formatPainterPluginKey
State: the captured formatting and whether the painter is armed.
gapCursorClassPlugin
Sets GAP_CURSOR_ACTIVE_CLASS on the editor root so CSS can hide the text
caret. Replaces .ProseMirror-focused:has(.ProseMirror-gapcursor) *, a
universal subject on :has() that was re-evaluated on every DOM insertion.
getAllowedImageTypes
The image types the policy lets users insert: what pickers offer.
| Name | Type | Default |
|---|---|---|
state | EditorState |
getImageUploadPolicy
The policy in force for a state; undefined means inline fallbacks are allowed.
| Name | Type | Default |
|---|---|---|
state | EditorState |
getMathEditorState
Whether the source editor is open, and for which equation.
| Name | Type | Default |
|---|---|---|
state | EditorState |
headerFooterGuardPlugin
Blocks edits to the headers/footers hidden in the main editor unless the
transaction carries the enableHeaderFooterEdits meta, and keeps the
selection out of them.
headerFooterRegion
Region key for one header/footer part of a section.
| Name | Type | Default |
|---|---|---|
sectionIndex | number | |
kind | "header" | "footer" | |
variant | string |
headerFooterSyncPluginKey
Key of @stesura/pagination’s header/footer sync plugin; its meta says which header/footer is open for editing.
headingInputRulesPlugin
Typing a space after a heading prefix (”#”..”########”, “h1”..“h8”, case-insensitive) that is a paragraph’s entire content turns it into that heading level and deletes the prefix.
An immediate backspace restores the prefix and a “Normal” paragraph in one transaction (one undo step).
highlightCounterNodesPlugin
Selecting a counter (a click, as in Word, or the highlightCounterNodes
meta) highlights every counter of the same list instance and level. While
active, mark steps on the selected counter are widened to those counters;
marks outside the allowlist are reverted.
highlightPlugin
Keeps the highlighter armed after a double click on the highlight button,
painting persistedColor on mouseup or Enter until dismissed.
highlightPluginKey
State: the armed highlighter color, if any.
imagePlugin
Upload placeholders plus the paste/drop handlers that start image uploads.
imagePluginKey
State: in-flight image upload placeholders.
imageUploadPolicy
Declares the editor’s image upload policy. Mounts whose documents travel
(collab, server persistence) pass allowInlineImages: false so that, with no
upload strategy installed, dropped/pasted images are refused rather than
embedded as base64, which bloats every commit and can exceed payload caps.
Without this plugin, inline base64 fallbacks are allowed.
| Name | Type | Default |
|---|---|---|
policy | ImageUploadPolicy |
imageUploadPolicyKey
State: the editor’s image upload policy (see imageUploadPolicy).
invisibleCharacters
Shows pilcrows, dots and arrows for paragraph marks, spaces and tabs by
toggling data-show-invisible on the editor element; the glyphs come from
CSS and the lazily loaded SpaceDot fonts.
Atom-final blocks get a real widget instead of the CSS ::after (see
pilcrow-decorations.ts). The widget set is kept regardless of show: it is
also the caret’s anchor after the atom, and only its glyph is gated in CSS.
invisibleCharactersPluginKey
State: whether the pilcrow / space / tab markers are shown, plus the pilcrow widgets.
languagePlugin
The language store’s plugin, which also puts the document’s language on the editor root, for the blocks without an entry.
languagePluginKey
State: the root lang tag plus decorations for blocks that deviate from it.
lowlightPlugin
Syntax highlighting for code blocks, backed by lowlight (highlight.js).
| Name | Type | Default |
|---|---|---|
spec | PluginSpec<CodeHighlightPluginState>The plugin’s spec object . | |
props | EditorProps<Plugin<CodeHighlightPluginState>>The props exported by this plugin. | |
getState | (state: EditorState) => CodeHighlightPluginState | undefinedExtract the plugin’s state field from an editor state. |
mathEditorEnterCmd
Enter / F2 on a selected equation opens its source editor.
Bound in the main keymap ahead of the generic Enter handling, which would otherwise insert a paragraph after the selected node.
| Name | Type | Default |
|---|---|---|
state | EditorState | |
dispatch | (tr: Transaction) => void | |
view | EditorView |
mathEditorPlugin
Tracks which equation’s source editor is open. The position is remapped per transaction and dropped once it no longer points at an equation.
mathEditorPluginKey
State: which equation, if any, has its floating LaTeX source editor open.
mathPlugins
Math editing: mathEditorPlugin plus input rules ($...$ → inline math,
$$ + space → display math). Returns [] for schemas without the math nodes.
Deliberately NOT upstream’s mathPlugin: its MathView nodeviews (nested
EditorView, destroy() removing its own DOM) crash under react-prosemirror,
which owns nodeview DOM. Our math nodes are atoms rendered by React nodeviews
(KaTeX only); the LaTeX source is edited in a floating editor
(editor-react/src/floating-menus/math-menu). mathSelectPlugin is skipped
too — it styles a selection inside the source, which is no longer in the doc.
| Name | Type | Default |
|---|---|---|
schema | Schema<any, any> |
numberingInputRulesPlugin
Typing a space after a known prefix at the very start of an unnumbered numbered-type block, with nothing after the caret, numbers it and deletes the prefix (“1. ” → numbered, ”* ” → bullet).
An immediate backspace (caret right after the counter) restores the typed text in one transaction (one undo step), and the rule stays off in that block until the caret leaves it.
numberingPlugin
Numbering plugin wired with track-changes hooks. Public surface for now; once the hook API is settled, this will be replaced with an export that lets callers pass their own hook factory (e.g. so users without the track-changes package can opt out by passing nothing).
numberingPluginKey
The counter-evaluating numbering plugin. Stateless (a view that publishes
the doc-global numbering context plus an appendTransaction that rewrites
counter nodes) — the key exists so transactions can be tagged and the plugin
excluded from sub-editors.
numberingPropsPlugin
Parameters:numberingPropsPluginKey
Props-only slice (caret teleport) — mounted in sub-editor views, which exclude the evaluating plugin.
openMathEditor
Opens the source editor for the selected equation.
Deliberately keeps the NodeSelection: the equation stays visibly selected while its source is being edited, and closing needs no selection restore.
| Name | Type | Default |
|---|---|---|
state | EditorState | |
dispatch | (tr: Transaction) => void | |
view | EditorView |
openMathEditorAt
Opens the source editor for the equation at pos (a click on the render).
| Name | Type | Default |
|---|---|---|
pos | number |
openMathEditorTarget
The equation the source editor is open for, with its position.
| Name | Type | Default |
|---|---|---|
state | EditorState |
paginationPluginKey
State: page breaks, measurements and dirty sections. Owned by @stesura/pagination.
pendingLinkPlugin
Tracks and decorates the range of a link being created, until the selection changes. Also opens links on Ctrl/Cmd+click.
pendingLinkPluginKey
State: the link being created or edited, before it is committed to the doc.
searchPlugin
Holds the active search query and range, and decorates its matches.
While a search is active, decorations are mapped through incoming steps rather than recomputed, so typing stays cheap; a debounced rescan then picks up matches the edit may have created.
| Name | Type | Default |
|---|---|---|
options | { initialQuery?: SearchQuery; initialRange?: { from: number; to: number; }; } |
searchPluginKey
State: the search query, range and match decorations.
SearchQuery
A search query and its options (case sensitivity, whole word, regex, literal escapes), with the match and replacement logic built on top of them.
sectionUnwrapPastePlugin
Flattens pasted section structure into its body content and drops headers/footers.
Copying across sections puts whole section nodes on the clipboard; left
alone, slice fitting splits the target section and duplicates headers/footers
mid-document. Like Google Docs we never paste section structure; sections
enter a doc via DOCX import only.
Only the slice’s top level is checked: sections can only be children of doc.
selectAfterMath
Closes the source editor and moves on: the caret lands right after an inline equation, or at the start of the block following a display one.
NOT Selection.near: forward from the end of section_content it walks
into the section FOOTER’s paragraph, and a raw TextSelection at the
position after a block node is not inside any textblock. When nothing
suitable follows a display equation it stays selected — the same result as
closing.
| Name | Type | Default |
|---|---|---|
pos | number |
selectionHighlightPlugin
Paints a non-empty text selection while the native highlight is gone: when the editor is
blurred (e.g. focus in a toolbar or dialog) and editable, and always in view mode, where PM’s
selectionToDOM writeback dies whenever chrome takes focus. Overlapping the native
::selection there doubles the alpha, deliberately. Node and cell selections are skipped: node
views and .selectedCell paint their own.
Inline atoms (pictures, placeholders, math, …) the selection covers get
.pm-in-selection at all times, which no ::selection reaches; recomputed
only when the doc or the selection’s range changes.
Painted with the CSS Custom Highlight API: an inline decoration wraps every selected text node
in a span, so after Ctrl+A the first transaction once focus left (a toolbar command) re-created
every text node of the document, seconds on a long one. Where the API is missing, the
.selection decoration, which lags a focus change by one transaction (focus dispatches nothing).
Inline atoms keep that decoration with the API too; a focus change dispatches for them, and only
when the selection holds one.
setMathSource
Replaces the LaTeX source of the equation at pos. False when unchanged.
Writes the MINIMAL step (common prefix/suffix untouched) so collab merges and track-changes attribution see fine-grained edits; replacing the whole formula per keystroke would make every edit conflict with every other.
| Name | Type | Default |
|---|---|---|
pos | number | |
source | string |
setPilcrowWidgetFactory
Installs the renderer-specific widget factory. @stesura/editor-react calls
it at module load, before any state exists: a state created earlier keeps an
empty set until its document changes. Without a factory no widgets are
produced (the CSS ::after still covers blocks that are not atom-final).
| Name | Type | Default |
|---|---|---|
factory | PilcrowWidgetFactory |
spacingPlugin
The spacing plugin: the spacingStore’s plugin.
stesuraPlugins
All Stesura plugins: basePlugins followed by customPlugins. A
layout: "flow" config adds flowLayoutGuardPlugin and drops doc styling.
| Name | Type | Default |
|---|---|---|
schema | Schema<any, any> | |
editorId | string | |
configuration | ConfigPluginProps | |
options | StesuraPluginsOptions |
stripTrackChangesMarksFromSlice
Sanitizes a pasted slice (see sanitizeFragment). Returns the ORIGINAL
slice instance when nothing changed. Open depths are clamped, since dropped
nodes can invalidate them.
| Name | Type | Default |
|---|---|---|
slice | Slice | |
deletion | MarkType | |
insertion | MarkType | |
modification | MarkType |
styleNumberingInstanceId
The instance a style-numbered paragraph belongs to. Derived, not minted, so a whole style family is one continuous list per story (Word semantics); a restart replaces it with an explicit per-paragraph instanceId.
| Name | Type | Default |
|---|---|---|
listRef | string | |
region | string |
styleSheetPlugin
Resolves the doc’s stylesheet and:
- publishes it (and the raw overrides) to the editor’s stores;
- writes its CSS into a
<style>element scoped by editor id; - heals blocks pointing at a style the sheet doesn’t have.
Throws on mount without an editor id, or one containing selector syntax.
styleSheetPluginKey
State: the resolved stylesheet and the doc’s raw overrides.
tableCellSelectionPreserver
Keeps a CellSelection alive when right-clicking inside it (for context menus).
tableDragHandlePlugin
Drag and drop for id-carrying nodes (tables). Works with any node view that
has a [data-drag-handle] element inside its [data-pm-node-view] root, and
either that root or a descendant carrying the node’s id. Ctrl/Cmd-drop copies.
tableDragHandlePluginKey
State: the in-flight row/column drag.
tableEmptyRowGuardPlugin
Removes empty rows from tables (left behind when all cells of two rows are merged vertically), shrinking the rowspans that crossed them.
tableHeaderCellsPlugin
Keeps each cell’s isHeader attr in step with its table’s headerRowCount
(the cell starts in a header row; a merge never promotes a cell). A cell’s toDOM cannot see its table, so the flag is
stamped onto the cells for the serializer, nodeview and exporters (A11Y-21).
Unlike the structural table guards it also runs on collab/history
transactions: a pure derivation converges, and skipping them would leave
peers rendering plain cells after a remote headerRowCount change.
tableResizingPlugin
Column and row resizing by dragging cell borders, zoom-corrected.
An inner border trades width between its two columns, keeping the table’s
total. An outer edge resizes its column alone and snaps to the section’s
content edges; which outer edges are draggable depends on table alignment.
A row dragged down to cellMinHeight or below resets to auto height.
| Name | Type | Default |
|---|---|---|
options | TableResizingOptions |
tableSizingPlugin
Keeps every table’s column grid (colwidth) consistent with its width
(docs/executed/table-sizing-plan.md §5). Doc-derived, idempotent, appended to the
triggering transaction (one undo event; collab peers re-running it append
nothing):
- T1, a table inserted (paste, DOCX import, AI, a move): a table with a
column of unknown width is fitted to the width it lays out in, an
inserted
fixed/pcttable is held to its width (I1), and a pasted autofit table is fitted to its block. Rows (or a cell paste) giving afixed/pcttable more columns widen it instead: its width follows the grid (Word). - T2, a column resized or a cell replaced: nested tables fit their host
(D3), a nested
pcttable follows it. - T3, a section’s attrs changed (page size, margins, columns) and T5, a
block’s
spanColumns: the section’spcttables follow their new width, header and footer ones included (they live in the main doc, D10). - T4, a table’s
widthset: that table is held to it.
The rescale skips track changes (D12): it records derived widths, not edits. Undo and redo restore exactly: their steps trigger nothing.
Ordering: must run AFTER prosemirror-tables’ tableEditing(), whose
fixTables repairs ragged Word tables. A still-invalid table is skipped and
re-fitted on the pass after the repair transaction; one the hold skipped is
held on a later pass of its dispatch that finds it repaired (TC-189).
tabStopsPlugin
Lays tab characters out against Word’s tab stops (DOCX-97): each \t is an
inline box sized so the text after it lands on the next stop — its own or
its style’s, then the default interval — by the stop’s alignment, with the
stop’s leader drawn in the gap.
Widths come from the rendered text, so they are measured after each render and applied as decorations; a tab not yet measured renders as the browser draws it. A render re-measures only the blocks that changed.
tabStopsPluginKey
themePastePlugin
Re-resolves pasted theme refs under this document’s theme, as Word does when keeping source formatting: themed text from a document with another theme takes this one’s colours. Unthemed colours paste as they are.
themePastePluginKey
Identifies the pasted theme-colour re-resolver.
TOCplugin
Rebuilds the anchor registry (TOC store) when a transaction touches an anchor, coalesced to a microtask rather than debounced, so the TOC DOM settles in the same frame as the doc and pagination measures it once.
tocPluginKey
Identifies the TOC plugin; the anchor registry itself lives in the editor’s stores.
trackChangesKey
State: whether suggestions are shown and being recorded. Lives here rather
than in @stesura/track-changes so core and comments can read it without
depending on that package.
trackChangesStripPastePlugin
Strips track-changes state from every pasted or dropped slice, and widens a copy’s cells as
accepting a pending column delete would (widenCopiedColumns).
The parse rules must accept <ins>/<del> and data-track-changes-* attrs
for clipboard round trips, which lets forged HTML author someone else’s
suggestion without the dispatch modifier. The gate is in core so it holds
without the track-changes plugin, and in transformPasted because
handlePaste never runs on drop (SEC-42). The JSON/collab path skips parsing
and is the server’s to enforce.
typographyPlugin
Replaces typed patterns with their typographic equivalents when followed by
a space (-- → —, ... → …, -> → →, (c) → ©, 1/2 → ½, smileys).
---- + space as a paragraph’s whole content becomes a horizontal rule.
An immediate backspace reverts a text replacement.
uniqueIdPlugin
Keeps node ids unique (one document-wide namespace) across edits. Load-time
healing is createNormalizeTransaction’s job, not this plugin’s.
Provenance rules:
- a node inserted by the transaction whose id existed in the old doc and still exists elsewhere is a copy → it gets a fresh id; if the id is now unique it was a move and keeps its identity. Sections too: the earlier section keeps the id its header/footer bindings and pagination caches key off;
- brand-new ids inserted more than once go through the ref-aware sweep
(
applyIdCorrections) shared with load-time healing.
Participation is schema-driven: any node whose spec declares an id attr
(all blocks, via the uniqueId global attribute, plus inline nodes declaring
id in their own spec, e.g. image_anchored).
viewModeInteractionsPlugin
Interaction fixes that only apply in view mode. The hooks run regardless of
view.editable (plugin props bypass ProseMirror’s editable gate), which is
exactly why they’re needed:
dragstart: dragging an existing text selection fires a real HTML5 drag even on a non-editable view — dropping back in is blocked by the mode gate, but dragging content OUT of a read-only document is not. Block it.keydown: ProseMirror runs no keymap on a non-editable view, so the positioned object cycle (Tab, Shift-Tab, Alt-F10, Escape) is bound here. Tab with no object selected still leaves the document.handleClick: on a non-editable view Chromium creates no caret on click, so ProseMirror keeps the previous selection — the fallback highlight would show a stale range. Collapse to the clicked position (matches what Firefox does natively).
widenCopiedColumns
slice, copied from selection, with each cell of a table it cuts at the width accepting
that table’s pending column deletes gives its column in the source (TC-198). A copy inside a
table holds rows or cells, no table, so the paste, which drops the struck cells, can’t widen
what’s left; a table the slice holds whole is the paste’s to widen (TC-190). The original
instance when nothing changed.
| Name | Type | Default |
|---|---|---|
slice | Slice | |
selection | Selection |
wordPasteHandlerPlugin
Normalizes HTML pasted from desktop Microsoft Word before ProseMirror parses it: converts equations to math nodes, rebuilds lists (Word pastes numbers as literal text), maps indentation/spacing onto the formatting global attributes, and strips Office junk. Non-Word HTML passes through.
A pasted list matching no existing config gets a new one, registered in
doc.attrs.listConfigs by appendTransaction once the paste lands (the
transform cannot dispatch). Currently the only transformPastedHTML plugin.
wordPasteHandlerPluginKey
Identifies the Word paste handler.
wrapSelectionWithPlugin
Typing a wrapping symbol over a selection wraps it instead of replacing it
(selecting “Hello” and typing [ yields [Hello]); the selection is kept.
Types
CodeHighlightPluginState
| Name | Type | Default |
|---|---|---|
cache | DecorationCache | |
decorations | DecorationSet | undefined | |
promises | Promise<void>[] |
ConfigPluginProps
PilcrowWidgetFactory
Builds the marker decoration at a block’s content end.
Injected because only a React widget places it correctly. A plain
Decoration.widget(pos, domFactory) is rendered by react-prosemirror’s
NativeWidgetView inside a bare editable <span> wrapper, which is what the
WidgetViewDesc binds to; contentEditable="false" lands on the inner
element only. react-prosemirror’s scanFor consults ignoreForSelection
only at contentEditable="false" elements, so it descends into the wrapper:
clicking past the atom parks an oversized caret there and the next keystroke
resolves to the start of the section.
widget(pos, Component) from @stesura/react-prosemirror renders
with no wrapper, so the factory lives in @stesura/editor-react.
| Name | Type | Default |
|---|---|---|
pos | number |
SearchState
StesuraPluginsOptions
Other exports
| Export | Type | Description |
|---|---|---|
GAP_CURSOR_ACTIVE_CLASS | "pm-gapcursor-active" | Root class set while the selection is a gap cursor. |
NUMBERING_REGION_BODY | "body" | Counter-stream regions — Word’s “stories”. Numbering DEFINITIONS are document-global; the counters that run off them are per story: the body, all footnotes together, all endnotes together, and each header/footer part. |
NUMBERING_REGION_ENDNOTES | "endnotes" | |
NUMBERING_REGION_FOOTNOTES | "footnotes" | |
OVERSIZED_CLAMPS_POKE | "oversizedClampsPoke" | Payload-less meta key meant to signal that the oversized-node clamp set changed. Currently neither dispatched nor read anywhere. |
PAGINATION_CLEAR_ALL_META | "clearAllPaginationCaches" | Meta key (true) that invalidates every section’s pagination cache, e.g. after a DOCX import. Lives in core so callers need not import pagination. |
PAGINATION_DIRTY_NODES_META | "paginationDirtyNodes" | Meta key carrying node ids (string[]) whose rendered size changed without a doc change, e.g. a nodeview whose async data grew its DOM. Pagination re-measures only those nodes, never wiping the cache. |
PILCROW_CLASS | "pm-pilcrow" | Class the CSS hangs the marker’s styling off. |