Search page titles, headings and text across the docs.

Packages

On this page

@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/headless
import { 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 error

Every 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 }, where path walks the block tree and offset counts UTF-16 code units, like JavaScript strings. Errors are CanvistErrors with the same stable codes 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/server serves POST /convert and a /sync/:room WebSocket in a few lines.
  • Server-side rendering: @canvist/headless/ssr renders a document as HTML and CSS laid out exactly as @canvist/web paints 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/scene loads 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.

Docs