prosemirror-rs
A Rust implementation of ProseMirror's core modules, providing document model, transforms, and a runtime-loadable schema system.
This crate implements the same functionality as:
prosemirror-model— document model, nodes, fragments, marks, resolved positions, content matching, slicing, replacingprosemirror-transform— step types, position mapping, theTransformbuilder, structure utilities, smart replace
The canonical references are the JavaScript source (primary) and Python port (secondary). The Rust implementation aims to produce identical results.
It ships as a single npm package — prosemirror-rs — that auto-dispatches to a
native addon in Node.js and WebAssembly in the browser, so existing ProseMirror
packages (view, state, commands, etc.) can use it as a drop-in replacement for
both prosemirror-model and prosemirror-transform.
Demos
Live browser demos running prosemirror-rs via WebAssembly are hosted on GitHub Pages:
The demos use Rspack to bundle the application and alias
prosemirror-model / prosemirror-transform to prosemirror-rs so that all
ProseMirror packages automatically use the Rust implementation.
- Basic — a simple rich-text editor with formatting, lists, and history.
- Advanced — two side-by-side editors with simulated real-time collaboration and tables.
Quick start
Add to your project
[]
= { = "../prosemirror-rs" }
= "1"
Or from crates.io (when published):
[]
= "0.2"
= "1"
Build from source
Run the tests
# Run all Rust tests (library + integration)
# Run only the library unit tests
# Run a specific test
Performance
The Rust implementation is significantly faster and more memory-efficient than
the pure-Python prosemirror package. Benchmark results from fiduswriter
(2000 documents × 500 steps each):
────────────────────────────────────────────────────────────
Backend : python
Documents : 2000 Steps/doc : 500
Wall time : 12.347 s
CPU time : 12.340 s
Memory Δ : +1973.9 MB
────────────────────────────────────────────────────────────
Backend : rust
Documents : 2000 Steps/doc : 500
Wall time : 6.047 s
CPU time : 6.040 s
Memory Δ : +22.1 MB
============================================================
COMPARISON (Rust vs Python)
============================================================
Wall time : 0.49× (faster)
CPU time : 0.49× (faster)
Memory Δ : 0.01× (faster)
============================================================
The native bindings (Python and Node.js) share the same underlying Rust engine, so similar gains apply regardless of which language you use to integrate with prosemirror-rs.
Architecture
The crate has three main modules:
model — Document model
Core data structures for representing ProseMirror documents:
| Type | Description |
|---|---|
Node<S> trait |
A document node (element, text, or leaf) |
Fragment<S> |
An ordered collection of child nodes |
Mark<S> / MarkSet<S> |
Inline formatting (bold, italic, etc.) |
ResolvedPos<S> |
A position resolved against a document tree |
NodeRange<S> |
A range between two resolved positions at a given depth |
Slice<S> |
A piece of document content with open boundaries |
ContentMatch<S> trait |
DFA-based content expression matching |
NodeType<S> trait |
Type descriptor for a node kind |
Schema trait |
Defines the full set of node types, mark types, and content rules |
transform — Document transformations
| Type | Description |
|---|---|
Step<S> enum |
A single atomic change (replace, add-mark, remove-mark, attr, etc.) |
StepMap / Mapping |
Position offset maps for tracking changes |
Transform<S> |
Builder that accumulates steps and tracks the document |
replace_step() |
Smart replace algorithm (the "Fitter") |
can_split(), can_join(), lift_target(), find_wrapping() |
Structure analysis |
join_point(), insert_point(), drop_point() |
Position finding |
dynamic — Runtime schema from JSON
Load a ProseMirror schema at runtime from a JSON SchemaSpec, the same format
used by the JavaScript and Python implementations:
use DynamicSchema;
use Node;
let schema_json = json!;
let schema = from_json.unwrap;
// All dynamic operations must run inside with_types()
schema.with_types;
Step types
The Step<S> enum supports all step types from the JS/Python implementations:
| Variant | JS class | Description |
|---|---|---|
Replace |
ReplaceStep |
Replace a range with a slice |
ReplaceAround |
ReplaceAroundStep |
Replace while preserving structure |
AddMark |
AddMarkStep |
Add a mark to an inline range |
RemoveMark |
RemoveMarkStep |
Remove a mark from an inline range |
AddNodeMark |
AddNodeMarkStep |
Add a mark to a specific node |
RemoveNodeMark |
RemoveNodeMarkStep |
Remove a mark from a specific node |
Attr |
AttrStep |
Set an attribute on a node |
DocAttr |
DocAttrStep |
Set an attribute on the document root |
Each step type supports:
apply(doc)— apply the step to produce a new documentget_map()— return theStepMapdescribing position changesinvert(doc)— return the inverse step (for undo)map(mapping)— map the step through a position mappingmerge(other)— attempt to combine with an adjacent step
Position mapping
use ;
// Insert 4 characters at position 2
let insert = new;
assert_eq!; // before insertion: unchanged
assert_eq!; // after insertion: shifted by 4
// Compose multiple maps
let mut mapping = new;
mapping.append_map;
mapping.append_map;
assert_eq!;
assert_eq!;
The Transform builder
use Transform;
// Use with a concrete schema (see dynamic module example)
Transform<S> accumulates document changes:
replace(from, to, slice)— low-level replacedelete(from, to)— delete a rangeinsert(pos, content)— insert contentadd_mark(from, to, mark)— add a markremove_mark(from, to, mark)— remove a marksplit(pos, depth, types_after)— split a nodejoin(pos, depth)— join adjacent nodeslift(range, target)— lift content out of a wrapperwrap(range, wrappers)— wrap content in nodesset_block_type(from, to, node_type)— change block typeset_node_markup(pos, node_type, marks)— change a node's type or marks; passNoneas the node type to keep the current typeset_node_attribute(pos, attr, value)— set a single attributeset_doc_attribute(attr, value)— set a document attribute
Content expressions
Content expressions like "block+", "inline*", "paragraph block*" are
parsed at runtime into a DFA (deterministic finite automaton):
use parse_content_expr;
use HashMap;
let groups = from;
let expr = parse_content_expr.unwrap;
assert!; // needs at least one block
assert!;
let s1 = expr.match_type.unwrap;
assert!; // one block = valid end
Currently implemented syntax includes *, +, ?, |, simple grouping with
(), group references, and node type names. Numeric repetition operators such as
{n}, {n,m}, and {n,} are not implemented yet.
Testing
The test suite is structured as:
Rust tests
# Run all Rust tests (library + integration)
Library unit tests (cargo test --lib) — covers model internals,
transform operations, content expression parsing, and dynamic schema loading.
Integration tests (tests/) — Ported from the JS and Python test suites:
tests/test_resolve.rs— fromprosemirror-model/test/test-resolve.tstests/test_mapping.rs— fromprosemirror-transform/test/test-mapping.tstests/test_transform_wrap.rs— Wrapper-stack validation and content-match tests
Python upstream tests (against Rust shim)
The python/tests/upstream/ directory contains the full test suite from
prosemirror-py, run against
the Rust implementation through a thin compatibility shim
(python/tests/shim/prosemirror/).
PYTHONPATH=tests/shim
Note:
test_dom.pywas removed because DOM serialization is out of scope for this project.
Node.js upstream tests (against Rust shim)
The node/run-upstream-tests.mjs script copies the upstream JavaScript test
files from ../prosemirror-model/test/ and ../prosemirror-transform/test/,
rewrites their ES imports to CommonJS requires pointing at a local shim
(node/test-shim/), and runs them with Mocha + ist.
Note:
test-dom.jsis excluded because DOM serialization is out of scope.
Python-generated fixtures
tests/spec/ contains a Python script that uses prosemirror-py to generate
expected JSON outputs. These can be consumed by Rust tests for
cross-implementation validation:
# Generate fixtures using prosemirror-py
Generated JSON files in tests/spec/expected/:
mapping.json— StepMap/Mapping test casesstep_merge.json— Step merge test casestransform_marks.json— addMark/removeMark test casestransform_edit.json— insert/delete test casestransform_structure.json— split test casesreplace.json— Replace test casesmodel.json— Slice/size/textContent test casesresolve.json— ResolvedPos test casesroundtrip.json— JSON round-trip test cases
Feature flags
This crate currently defines no Cargo feature flags.
API coverage
The Node.js, browser (WebAssembly), and Python bindings expose the full public
API documented at https://prosemirror.net/docs/ref/ for both
prosemirror-model and
prosemirror-transform, including
DOM serialization via DOMSerializer and DOMParser.
Differences from JS/Python
- Compile-time schema support — The
Schematrait and associated types allow zero-cost abstraction over schemas defined as Rust types. Thedynamicmodule provides the runtime-loadable equivalent. - UTF-16 position tracking — Like JavaScript, positions are counted in
UTF-16 code units. The
Texttype tracks both UTF-8 and UTF-16 lengths.
Releasing a new version
Before tagging a release, bump the version number in all five packaging files to the same new version string. Use the helper script:
The files it manages:
| File | Key |
|---|---|
Cargo.toml |
[package] version — the Rust crate published to crates.io |
python/Cargo.toml |
[package] version — the native extension built by maturin |
python/pyproject.toml |
[project] version — the Python package published to PyPI |
node/Cargo.toml |
[package] version — the native extension built by cargo |
node/package.json |
version — the npm package published to npm |
Once the files are updated and committed, push an annotated tag to trigger the publish workflow:
Credits
This crate was originally created by Daniel Seiler (Xiphoseer, me@dseiler.eu), who designed and implemented the document model, transform pipeline, and runtime schema system. Mustafa J (mustafa.0x@gmail.com) has made substantial contributions to the core Rust implementation. The project is now maintained by Johannes Wilm (FidusWriter, johannes@fiduswriter.org).
The ProseMirror data model and step format are the work of Marijn Haverbeke and the ProseMirror contributors. See prosemirror.net for the upstream project.
The Python test suite used for cross-implementation validation is taken from
prosemirror-py, a Python port
of ProseMirror originally created by Shen Li and maintained by
Samuel Cormier-Iijima, Patrick Gingras, and others at
Fellow Insights Inc.
License
MIT — see LICENSE.
Copyright 2026 Johannes Wilm
Copyright 2025–2026 Mustafa J
Copyright 2020 Daniel Seiler
Copyright 2019 Fellow Insights Inc. (prosemirror-py Python port)
Copyright 2015–2026 Marijn Haverbeke and others