Quickstart¶
Installation¶
Basic Usage¶
from quillmark import Document, Quill, Quillmark, OutputFormat
engine = Quillmark() # backend registry + render dispatcher
quill = Quill.from_path("path/to/quill") # engine-free, validated config data
markdown = """~~~
$quill: my_quill
$kind: main
title: Example Document
~~~
# Hello World
"""
doc = Document.from_markdown(markdown)
result = engine.render(quill, doc, OutputFormat.PDF)
with open("output.pdf", "wb") as f:
f.write(result.artifacts[0].bytes)
Installation¶
Basic Usage¶
// The single root import is the canonical API: Quill/Document (re-exported
// from the internal Typst-less core build) plus the Engine render
// dispatcher. An editor that only validates uses Quill/Document and loads
// no backend — Typst loads lazily on the first render.
import { Document, Quill, Engine } from "@quillmark/wasm";
const enc = new TextEncoder();
// A Quill is engine-free, validated data — no engine needed to load it.
const quill = Quill.fromTree(new Map([
["Quill.yaml", enc.encode("quill:\n name: my_quill\n backend: typst\n version: 1.0.0\n description: Demo\n plate_file: plate.typ\n")],
["plate.typ", enc.encode("#import \"@local/quillmark-helper:0.1.0\": data\n#data.at(\"$body\")\n")],
]));
const markdown = `~~~
$quill: my_quill
$kind: main
title: Example Document
~~~
# Hello World`;
const doc = Document.fromMarkdown(markdown);
// Rendering goes through the Engine. Its methods are async — the first call
// lazily loads the Typst backend binary; the canonical quill crosses into
// backend memory internally (no manual fromTree/fromJson needed).
const engine = new Engine();
const result = await engine.render(quill, doc, { format: "pdf" });
const pdfBytes = result.artifacts[0].bytes;
Live Preview (Canvas)¶
For editor-style previews, paint pages directly into a <canvas> instead
of round-tripping through PNG/SVG. paint is Typst-only and WASM-only,
and shares the cached compile with the byte-output render path.
const session = await engine.open(quill, doc); // compile once (async)
// Surface session-level diagnostics from compile time.
for (const w of session.warnings) console.warn(w.message);
function renderPage(canvas, page, userZoom = 1) {
const densityScale = (window.devicePixelRatio || 1) * userZoom;
const result = session.paint(canvas.getContext("2d"), page, {
layoutScale: 1,
densityScale,
});
canvas.style.width = `${result.layoutWidth}px`;
canvas.style.height = `${result.layoutHeight}px`;
}
for (let p = 0; p < session.pageCount; p++) renderPage(canvases[p], p);
session.free(); // when doc changes
Key contract points:
- The painter owns
canvas.width/canvas.heightand rewrites them on every call (so eachpaintis a full repaint — noclearRectneeded). The consumer ownscanvas.style.*and readsresult.layoutWidth/layoutHeightto size the display box. - Fold
devicePixelRatioand in-app zoom intodensityScale;layoutScalecontrols display size. - If
layoutScale * densityScalewould push either dimension past 16384 px,densityScaleis clamped to fit; compareresult.pixelWidthtoround(result.layoutWidth * densityScale)to detect the clamp. pageCountandpageSize(page)are stable for the session's lifetime (the compiled document is an immutable snapshot) — cache them.
Full design rationale: PREVIEW.md.