Skip to Content
ProseMirror API

ProseMirror API

Stesura is built on ProseMirror, and its state, transactions, commands and plugins are plain ProseMirror. This page covers the concepts you need to extend the editor and the Stesura helpers around them.

Core Concepts

Document Model

A document is a tree of nodes. Each node has a type (from the schema), attributes, and either child content or, for text, marks.

doc ├── section │ ├── section_header │ ├── section_content │ │ ├── paragraph attrs: { styleId: "Normal", textAlign: "center", … } │ │ │ ├── text "Hello " marks: [strong] │ │ │ └── text "world" │ │ └── paragraph │ │ └── text "Second paragraph" │ └── section_footer └── footnotes

Text never starts at position 0: section scaffolding comes first. Work from the selection, or find nodes by walking the doc.

The document is immutable. To change it, build a transaction and dispatch it.

Transactions

A transaction (tr) describes changes to the editor state. Its methods chain:

const { from } = view.state.selection; const tr = view.state.tr .insertText("Hello", from) .addMark(from, from + 5, view.state.schema.marks.strong.create()) .setMeta("source", "my-plugin"); view.dispatch(tr);

Commands

A command has the signature:

type Command = ( state: EditorState, dispatch?: (tr: Transaction) => void, view?: EditorView ) => boolean;

It returns true if it can run (and ran, if dispatch was provided). Calling it without dispatch is a dry run.

import { toggleBold } from "@stesura/core/commands"; const canToggle = toggleBold(view.state); toggleBold(view.state, view.dispatch, view);

Composing commands

To run several commands as ONE transaction (one undo step, each command seeing what the previous ones wrote), compose them:

import { composeCommands, setSpacing, setTextAlign } from "@stesura/core/commands"; const formatParagraph = composeCommands( setTextAlign("center"), setSpacing({ before: 12, after: 6 }) ); formatParagraph(view.state, view.dispatch);

composeCommands succeeds if any command did something; a command that returns false keeps whatever it already wrote. sequenceCommands stops at the first false and dispatches nothing, so the group is all-or-nothing. Neither is ProseMirror’s chainCommands, which stops at the first command that succeeds. See Composing commands.

Accessing the Editor

useEditorEventCallback

The way to act on the editor from React components rendered inside the ProseMirror tree (node views, floating menus):

import { useEditorEventCallback } from "@handlewithcare/react-prosemirror"; import { toggleBold } from "@stesura/core/commands"; const BoldButton = () => { const handleClick = useEditorEventCallback((view) => { toggleBold(view.state, view.dispatch, view); }); return <button onClick={handleClick}>Bold</button>; };

The callback is stable and always sees the latest EditorView, without causing re-renders.

The toolbar and other chrome render outside that tree, so there use useRunTbCommand or useBoundEditorViewCallback from @stesura/editor-react. They target the editor bound to the toolbar, which can be a header, footer or note sub-editor. See Customizing the Toolbar.

Plugin State

Read plugin state through the exported plugin keys:

import { highlightPluginKey, trackChangesKey } from "@stesura/core/plugins"; const highlightState = highlightPluginKey.getState(view.state); const trackChangesState = trackChangesKey.getState(view.state); // undefined unless the track-changes pack is mounted

Transaction Metadata

Plugins communicate through transaction metadata:

// Set metadata tr.setMeta(myPluginKey, { action: "update", value: 42 }); // Read metadata (in a plugin's apply function) const meta = tr.getMeta(myPluginKey);

Schema

The schema defines which nodes and marks are valid. Build it once per app with stesuraSchema and reuse it; it is a value, not a singleton.

import { stesuraSchema } from "@stesura/core/schema"; const schema = stesuraSchema(); schema.nodes.paragraph; schema.marks.textStyle;

See Schema for the full reference.

Plugins

ProseMirror plugins hold state and hook into editor behaviour:

import { Plugin, PluginKey } from "prosemirror-state"; const myPluginKey = new PluginKey<{ count: number }>("my-plugin"); const myPlugin = new Plugin({ key: myPluginKey, state: { init: () => ({ count: 0 }), apply(tr, value) { const meta = tr.getMeta(myPluginKey); return meta ? { count: meta.count } : value; }, }, props: { handleKeyDown(view, event) { return false; // true prevents default handling }, }, });

See Plugins for Stesura’s plugin set.

Key Helpers

@stesura/core/helpers wraps common ProseMirror queries:

import { findNode, findNodes, getAttributes, getMarkAttributes, getMarkType, getNodeType, isMarkActive, isNodeActive, } from "@stesura/core/helpers"; isMarkActive(state.schema.marks.strong, state); // at the selection isNodeActive(state, "heading", { level: 2 }); getAttributes(state, "heading"); // { level: 2, textAlign: null, … } findNode(state.doc, (node) => node.type.name === "toc"); // { node, pos } | undefined
HelperSignature
isMarkActive(mark, state) => boolean
isNodeActive(state, typeOrName, attrs?) => boolean
getAttributes / getMarkAttributes(state, typeOrName) => Attrs
findNode / findNodes(node, predicate): first match / all matches among node’s descendants, with positions
getNodeType / getMarkType(nameOrType, schema)
docChanged / getChangedRangesFor appendTransaction: did any transaction change the doc / the ranges a transform changed

Next Steps

Last updated on