If you want to update this page or add new content, please submit a pull request to the Homepage.
ProseMirror
@yorkie-js/prosemirror is a ProseMirror binding for the Yorkie JS SDK that provides two-way synchronization between a ProseMirror editor and Yorkie's Tree CRDT. It handles content syncing, mark-to-element conversion, remote cursor display, and IME composition safely — so you can focus on building your editor.
Installation
npm install @yorkie-js/prosemirror
Peer Dependencies
The following packages must be installed alongside:
npm install @yorkie-js/sdk prosemirror-model prosemirror-state prosemirror-view
Quick Start
Below is a minimal example that connects a ProseMirror editor to Yorkie for real-time collaboration.
1import yorkie from '@yorkie-js/sdk';2import { EditorState } from 'prosemirror-state';3import { EditorView } from 'prosemirror-view';4import { schema } from 'prosemirror-schema-basic';5import { exampleSetup } from 'prosemirror-example-setup';6import {7 YorkieProseMirrorBinding,8 remoteSelectionPlugin,9} from '@yorkie-js/prosemirror';1011async function main() {12 // 1. Connect to Yorkie13 const client = new yorkie.Client({14 rpcAddr: 'https://api.yorkie.dev',15 apiKey: 'xxxxxxxxxxxxxxxxxxxx',16 });17 await client.activate();1819 const doc = new yorkie.Document('my-prosemirror-doc');20 await client.attach(doc, { initialPresence: {} });2122 // 2. Create the ProseMirror editor23 const view = new EditorView(document.getElementById('editor')!, {24 state: EditorState.create({25 doc: schema.node('doc', null, [26 schema.node('paragraph', null, [schema.text('Hello, world!')]),27 ]),28 plugins: [29 ...exampleSetup({ schema }),30 remoteSelectionPlugin(), // Renders remote selection highlights31 ],32 }),33 });3435 // 3. Bind ProseMirror to Yorkie36 const binding = new YorkieProseMirrorBinding(view, doc, 'tree', {37 cursors: {38 enabled: true,39 overlayElement: document.getElementById('cursor-overlay')!,40 wrapperElement: document.getElementById('editor-wrapper')!,41 },42 });43 binding.initialize();44}4546main();
The HTML structure should include an overlay container for remote cursors:
<div id="editor-wrapper" style="position: relative;"><div id="editor"></div><div id="cursor-overlay"></div></div>
The cursor-overlay element must be positioned absolutely within the wrapper:
#cursor-overlay {position: absolute;top: 0;left: 0;right: 0;bottom: 0;pointer-events: none;z-index: 10;}
When initialize() is called:
- If the Yorkie document has no tree yet, the binding creates one from the current ProseMirror document.
- If a tree already exists (another client created it), the binding loads it into the editor.
When you're done, call binding.destroy() to clean up subscriptions and event listeners.
Configuration
YorkieProseMirrorBinding accepts an options object as its fourth argument:
1const binding = new YorkieProseMirrorBinding(view, doc, 'tree', {2 markMapping: { strong: 'strong', em: 'em', code: 'code', link: 'link' },3 wrapperElementName: 'span',4 cursors: { enabled: true, overlayElement: cursorOverlayEl },5 onLog: (type, message) => console.log(`[${type}] ${message}`),6});
| Option | Type | Default | Description |
|---|---|---|---|
markMapping | MarkMapping | Auto-generated from schema | Maps ProseMirror mark names to Yorkie element type names |
wrapperElementName | string | 'span' | Element name used to wrap bare text alongside mark elements |
cursors | CursorOptions | undefined | Remote cursor display configuration |
onLog | (type, message) => void | undefined | Callback for sync event logging |
Mark Mapping
ProseMirror represents formatting as marks on text nodes (e.g., bold text has a strong mark). Yorkie's Tree CRDT has no mark concept — formatting is represented as wrapper elements instead.
The binding bridges this gap through mark mapping:
ProseMirror: paragraph > text("hello", marks=[strong])Yorkie Tree: <paragraph><strong><text>hello</text></strong></paragraph>
Default Mapping
If you don't provide a markMapping, the binding auto-generates one from your ProseMirror schema using identity mapping (mark name = element name). The built-in defaultMarkMapping covers the common cases:
1import { defaultMarkMapping } from '@yorkie-js/prosemirror';2// { strong: 'strong', em: 'em', code: 'code', link: 'link' }
Custom Mapping
If your schema uses different mark names, provide a custom mapping:
1const binding = new YorkieProseMirrorBinding(view, doc, 'tree', {2 markMapping: {3 bold: 'strong', // PM's 'bold' mark → Yorkie's 'strong' element4 italic: 'em', // PM's 'italic' mark → Yorkie's 'em' element5 },6});
Building from Schema
Use buildMarkMapping() to auto-generate a mapping from your schema with optional overrides:
1import { buildMarkMapping } from '@yorkie-js/prosemirror';23const mapping = buildMarkMapping(mySchema, {4 bold: 'strong', // Override specific marks5});
Remote Cursors
The binding provides two complementary systems for showing other users' cursors and selections.
Cursor Overlays
Cursor carets are rendered as DOM overlays positioned above the editor. Enable them via the cursors option:
1const binding = new YorkieProseMirrorBinding(view, doc, 'tree', {2 cursors: {3 enabled: true,4 overlayElement: document.getElementById('cursor-overlay')!,5 wrapperElement: document.getElementById('editor-wrapper')!, // Optional, for positioning reference6 colors: ['#FECEEA', '#FEF1D2', '#A9FDD8', '#D7F8FF', '#CEC5FA'], // Optional custom colors7 },8});
Each remote cursor displays as a colored vertical caret with a label showing the last 2 characters of the client ID.
Selection Highlights
remoteSelectionPlugin() is a ProseMirror plugin that renders inline decorations for remote users' text selections:
1import { remoteSelectionPlugin } from '@yorkie-js/prosemirror';23const state = EditorState.create({4 plugins: [5 ...otherPlugins,6 remoteSelectionPlugin(),7 ],8});
The binding automatically dispatches selection updates to this plugin when remote users change their selections.
How It Works
Two-Way Sync
Upstream (ProseMirror → Yorkie): When the local user edits, the binding compares the old and new ProseMirror documents using a two-level diffing strategy:
- Block-level diff — identifies which top-level blocks changed
- Character-level diff — if only one block changed with identical structure, produces minimal
tree.edit()calls for efficient concurrent editing - Full block replacement — falls back to replacing entire blocks for structural changes (e.g., toggling bold, splitting paragraphs)
Downstream (Yorkie → ProseMirror): When remote changes arrive, the binding builds a new ProseMirror document from the Yorkie tree and applies the minimal diff as a transaction, preserving local cursor position and undo history.
IME Composition Handling
During IME input (Chinese, Japanese, Korean), the binding defers remote changes that overlap the block being composed. Non-overlapping changes in other blocks are applied immediately. Deferred changes are flushed when composition ends, preventing the browser from breaking the active composition.
Lower-Level Utilities
For custom integrations, the package exports individual functions:
Format Conversion
1import { pmToYorkie, yorkieToJSON } from '@yorkie-js/prosemirror';23// ProseMirror Node → Yorkie tree JSON4const yorkieDoc = pmToYorkie(pmNode, markMapping, 'span');56// Yorkie tree → ProseMirror-compatible JSON (for Node.fromJSON)7const pmJSON = yorkieToJSON(tree, schema, elementToMarkMapping, 'span');
Manual Sync
1import {2 syncToYorkie,3 syncToPMIncremental,4 syncToPM,5 diffDocs,6 buildDocFromYorkieTree,7 applyDocDiff,8} from '@yorkie-js/prosemirror';910// Upstream: diff PM docs and apply to Yorkie tree11syncToYorkie(tree, oldDoc, newDoc, markMapping, onLog, 'span');1213// Downstream: incremental sync (preferred, preserves undo history)14syncToPMIncremental(view, tree, schema, elementToMarkMapping, onLog, 'span');1516// Downstream: full rebuild (used on initialization and as error recovery)17syncToPM(view, tree, schema, elementToMarkMapping, onLog, 'span');1819// Block-level diff between two PM docs20const diff = diffDocs(currentDoc, newDoc); // Returns { fromPos, toPos, slice }
Position Mapping
Bidirectional mapping between ProseMirror positions and Yorkie flat indices, useful for cursor synchronization:
1import {2 buildPositionMap,3 pmPosToYorkieIdx,4 yorkieIdxToPmPos,5} from '@yorkie-js/prosemirror';67const treeJSON = JSON.parse(tree.toJSON());8const map = buildPositionMap(pmDoc, treeJSON);910// Convert positions between coordinate systems11const yorkieIdx = pmPosToYorkieIdx(map, pmPos);12const pmPos = yorkieIdxToPmPos(map, yorkieIdx);
Examples
- ProseMirror example — Live collaborative rich-text editor
- vanilla-prosemirror — Minimal setup with toolbar, remote cursors, and devtools