@quillmark/wasm 0.67.0 → 0.69.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -79,53 +79,89 @@ O(1) getter for the number of composable cards (excluding the main card).
79
79
  Use this to validate indices before calling card mutators (`removeCard`,
80
80
  `updateCardField`, etc.) without allocating the full `cards` array.
81
81
 
82
+ ### `quill.form(doc)`
83
+
84
+ Returns `{ main, cards, diagnostics }` — a schema-aware snapshot of `doc`
85
+ without invoking the backend. `diagnostics` contains validation errors and
86
+ warnings; an empty array means the document is valid. Useful for validating
87
+ content without rendering:
88
+
89
+ ```ts
90
+ const form = quill.form(Document.fromMarkdown(markdown));
91
+ const errors = form.diagnostics.filter(d => d.severity === "error");
92
+ ```
93
+
94
+ ### `quill.render(parsed, opts?)` vs. `quill.open(parsed)`
95
+
96
+ Use **`Quill.render`** for one-shot exports (PDF/SVG/PNG) — compiles, emits
97
+ artifacts, done. Use **`RenderSession`** (returned by `Quill.open`) for
98
+ reactive previews where you'll paint or re-emit pages multiple times: the
99
+ session retains the compiled snapshot so subsequent `paint` / `render`
100
+ calls skip recompilation. Don't open a session per export.
101
+
82
102
  ### `quill.render(parsed, opts?)`
83
103
  Render with a pre-parsed `Document`.
84
104
 
85
105
  ### `quill.open(parsed)` + `session.render(opts?)`
86
106
  Open once, render all or selected pages (`opts.pages`).
87
107
 
88
- The session also exposes `pageCount`, `backendId`, `warnings` (snapshot of
89
- session-level diagnostics attached at `open` time), `pageSize(page)`, and
90
- `paint(ctx, page, scale)` for canvas previews. See below.
108
+ The session also exposes `pageCount`, `backendId`, `supportsCanvas`,
109
+ `warnings` (snapshot of session-level diagnostics attached at `open` time),
110
+ `pageSize(page)`, and `paint(ctx, page, opts?)` for canvas previews. See
111
+ below.
112
+
113
+ A document that compiles to zero pages still produces a valid session
114
+ (`pageCount === 0`); `paint(ctx, 0)` and `pageSize(0)` then throw
115
+ `page index 0 out of range (pageCount=0)`. Branch on `pageCount === 0` to
116
+ render a "no pages to preview" UI without relying on the throw.
91
117
 
92
118
  ### Canvas Preview (Typst only)
93
119
 
94
- `session.paint(ctx, page, scale)` rasterizes a page directly into a
95
- `CanvasRenderingContext2D`, skipping PNG/SVG byte round-trips. Pair with
96
- `session.pageSize(page)` to size the canvas:
120
+ `session.paint(ctx, page, opts?)` rasterizes a page directly into a
121
+ `CanvasRenderingContext2D` (main thread) or
122
+ `OffscreenCanvasRenderingContext2D` (Worker), skipping PNG/SVG byte
123
+ round-trips.
124
+
125
+ The painter owns `canvas.width` / `canvas.height` — it sizes the backing
126
+ store itself. Consumers own `canvas.style.*` (or the layout system that
127
+ sets them) and read `layoutWidth` / `layoutHeight` from the returned
128
+ `PaintResult`.
97
129
 
98
130
  ```ts
99
- const dpr = window.devicePixelRatio || 1;
100
- const userZoom = 1; // your zoom UI
101
- const scale = dpr * userZoom; // multiplier on 72 ppi
102
-
103
- const { widthPt, heightPt } = session.pageSize(0);
104
- // Reassigning canvas.width/height clears the backing store, which is what
105
- // you want between pages. If you reuse the same canvas at the same size
106
- // (e.g. repaint after a zoom that didn't change scale), call
107
- // ctx.clearRect(0, 0, canvas.width, canvas.height) before paint instead.
108
- canvas.width = Math.round(widthPt * scale); // device px
109
- canvas.height = Math.round(heightPt * scale);
110
- canvas.style.width = `${widthPt * userZoom}px`;
111
- canvas.style.height = `${heightPt * userZoom}px`;
112
-
113
- session.paint(canvas.getContext("2d"), 0, scale);
131
+ const result = session.paint(canvas.getContext("2d"), 0, {
132
+ layoutScale: 1, // layout px per Typst pt
133
+ densityScale: window.devicePixelRatio, // backing-store density
134
+ });
135
+
136
+ canvas.style.width = `${result.layoutWidth}px`;
137
+ canvas.style.height = `${result.layoutHeight}px`;
114
138
  ```
115
139
 
116
- - `scale` is a multiplier on Typst's natural 72 ppi (1 pt → 1 device
117
- pixel at `scale = 1`). Always include `devicePixelRatio` for crisp
118
- output.
140
+ - `layoutScale` (default 1) sets the canvas's display-box size:
141
+ `layoutWidth = widthPt * layoutScale`. For on-screen canvases this is
142
+ CSS pixels per Typst point. Defaults to 1 (one CSS pixel per pt).
143
+ - `densityScale` (default 1) is the backing-store density multiplier.
144
+ Fold `window.devicePixelRatio`, in-app zoom, and `visualViewport.scale`
145
+ (pinch-zoom) into a single value here. Pass `devicePixelRatio` for
146
+ crisp output on high-DPI displays.
147
+ - The effective rasterization scale is `layoutScale * densityScale`. If
148
+ that would exceed the safe maximum (16384 px per side), `densityScale`
149
+ is clamped proportionally; compare `result.pixelWidth` against
150
+ `Math.round(result.layoutWidth * densityScale)` to detect.
151
+ - `paint` is always a full repaint — setting the backing-store width /
152
+ height clears it. No `clearRect` required.
119
153
  - `pageCount` and `pageSize(page)` are stable for the session's
120
154
  lifetime (immutable snapshot) — cache them.
121
- - Setting `canvas.width` / `canvas.height` clears the backing store; if
122
- you reuse a canvas without resizing, call `clearRect` before `paint`.
123
- - Currently main-thread only: `paint` accepts `CanvasRenderingContext2D`,
124
- not `OffscreenCanvasRenderingContext2D`. Worker support is on the
125
- follow-up list.
126
- - Backend support: Typst only. Calling `paint` on a session opened by
127
- any other backend throws an error that includes the resolved
128
- `backendId`.
155
+ - Worker support: pass an `OffscreenCanvasRenderingContext2D` and the
156
+ same call signature works. `layoutWidth` / `layoutHeight` are
157
+ informational in that mode (no CSS layout box); fold everything into
158
+ `densityScale`. Loading the WASM module inside a Worker is the host's
159
+ responsibility.
160
+ - Backend support: gated by `supportsCanvas`. Probe upfront with
161
+ `quill.supportsCanvas` (or `session.supportsCanvas`) before mounting a
162
+ canvas-based UI; the throw on `paint` / `pageSize` remains the
163
+ enforcement contract and includes the resolved `backendId` for
164
+ debugging.
129
165
 
130
166
  ### Errors
131
167
 
@@ -160,6 +196,22 @@ without manual `.free()` discipline. `.free()` is still emitted as an eager
160
196
  teardown hook for callers that want deterministic release. Requires
161
197
  Node 14.6+ / current evergreen browsers (all supported targets).
162
198
 
199
+ For environments where `using` (the [explicit resource management][erm]
200
+ proposal) hasn't landed, use an explicit `try` / `finally`:
201
+
202
+ ```ts
203
+ const session = quill.open(doc);
204
+ try {
205
+ for (let p = 0; p < session.pageCount; p++) {
206
+ session.paint(ctx, p);
207
+ }
208
+ } finally {
209
+ session.free();
210
+ }
211
+ ```
212
+
213
+ [erm]: https://github.com/tc39/proposal-explicit-resource-management
214
+
163
215
  ## Notes
164
216
 
165
217
  - Parsed markdown requires top-level `QUILL` in frontmatter. Empty input
package/bundler/wasm.d.ts CHANGED
@@ -17,14 +17,179 @@ export interface CardInput {
17
17
  /**
18
18
  * Page dimensions in Typst points (1 pt = 1/72 inch).
19
19
  *
20
- * Returned by `RenderSession.pageSize`. Use these to size a canvas backing
21
- * store (`widthPt * scale × heightPt * scale`) before calling `paint`.
20
+ * Report-only: the painter sizes the canvas itself based on
21
+ * `PaintOptions`. `pageSize` is exposed for callers that need page
22
+ * geometry up-front (e.g. to lay out a scrollable list of canvases
23
+ * before any pixels are rendered).
22
24
  */
23
25
  export interface PageSize {
24
26
  widthPt: number;
25
27
  heightPt: number;
26
28
  }
27
29
 
30
+ /**
31
+ * Inputs to `RenderSession.paint`. Both fields are optional and default
32
+ * to `1`.
33
+ *
34
+ * - `layoutScale` — layout-space pixels per Typst point. For on-screen
35
+ * canvases this is CSS pixels per pt; the page's layout-pixel size is
36
+ * `widthPt * layoutScale × heightPt * layoutScale`. The painter
37
+ * surfaces these dimensions as `layoutWidth` / `layoutHeight` so
38
+ * consumers can drive `canvas.style.*` (or any layout system).
39
+ * - `densityScale` — backing-store density multiplier. Fold
40
+ * `window.devicePixelRatio`, in-app zoom, and `visualViewport.scale`
41
+ * (pinch-zoom) into a single value here. Defaults to `1`, which
42
+ * produces a non-retina backing store — pass `window.devicePixelRatio`
43
+ * for crisp output on high-DPI displays.
44
+ *
45
+ * The effective rasterization scale is `layoutScale * densityScale`.
46
+ * Both must be finite and `> 0`. For `OffscreenCanvasRenderingContext2D`
47
+ * the two collapse to a single scalar; folding everything into
48
+ * `densityScale` is the simplest convention.
49
+ */
50
+ export interface PaintOptions {
51
+ layoutScale?: number;
52
+ densityScale?: number;
53
+ }
54
+
55
+ /**
56
+ * Returned by `RenderSession.paint`.
57
+ *
58
+ * - `layoutWidth` / `layoutHeight` — layout-pixel dimensions of the
59
+ * canvas's display box. For on-screen canvases this is CSS pixels:
60
+ * set `canvas.style.width = layoutWidth + "px"` and
61
+ * `canvas.style.height = layoutHeight + "px"` (or feed these into
62
+ * your layout system). Independent of `densityScale`.
63
+ * - `pixelWidth` / `pixelHeight` — integer backing-store pixel
64
+ * dimensions the painter wrote to `canvas.width` / `canvas.height`.
65
+ * Equal to `round(layoutWidth * densityScale)` ×
66
+ * `round(layoutHeight * densityScale)` *unless* the requested backing
67
+ * exceeded the painter's safe maximum (16384 px per side), in which
68
+ * case `densityScale` was clamped to fit. Detect clamping via
69
+ * `pixelWidth < round(layoutWidth * densityScale)`.
70
+ *
71
+ * The painter owns `canvas.width` / `canvas.height`; consumers must not
72
+ * write to them. The painter does **not** touch `canvas.style.*`;
73
+ * consumers own layout.
74
+ *
75
+ * For `OffscreenCanvasRenderingContext2D` (Worker rasterization, no
76
+ * DOM), `layoutWidth` / `layoutHeight` are informational — there's no
77
+ * CSS layout box to apply them to.
78
+ */
79
+ export interface PaintResult {
80
+ layoutWidth: number;
81
+ layoutHeight: number;
82
+ pixelWidth: number;
83
+ pixelHeight: number;
84
+ }
85
+
86
+
87
+
88
+ /** UI layout hints for a single field. */
89
+ export interface QuillFieldUi {
90
+ group?: string;
91
+ order?: number;
92
+ compact?: boolean;
93
+ multiline?: boolean;
94
+ }
95
+
96
+ /** UI layout hints for a card (main or named card type). */
97
+ export interface QuillCardUi {
98
+ hide_body?: boolean;
99
+ default_title?: string;
100
+ }
101
+
102
+ /** Schema entry for a single field declared in a quill's `Quill.yaml`. */
103
+ export interface QuillFieldSchema {
104
+ type: "string" | "number" | "integer" | "boolean" | "array" | "object" | "date" | "datetime" | "markdown";
105
+ title?: string;
106
+ description?: string;
107
+ default?: unknown;
108
+ examples?: unknown;
109
+ required?: boolean;
110
+ enum?: string[];
111
+ ui?: QuillFieldUi;
112
+ properties?: Record<string, QuillFieldSchema>;
113
+ items?: QuillFieldSchema;
114
+ }
115
+
116
+ /** Schema entry for the main card or a named card type. */
117
+ export interface QuillCardSchema {
118
+ title?: string;
119
+ description?: string;
120
+ fields: Record<string, QuillFieldSchema>;
121
+ ui?: QuillCardUi;
122
+ }
123
+
124
+ /**
125
+ * Public schema contract returned as `QuillMetadata.schema`.
126
+ *
127
+ * Identical to `QuillConfig::public_schema()` on the Rust side.
128
+ */
129
+ export interface QuillSchema {
130
+ name: string;
131
+ main: QuillCardSchema;
132
+ /** Present only when the quill declares at least one named card type. */
133
+ card_types?: Record<string, QuillCardSchema>;
134
+ /** The quill's bundled example document, if declared. */
135
+ example?: string;
136
+ }
137
+
138
+ /**
139
+ * Read-only snapshot of the loaded quill's engine info and declared schema.
140
+ * Returned by `Quill.metadata`.
141
+ *
142
+ * Well-known keys are strongly typed; any additional keys declared under
143
+ * `quill:` in `Quill.yaml` appear as `unknown`.
144
+ */
145
+ export interface QuillMetadata {
146
+ schema: QuillSchema;
147
+ backend: string;
148
+ version: string;
149
+ author: string;
150
+ description: string;
151
+ supportedFormats: OutputFormat[];
152
+ [key: string]: unknown;
153
+ }
154
+
155
+ /** Source of a field's effective value in a form view. */
156
+ export type FormFieldSource = "document" | "default" | "missing";
157
+
158
+ /**
159
+ * A single field's view within a `FormCard`.
160
+ *
161
+ * - `value` — the document-supplied value (`null` when absent).
162
+ * - `default` — the schema default (`null` when no default is declared).
163
+ * - `source` — where the effective value comes from.
164
+ */
165
+ export interface FormFieldValue {
166
+ value: unknown;
167
+ default: unknown;
168
+ source: FormFieldSource;
169
+ }
170
+
171
+ /**
172
+ * A card viewed through its schema, as returned by `Quill.form`,
173
+ * `Quill.blankMain`, and `Quill.blankCard`.
174
+ */
175
+ export interface FormCard {
176
+ schema: QuillCardSchema;
177
+ values: Record<string, FormFieldValue>;
178
+ }
179
+
180
+ /**
181
+ * Schema-aware form view of a document, returned by `Quill.form`.
182
+ *
183
+ * - `main` — the main card viewed through the quill's main schema.
184
+ * - `cards` — composable card blocks, in document order (unknown tags excluded).
185
+ * - `diagnostics` — diagnostics from unknown card tags and validation.
186
+ */
187
+ export interface Form {
188
+ main: FormCard;
189
+ cards: FormCard[];
190
+ diagnostics: Diagnostic[];
191
+ }
192
+
28
193
 
29
194
  export interface Artifact {
30
195
  format: OutputFormat;
@@ -299,7 +464,7 @@ export class Quill {
299
464
  *
300
465
  * [`Form::cards`]: quillmark::form::Form::cards
301
466
  */
302
- blankCard(card_type: string): any;
467
+ blankCard(card_type: string): FormCard | null;
303
468
  /**
304
469
  * A blank form for the main card — no document values supplied.
305
470
  *
@@ -309,7 +474,7 @@ export class Quill {
309
474
  *
310
475
  * [`Form::main`]: quillmark::form::Form::main
311
476
  */
312
- blankMain(): any;
477
+ blankMain(): FormCard;
313
478
  /**
314
479
  * The schema-aware form view of `doc`.
315
480
  *
@@ -329,7 +494,7 @@ export class Quill {
329
494
  *
330
495
  * [`Form`]: quillmark::form::Form
331
496
  */
332
- form(doc: Document): any;
497
+ form(doc: Document): Form;
333
498
  /**
334
499
  * Open an iterative render session for page-selective rendering.
335
500
  */
@@ -367,7 +532,16 @@ export class Quill {
367
532
  * Equivalent by value for the lifetime of the handle; the quill is
368
533
  * immutable once constructed.
369
534
  */
370
- readonly metadata: any;
535
+ readonly metadata: QuillMetadata;
536
+ /**
537
+ * Whether this quill's backend supports canvas preview.
538
+ *
539
+ * `true` iff `RenderSession.paint` and `RenderSession.pageSize` will
540
+ * succeed for sessions opened by this quill. Use this as a precondition
541
+ * probe before mounting a canvas-based preview UI; the throw on `paint`
542
+ * remains the enforcement contract.
543
+ */
544
+ readonly supportsCanvas: boolean;
371
545
  }
372
546
 
373
547
  /**
@@ -391,6 +565,21 @@ export class Quillmark {
391
565
  quill(tree: Map<string, Uint8Array>): Quill;
392
566
  }
393
567
 
568
+ /**
569
+ * An iterative render handle backed by an immutable compiled snapshot.
570
+ *
571
+ * Created via [`Quill::open`]. Holds the compiled output so that
572
+ * [`RenderSession::render`], [`RenderSession::paint`], and
573
+ * [`RenderSession::page_size`] can be called repeatedly without
574
+ * recompiling.
575
+ *
576
+ * **Empty documents.** A document that compiles to zero pages still
577
+ * produces a valid session (`pageCount === 0`). Iterating
578
+ * `0..pageCount` is then a no-op; calling `paint(ctx, 0)` or
579
+ * `pageSize(0)` throws `"... page index 0 out of range
580
+ * (pageCount=0)"`. Hosts that surface "no pages to preview" UI should
581
+ * branch on `pageCount === 0` rather than on a thrown error.
582
+ */
394
583
  export class RenderSession {
395
584
  private constructor();
396
585
  free(): void;
@@ -398,6 +587,11 @@ export class RenderSession {
398
587
  /**
399
588
  * Page dimensions in Typst points (1 pt = 1/72 inch).
400
589
  *
590
+ * Report-only: the painter sizes the canvas itself based on
591
+ * `PaintOptions`. Exposed for consumers that need page geometry
592
+ * up-front (e.g. to lay out a scrollable list of canvases before
593
+ * any pixels are rendered).
594
+ *
401
595
  * Stable for a given `page` across the session's lifetime — the
402
596
  * compiled document is an immutable snapshot, so callers can cache
403
597
  * results.
@@ -409,33 +603,51 @@ export class RenderSession {
409
603
  /**
410
604
  * Paint `page` into a 2D canvas context.
411
605
  *
412
- * `scale` multiplies Typst's natural 72 ppi (1 pt 1 device pixel at
413
- * `scale = 1`). Typical usage:
414
- * `scale = (window.devicePixelRatio || 1) * userZoom`.
415
- *
416
- * The caller must size `ctx.canvas` so that
417
- * `canvas.width === round(widthPt * scale)` and `canvas.height ===
418
- * round(heightPt * scale)` *before* calling `paint`. Setting
419
- * `canvas.width` / `canvas.height` clears the backing store, which is
420
- * the recommended way to handle page-to-page transitions; if you
421
- * reuse a canvas without resizing, call
422
- * `ctx.clearRect(0, 0, canvas.width, canvas.height)` first to avoid
423
- * stale pixels showing through transparent regions.
424
- *
425
- * `paint` writes into the backing store at origin `(0, 0)` and does
426
- * not clear outside the rendered region.
427
- *
428
- * Throws if the backend does not support canvas preview (the message
429
- * includes the resolved `backendId` for debugging), or if `page` is
430
- * out of range.
431
- */
432
- paint(ctx: CanvasRenderingContext2D, page: number, scale: number): void;
606
+ * Accepts either a `CanvasRenderingContext2D` (main thread) or an
607
+ * `OffscreenCanvasRenderingContext2D` (Worker / off-DOM rasterization).
608
+ * Both dispatch to the same Rust rasterizer; the dispatch happens at
609
+ * the JS boundary so neither context type is privileged.
610
+ *
611
+ * The painter owns `canvas.width` / `canvas.height` and writes them
612
+ * itself; consumers must not. The painter does not touch
613
+ * `canvas.style.*` that's layout, owned by the consumer (see
614
+ * `PaintResult.layoutWidth` / `layoutHeight`).
615
+ *
616
+ * `opts.layoutScale` (default 1.0) is layout-space pixels per Typst
617
+ * point and determines the canvas's display-box size. `opts.densityScale`
618
+ * (default 1.0) is the rasterization density multiplier the consumer
619
+ * folds `window.devicePixelRatio`, in-app zoom, and
620
+ * `visualViewport.scale` (pinch-zoom) into. The effective
621
+ * rasterization scale is `layoutScale * densityScale`.
622
+ *
623
+ * If `layoutScale * densityScale` would exceed the safe backing-store
624
+ * maximum (16384 px per side), `densityScale` is clamped
625
+ * proportionally so the largest dimension fits. The actual
626
+ * backing-store dimensions are reported in the returned
627
+ * `PaintResult` — compare against
628
+ * `round(layoutWidth * densityScale)` to detect clamping.
629
+ *
630
+ * Each call resets the backing store (`paint` is always a full
631
+ * repaint). Consumers do not need to call `clearRect`.
632
+ *
633
+ * Throws when:
634
+ * - the backend does not support canvas preview (message includes the
635
+ * resolved `backendId`),
636
+ * - `page` is out of range,
637
+ * - `ctx` is neither `CanvasRenderingContext2D` nor
638
+ * `OffscreenCanvasRenderingContext2D`,
639
+ * - `opts.layoutScale` or `opts.densityScale` is non-finite or `<= 0`.
640
+ */
641
+ paint(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, page: number, opts: PaintOptions | undefined): PaintResult;
433
642
  /**
434
643
  * Render all or selected pages from this session.
435
644
  */
436
645
  render(opts?: RenderOptions | null): RenderResult;
437
646
  /**
438
647
  * The backend that produced this session (e.g. `"typst"`).
648
+ *
649
+ * Equal to the `backendId` of the [`Quill`] that opened this session
650
+ * (sessions inherit their quill's backend), so checking either is fine.
439
651
  */
440
652
  readonly backendId: string;
441
653
  /**
@@ -445,6 +657,14 @@ export class RenderSession {
445
657
  * document is an immutable snapshot.
446
658
  */
447
659
  readonly pageCount: number;
660
+ /**
661
+ * Whether this session's backend supports canvas preview.
662
+ *
663
+ * `true` iff [`paint`](Self::paint) and [`page_size`](Self::page_size)
664
+ * will succeed. Equal to `Quill.supportsCanvas` for the quill that
665
+ * opened this session.
666
+ */
667
+ readonly supportsCanvas: boolean;
448
668
  /**
449
669
  * Session-level warnings attached at `quill.open(...)` time.
450
670
  *