prosemirror 0.5.2

A Rust implementation of ProseMirror's document model and transform pipeline
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# 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:

| 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:

```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:

| 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 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:

| File                                             | Key                                                         |
| ------------------------------------------------ | ----------------------------------------------------------- |
| [`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