@tiptap-studio/rusty-parser
The Rust parser compiled to WebAssembly, for browsers and bundlers.
@tiptap-studio/rusty-parser is tiptap-rusty-parser compiled to
WebAssembly. It parses, queries, edits, diffs and validates Tiptap / ProseMirror
JSONContent from JavaScript, with the document held in WebAssembly memory.
The npm version follows the crate. This section documents 0.6.1.
Install
npm install @tiptap-studio/rusty-parserThe package exports one class and six functions:
| Export | Kind | Page |
|---|---|---|
TiptapDoc | class | TiptapDoc |
mapPosition, mapPositionRange | functions | Positions |
compose, compact, mapPath | functions | Change algebra |
validateNode | function | Schema |
Bundler setup
The package is built with wasm-pack build --target bundler. It is an ES module
("type": "module") whose entry file imports the WebAssembly binary directly:
import * as wasm from "./tiptap_rusty_parser_wasm_bg.wasm";The module instantiates itself when it is first imported. There is no init() function
to call. Your toolchain has to support importing a .wasm file as a module:
- webpack 5: turn on
experiments.asyncWebAssembly. - Vite: add
vite-plugin-wasm, plusvite-plugin-top-level-awaitunless yourbuild.targetisesnext. - Node.js: Node 24.22 imports it directly and prints an
ExperimentalWarningabout WebAssembly modules.
For webpack:
// webpack.config.js
export default {
experiments: { asyncWebAssembly: true },
};For Vite:
// vite.config.js
import { defineConfig } from "vite";
import wasm from "vite-plugin-wasm";
import topLevelAwait from "vite-plugin-top-level-await";
export default defineConfig({
plugins: [wasm(), topLevelAwait()],
});Next.js 16 builds with Turbopack by default, and a webpack() hook in next.config only
applies to webpack builds (next build --webpack).
Check your bundler's documentation for the exact setting in the version you use. Only the
bundler build is published. A --target web build, which loads with an init() call and
needs no bundler support, is not on npm.
First example
import { TiptapDoc } from "@tiptap-studio/rusty-parser";
const doc = TiptapDoc.fromJSON({
type: "doc",
content: [
{ type: "heading", attrs: { level: 2 }, content: [{ type: "text", text: "Title" }] },
{ type: "paragraph", content: [{ type: "text", text: "Hello world" }] },
],
});
doc.textContent(); // "TitleHello world"
doc.wordCount(); // 3
// Find nodes by index path, then edit by path.
const [headingPath] = doc.pathsByType("heading"); // [0]
doc.setAttr(headingPath, "level", 1);
doc.addMark([0, 0], "bold", undefined); // true
const json = doc.toJSON(); // a plain JSONContent object
doc.free();Paths are plain number[] index paths from the root: [] is the root, [1, 0] is the
first child of the second child. See Paths and positions
for the model.
How values cross the boundary
The document tree stays inside WebAssembly. Only the values you pass in and get back cross
the boundary, converted with serde-wasm-bindgen:
- Arguments such as paths, nodes, marks, attributes, schemas and change lists are plain JS objects and arrays.
- Return values are plain JS objects and arrays. They are copies: changing a node returned
by
nodeAtorbyTypedoes not change the document. Edit through theTiptapDocmethods instead. - Unknown node and mark fields are kept, so
fromJSONthentoJSONreturns the same content.
Memory and disposal
A TiptapDoc owns memory inside the WebAssembly instance. JavaScript's garbage collector
cannot see how large it is.
-
Call
doc.free()when you are done with a document. Using the object afterwards throws. -
The class also implements
Symbol.dispose, so a runtime with explicit resource management can free it at the end of a scope withusing:{ using doc = TiptapDoc.fromJSON(json); doc.textContent(); } // doc.free() runs here -
A
TiptapDocthat is never freed is released by aFinalizationRegistryafter it is garbage collected, where the runtime supports one. That can happen late or not at all, so free documents explicitly in long-running code, especially in loops.
Values returned by methods, such as toJSON() or a change list, are ordinary JS values and
need no freeing.
Errors
Methods throw a JS Error when an argument does not have the expected shape, when a path
does not resolve, or when an edit is out of range. The message is the Rust error's text,
for example pos: 99 out of range (document size 20). See
TypeScript and errors.
What is not exposed
The JS API is a subset of the Rust crate. These Rust features have no JS binding yet:
- the query language and closure queries;
- transactions (
Transform), see Transactions and undo for the JS equivalent; unmap_pathandrebase_changefrom the change algebra;- custom leaf policies for flat positions;
- source maps and Markdown conversion.