@canvist/headless
canvist’s document model and editing engine for JavaScript with no DOM: Node, Deno, Bun, and Workers. Convert between ProseMirror/TipTap JSON, HTML, Markdown, and text, run typed editing commands, and serve Yjs collaboration from the same Rust core the editor uses, compiled to WebAssembly.
npm install @canvist/headlessimport { createEngine } from "@canvist/headless";
const engine = await createEngine(); // TipTap's StarterKit
const doc = engine.parse("markdown", "Hello world");
doc.select({ path: [0], offset: 6 }, { path: [0], offset: 11 });
doc.commands.toggleBold();
doc.export("html"); // "<p>Hello <strong>world</strong></p>"Types follow your extensions
A document only offers the commands, queries, blocks, and marks of the extensions you registered:
import { bold, createEngine, heading } from "@canvist/headless";
const engine = await createEngine([bold, heading]);
const doc = engine.create();
doc.commands.setHeading({ level: 2 }); // levels are 1 | 2 | 3 | 4 | 5 | 6
doc.commands.toggleItalic(); // type error: italic is not registered
doc.commands.setHeading({ level: 7 }); // type errorEvery other extension
@canvist/headless compiles in TipTap’s StarterKit. Every other extension canvist ships, from highlights and text styles to mentions, emoji, and media, is in @canvist/headless/full: a larger WebAssembly module with the same API.
import { createEngine, highlight, starterKit } from "@canvist/headless/full";
const engine = await createEngine([...starterKit, highlight]);
const doc = engine.parse("markdown", "Hello world");
doc.selectAll();
doc.commands.toggleHighlight();
doc.export("html"); // "<p><mark>Hello world</mark></p>"What else it does
- Positions:
{ path, offset }, wherepathwalks the block tree andoffsetcounts UTF-16 code units, like JavaScript strings. Errors areCanvistErrors with the same stablecodes as the Rust and Dart packages. - Collaboration: a document can be a Yjs peer and a y-websocket-compatible server endpoint (
share,syncStart,receiveSync).examples/serverservesPOST /convertand a/sync/:roomWebSocket in a few lines. - Server-side rendering:
@canvist/headless/ssrrenders a document as HTML and CSS laid out exactly as@canvist/webpaints it, so the live editor hydrates from it without moving a pixel, and as SVG for previews. It’s a separate WebAssembly module too. See Server-side rendering. - Canvas scenes:
@canvist/headless/sceneloads canvist canvas scenes and exports SVG and PNG, for thumbnails, social cards, and designs an agent writes. It’s a separate WebAssembly module, so the document entry point stays small. - Workers: runtimes without a file system pass the module they imported:
createEngine(undefined, { wasm }).
A typed command costs about 0.7 µs in Node and a collaborator’s keystroke about 4.6 µs, at 10,000 paragraphs. node bench/overhead.ts reproduces the numbers.
Development
npm ci
npm run build:wasm # the StarterKit module into wasm/, the full one into wasm-full/, the scene module into wasm-scene/, and the rendering module into wasm-ssr/
npm test # conformance and API tests in Node
deno task test # the same, plus the server example, in Deno
npm run check:types # tsc --noEmit, including the type-level tests
npm run generate # regenerate the extension descriptors
npm run build # JavaScript and declarations for npm, into dist/The tests run the shared fixtures in conformance/, which the Rust and Dart packages run too.