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.
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, type, attrs)— change block typeset_node_markup(pos, type, attrs, marks)— change a node's markupset_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 ContentExpr;
use HashMap;
let groups = from;
let expr = parse.unwrap;
assert!; // needs at least one block
assert!;
let s1 = expr.match_type.unwrap;
assert!; // one block = valid end
Supported syntax: *, +, ?, {n}, {n,m}, {n,}, |, (), group
references, and node type names.
Testing
The test suite is structured as:
-
Library unit tests (
cargo test --lib) — 44 tests covering 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— 9 tests fromprosemirror-model/test/test-resolve.tstests/test_mapping.rs— 11 tests fromprosemirror-transform/test/test-mapping.ts
-
Python-generated fixtures (
tests/spec/) — A Python script that usesprosemirror-pyto generate expected JSON outputs. These can be consumed by Rust tests for cross-implementation validation:# Generate fixtures using prosemirror-pyGenerated 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
| Feature | Default | Description |
|---|---|---|
cmark |
no | CommonMark parsing/serialization via pulldown-cmark |
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. - No DOM parsing/serialization — Server-side crate; HTML round-trip is not included.
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. 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.
License
MIT — see LICENSE.
Copyright 2026 Johannes Wilm
Copyright 2020 Daniel Seiler
Copyright 2015–2026 Marijn Haverbeke and others