# prosemirror-rs
A Rust implementation of [ProseMirror](https://prosemirror.net)'s core modules,
providing document model, transforms, and a runtime-loadable schema system.
This crate implements the same functionality as:
- [`prosemirror-model`](https://github.com/ProseMirror/prosemirror-model) — document model, nodes, fragments, marks, resolved positions, content matching, slicing, replacing
- [`prosemirror-transform`](https://github.com/ProseMirror/prosemirror-transform) — step types, position mapping, the `Transform` builder, 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:
**[→ View demos](https://fiduswriter.github.io/prosemirror-rs)**
The demos use [Rspack](https://rspack.dev) 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
```toml
[dependencies]
prosemirror = { path = "../prosemirror-rs" }
serde_json = "1"
```
Or from crates.io (when published):
```toml
[dependencies]
prosemirror = "0.2"
serde_json = "1"
```
### Build from source
```bash
git clone <repo-url>
cd prosemirror-rs
cargo build # debug build
cargo build --release # optimized build
```
### Run the tests
```bash
# Run all Rust tests (library + integration)
cargo test
# Run only the library unit tests
cargo test --lib
# Run a specific test
cargo test test_resolve
```
## 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:
| `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
| `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:
```rust
use prosemirror::dynamic::DynamicSchema;
use prosemirror::model::Node;
let schema_json = serde_json::json!({
"nodes": {
"doc": { "content": "block+" },
"paragraph": { "content": "inline*", "group": "block" },
"heading": { "attrs": { "level": { "default": 1 } },
"content": "inline*", "group": "block" },
"text": { "group": "inline" }
},
"marks": {
"strong": {},
"em": {}
}
});
let schema = DynamicSchema::from_json(&schema_json).unwrap();
// All dynamic operations must run inside with_types()
schema.with_types(|| {
let doc = schema.node_from_json(&serde_json::json!({
"type": "doc",
"content": [{
"type": "heading",
"attrs": { "level": 1 },
"content": [{ "type": "text", "text": "Hello" }]
}]
})).unwrap();
assert_eq!(doc.text_content(), "Hello");
assert_eq!(doc.child_count(), 1);
// Serialize back to JSON
let json = serde_json::to_value(&doc).unwrap();
});
```
## Step types
The `Step<S>` enum supports all step types from the JS/Python implementations:
| `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 document
- `get_map()` — return the `StepMap` describing position changes
- `invert(doc)` — return the inverse step (for undo)
- `map(mapping)` — map the step through a position mapping
- `merge(other)` — attempt to combine with an adjacent step
## Position mapping
```rust
use prosemirror::transform::{StepMap, Mapping, Mappable};
// Insert 4 characters at position 2
let insert = StepMap::new(vec![2, 0, 4]);
assert_eq!(insert.map(0, 1), 0); // before insertion: unchanged
assert_eq!(insert.map(3, 1), 7); // after insertion: shifted by 4
// Compose multiple maps
let mut mapping = Mapping::new();
mapping.append_map(StepMap::new(vec![2, 0, 4]), None);
mapping.append_map(StepMap::new(vec![10, 3, 0]), None);
assert_eq!(mapping.map(0, 1), 0);
assert_eq!(mapping.map(3, 1), 7);
```
## The Transform builder
```rust
use prosemirror::transform::Transform;
// Use with a concrete schema (see dynamic module example)
```
`Transform<S>` accumulates document changes:
- `replace(from, to, slice)` — low-level replace
- `delete(from, to)` — delete a range
- `insert(pos, content)` — insert content
- `add_mark(from, to, mark)` — add a mark
- `remove_mark(from, to, mark)` — remove a mark
- `split(pos, depth, types_after)` — split a node
- `join(pos, depth)` — join adjacent nodes
- `lift(range, target)` — lift content out of a wrapper
- `wrap(range, wrappers)` — wrap content in nodes
- `set_block_type(from, to, node_type)` — change block type
- `set_node_markup(pos, node_type, marks)` — change a node's type or marks; pass `None` as the node type to keep the current type
- `set_node_attribute(pos, attr, value)` — set a single attribute
- `set_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):
```rust
use prosemirror::dynamic::content_expr::parse_content_expr;
use std::collections::HashMap;
let groups = HashMap::from([
("block".to_string(), vec!["paragraph".to_string(), "heading".to_string()]),
("inline".to_string(), vec!["text".to_string()]),
]);
let expr = parse_content_expr("block+", &groups).unwrap();
assert!(!expr.valid_end(0)); // needs at least one block
assert!(expr.match_type(0, "paragraph").is_some());
let s1 = expr.match_type(0, "paragraph").unwrap();
assert!(expr.valid_end(s1)); // 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
```bash
# Run all Rust tests (library + integration)
cargo test --all
```
**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` — from `prosemirror-model/test/test-resolve.ts`
- `tests/test_mapping.rs` — from `prosemirror-transform/test/test-mapping.ts`
- `tests/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`](https://github.com/fellowapp/prosemirror-py), run against
the Rust implementation through a thin compatibility shim
(`python/tests/shim/prosemirror/`).
```bash
cd python
maturin develop
PYTHONPATH=tests/shim pytest tests/upstream/ -v
```
> **Note:** `test_dom.py` was 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`.
```bash
cd node
npm install
npm run build:release
node run-upstream-tests.mjs
```
> **Note:** `test-dom.js` is 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:
```bash
# Generate fixtures using prosemirror-py
cd ../prosemirror-py
python ../prosemirror-rs/tests/spec/generate_fixtures.py
```
Generated JSON files in `tests/spec/expected/`:
- `mapping.json` — StepMap/Mapping test cases
- `step_merge.json` — Step merge test cases
- `transform_marks.json` — addMark/removeMark test cases
- `transform_edit.json` — insert/delete test cases
- `transform_structure.json` — split test cases
- `replace.json` — Replace test cases
- `model.json` — Slice/size/textContent test cases
- `resolve.json` — ResolvedPos test cases
- `roundtrip.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`](https://prosemirror.net/docs/ref/#model) and
[`prosemirror-transform`](https://prosemirror.net/docs/ref/#transform), including
DOM serialization via `DOMSerializer` and `DOMParser`.
## Differences from JS/Python
- **Compile-time schema support** — The `Schema` trait and associated types
allow zero-cost abstraction over schemas defined as Rust types. The
`dynamic` module provides the runtime-loadable equivalent.
- **UTF-16 position tracking** — Like JavaScript, positions are counted in
UTF-16 code units. The `Text` type 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:
```bash
python scripts/bump-version.py # show current versions
python scripts/bump-version.py 0.4.0 # set all to 0.4.0
python scripts/bump-version.py --check # exit 1 if they differ
```
The files it manages:
| [`Cargo.toml`](Cargo.toml) | `[package] version` — the Rust crate published to crates.io |
| [`python/Cargo.toml`](python/Cargo.toml) | `[package] version` — the native extension built by maturin |
| [`python/pyproject.toml`](python/pyproject.toml) | `[project] version` — the Python package published to PyPI |
| [`node/Cargo.toml`](node/Cargo.toml) | `[package] version` — the native extension built by cargo |
| [`node/package.json`](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:
```bash
git tag v0.3.2
git push origin v0.3.2
```
## Credits
This crate was originally created by **Daniel Seiler**
([Xiphoseer](https://github.com/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](https://github.com/fiduswriter),
<johannes@fiduswriter.org>).
The ProseMirror data model and step format are the work of
**Marijn Haverbeke** and the ProseMirror contributors. See
[prosemirror.net](https://prosemirror.net) for the upstream project.
The Python test suite used for cross-implementation validation is taken from
[`prosemirror-py`](https://github.com/fellowapp/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.](https://github.com/fellowapp)
## License
MIT — see [LICENSE](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