@quillmark/wasm 0.92.1 → 0.94.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/core/wasm_bg.wasm CHANGED
Binary file
@@ -3,20 +3,31 @@
3
3
  export const memory: WebAssembly.Memory;
4
4
  export const __wbg_document_free: (a: number, b: number) => void;
5
5
  export const __wbg_quill_free: (a: number, b: number) => void;
6
+ export const document_addCard: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void;
7
+ export const document_applyChange: (a: number, b: number, c: number, d: number) => void;
6
8
  export const document_blueprintInstruction: (a: number, b: number, c: number) => void;
7
9
  export const document_cardCount: (a: number) => number;
8
10
  export const document_cards: (a: number, b: number) => void;
9
11
  export const document_clone: (a: number) => number;
12
+ export const document_commitCardField: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
13
+ export const document_commitCardFields: (a: number, b: number, c: number, d: number, e: number) => void;
14
+ export const document_commitField: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
15
+ export const document_commitFields: (a: number, b: number, c: number, d: number) => void;
10
16
  export const document_currentSchemaVersion: (a: number) => void;
11
17
  export const document_equals: (a: number, b: number) => number;
12
18
  export const document_formatDiagnostic: (a: number, b: number) => void;
13
19
  export const document_formatRules: (a: number) => void;
14
20
  export const document_fromJson: (a: number, b: number, c: number) => void;
15
21
  export const document_fromMarkdown: (a: number, b: number, c: number) => void;
22
+ export const document_get: (a: number, b: number, c: number, d: number) => void;
23
+ export const document_getMarkdown: (a: number, b: number, c: number, d: number) => void;
16
24
  export const document_insertCard: (a: number, b: number, c: number, d: number) => void;
25
+ export const document_install: (a: number, b: number, c: number, d: number) => void;
26
+ export const document_loadJson: (a: number, b: number, c: number, d: number) => void;
17
27
  export const document_main: (a: number, b: number) => void;
18
28
  export const document_makeCard: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
19
29
  export const document_moveCard: (a: number, b: number, c: number, d: number) => void;
30
+ export const document_new: (a: number, b: number, c: number) => void;
20
31
  export const document_pushCard: (a: number, b: number, c: number) => void;
21
32
  export const document_quillRef: (a: number, b: number) => void;
22
33
  export const document_quillRefHint: (a: number) => void;
@@ -28,24 +39,29 @@ export const document_removeExt: (a: number, b: number) => void;
28
39
  export const document_removeExtNamespace: (a: number, b: number, c: number, d: number) => void;
29
40
  export const document_removeField: (a: number, b: number, c: number, d: number) => void;
30
41
  export const document_removeSeedNamespace: (a: number, b: number, c: number, d: number) => void;
31
- export const document_replaceBody: (a: number, b: number, c: number) => void;
42
+ export const document_replaceBody: (a: number, b: number, c: number, d: number) => void;
43
+ export const document_revise: (a: number, b: number, c: number, d: number, e: number) => void;
32
44
  export const document_schemaVersionOf: (a: number, b: number, c: number) => void;
33
45
  export const document_setCardExt: (a: number, b: number, c: number, d: number) => void;
34
46
  export const document_setCardExtNamespace: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
47
+ export const document_setCardField: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
48
+ export const document_setCardFields: (a: number, b: number, c: number, d: number) => void;
35
49
  export const document_setCardKind: (a: number, b: number, c: number, d: number, e: number) => void;
36
50
  export const document_setExt: (a: number, b: number, c: number) => void;
37
51
  export const document_setExtNamespace: (a: number, b: number, c: number, d: number, e: number) => void;
38
52
  export const document_setField: (a: number, b: number, c: number, d: number, e: number) => void;
53
+ export const document_setFields: (a: number, b: number, c: number) => void;
39
54
  export const document_setFill: (a: number, b: number, c: number, d: number, e: number) => void;
40
55
  export const document_setQuillRef: (a: number, b: number, c: number, d: number) => void;
41
56
  export const document_setSeedNamespace: (a: number, b: number, c: number, d: number, e: number) => void;
42
57
  export const document_toJson: (a: number, b: number) => void;
43
58
  export const document_toMarkdown: (a: number, b: number) => void;
44
59
  export const document_tryFromJson: (a: number, b: number) => number;
45
- export const document_updateCardBody: (a: number, b: number, c: number, d: number, e: number) => void;
46
- export const document_updateCardField: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
47
60
  export const document_warnings: (a: number, b: number) => void;
61
+ export const exportMarkdown: (a: number, b: number) => void;
62
+ export const importMarkdown: (a: number, b: number, c: number) => void;
48
63
  export const init: () => void;
64
+ export const mapPos: (a: number, b: number, c: number, d: number) => void;
49
65
  export const quill_backendId: (a: number, b: number) => void;
50
66
  export const quill_blueprint: (a: number, b: number) => void;
51
67
  export const quill_fromTree: (a: number, b: number) => void;
@@ -56,6 +72,7 @@ export const quill_seedDocument: (a: number) => number;
56
72
  export const quill_seedMain: (a: number, b: number) => void;
57
73
  export const quill_toTree: (a: number) => number;
58
74
  export const quill_validate: (a: number, b: number, c: number) => void;
75
+ export const rebase: (a: number, b: number, c: number, d: number) => void;
59
76
  export const __wbindgen_export: (a: number, b: number) => number;
60
77
  export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
61
78
  export const __wbindgen_export3: (a: number) => void;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@quillmark/wasm",
3
- "version": "0.92.1",
4
- "description": "WebAssembly bindings for quillmark",
3
+ "version": "0.94.0",
4
+ "description": "WebAssembly bindings for Quillmark, a schema-driven document engine",
5
5
  "type": "module",
6
6
  "license": "MIT OR Apache-2.0",
7
7
  "engines": {
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "git+https://github.com/quillmark-org/quillmark.git"
12
+ "url": "git+https://github.com/borb-sh/quillmark.git"
13
13
  },
14
14
  "files": [
15
15
  "core",
@@ -30,6 +30,7 @@
30
30
  "sideEffects": [
31
31
  "./core/wasm.js",
32
32
  "./backends/typst/wasm.js",
33
+ "./backends/pdfform/wasm.js",
33
34
  "./runtime/runtime.js"
34
35
  ]
35
36
  }
@@ -14,6 +14,8 @@
14
14
  // public entry point, so this is a structural fact. Replacing the re-export
15
15
  // with a wrapper is a breaking design change, not a refactor. See runtime.js.
16
16
  export { Quill, Document, init } from '../core/wasm.js';
17
+ // The document-free corpus codec, re-exported from the core build.
18
+ export { importMarkdown, exportMarkdown, rebase, mapPos } from '../core/wasm.js';
17
19
 
18
20
  // Core-build types consumers read off `Quill`/`Document`.
19
21
  export type {
@@ -68,7 +70,7 @@ export declare function isQuillmarkError(e: unknown): e is QuillmarkError;
68
70
  // (run via `npm run typecheck`), so these and the generated
69
71
  // `pkg/backends/typst/wasm.d.ts` cannot silently diverge.
70
72
 
71
- import type { Quill, Document } from '../core/wasm.js';
73
+ import type { Quill, Document, Card } from '../core/wasm.js';
72
74
  import type { Diagnostic } from '../core/wasm.js';
73
75
 
74
76
  /** Canonical contract every backend build must satisfy. One emitted output. */
@@ -84,6 +86,92 @@ export interface RenderOptions {
84
86
  ppi?: number;
85
87
  pages?: number[];
86
88
  producer?: string;
89
+ /**
90
+ * Populate {@link RenderResult.regions} with the schema-field geometry
91
+ * sidecar (the same entries {@link LiveSession.regions} serves), for
92
+ * consumers without a live session — e.g. overlays over a one-shot SVG
93
+ * export. Defaults to `false`: exports pay no introspection cost.
94
+ */
95
+ regions?: boolean;
96
+ }
97
+
98
+ /**
99
+ * How precisely a {@link CorpusHit.pos} resolved — the marker a caret UI reads
100
+ * to decide whether to trust the offset. Never sub-cluster: `'cluster'` is the
101
+ * finest, `'segment'` the floor it degrades to on origin-less ink.
102
+ *
103
+ * - `'cluster'` — `pos` is the first corpus char of the cluster under the point
104
+ * (an escaped/CJK/shaping cluster floors to its first char). Place the caret
105
+ * at `pos` directly.
106
+ * - `'segment'` — the point hit origin-less ink (list markers, numbering, a
107
+ * multi-line code fence's interior), so `pos` degraded to the containing
108
+ * segment's start. Treat `pos` as the selected segment, not a caret.
109
+ */
110
+ export type HitGranularity = 'cluster' | 'segment';
111
+
112
+ /** A click resolved to a field and USV offset into its RichText. */
113
+ export interface CorpusHit {
114
+ field: string;
115
+ pos: number;
116
+ /**
117
+ * Whether {@link pos} is cluster-exact or floored to the segment start
118
+ * ({@link HitGranularity}). Absent when the backend does not report it.
119
+ */
120
+ granularity?: HitGranularity;
121
+ }
122
+
123
+ /**
124
+ * A rendered field region: the quill schema field address (`field`) plus its
125
+ * geometry (`rect`) on the page. Emitted by backends that place schema fields
126
+ * (`pdfform` AcroForm widgets; Typst form-fields and span-tracked content —
127
+ * richtext bodies, `richtext[]` elements, card content fields, direct scalar
128
+ * references). Only fields with a schema address produce a region — a
129
+ * backend-only widget produces none, and the backend widget name never
130
+ * appears.
131
+ *
132
+ * Use it to scroll to / highlight the focused field's rect; for the click
133
+ * direction use {@link LiveSession.fieldAt}, which resolves a point on *any*
134
+ * placement, not just the first one surfaced here. Geometry only —
135
+ * `LiveSession.paint` already bakes every value into the raster (see
136
+ * {@link LiveSession}), so a region is never a compositing input.
137
+ *
138
+ * COORDINATE TRANSFORM. `rect` is in PDF points with a **bottom-left** origin.
139
+ *
140
+ * For an **HTML/CSS overlay** on a `width:100%` canvas, position hotspots as
141
+ * percentages of the page — they track the displayed size across DPI and pane
142
+ * resize for free, and only the Y axis flips:
143
+ *
144
+ * ```js
145
+ * const [x0, y0, x1, y1] = region.rect; // PDF pt, bottom-left origin
146
+ * const left = (x0 / pageWidthPt) * 100; // % of page (from PageSize.widthPt)
147
+ * const top = (1 - y1 / pageHeightPt) * 100; // % — flip Y (from PageSize.heightPt)
148
+ * const width = ((x1 - x0) / pageWidthPt) * 100;
149
+ * const height = ((y1 - y0) / pageHeightPt) * 100;
150
+ * ```
151
+ *
152
+ * For painting **into a raster** at `renderScale` (= `layoutScale × densityScale`),
153
+ * use the device-pixel form instead:
154
+ *
155
+ * ```js
156
+ * const left = x0 * renderScale;
157
+ * const top = (pageHeightPt - y1) * renderScale; // flip Y
158
+ * ```
159
+ */
160
+ export interface FieldRegion {
161
+ /** Quill schema field path (e.g. `"signature_block"`), not a backend widget name. */
162
+ field: string;
163
+ /** 0-based page index. */
164
+ page: number;
165
+ /** `[x0, y0, x1, y1]` in PDF points (1/72″), bottom-left origin. */
166
+ rect: [number, number, number, number];
167
+ /**
168
+ * The corpus slice this box covers — USV `[start, end)` into the field's
169
+ * `RichText` for content ink (one segment), absent for a scalar reference
170
+ * site or widget. Consumers key segment highlights on it;
171
+ * {@link LiveSession.fieldBoxes} unions same-page segments for the
172
+ * whole-field box.
173
+ */
174
+ span?: [number, number];
87
175
  }
88
176
 
89
177
  /** Canonical contract every backend build must satisfy. Result of one render. */
@@ -92,6 +180,14 @@ export interface RenderResult {
92
180
  warnings: Diagnostic[];
93
181
  outputFormat: OutputFormat;
94
182
  renderTimeMs: number;
183
+ /**
184
+ * Schema-field geometry sidecar — populated only when
185
+ * {@link RenderOptions.regions} requested it; empty otherwise. The same
186
+ * entries {@link LiveSession.regions} serves, for consumers without a live
187
+ * session. Page indices are document-space even under a `pages` subset
188
+ * render.
189
+ */
190
+ regions: FieldRegion[];
95
191
  }
96
192
 
97
193
  /** Canonical contract every backend build must satisfy. The emittable formats. */
@@ -99,7 +195,6 @@ export type OutputFormat = 'pdf' | 'svg' | 'txt' | 'png';
99
195
 
100
196
  /**
101
197
  * Canonical contract every backend build must satisfy. Page geometry in pt.
102
- * @experimental Part of the iterative-session/canvas surface — see {@link RenderSession}.
103
198
  */
104
199
  export interface PageSize {
105
200
  widthPt: number;
@@ -108,7 +203,6 @@ export interface PageSize {
108
203
 
109
204
  /**
110
205
  * Canonical contract every backend build must satisfy. Inputs to `paint`.
111
- * @experimental Part of the iterative-session/canvas surface — see {@link RenderSession}.
112
206
  */
113
207
  export interface PaintOptions {
114
208
  layoutScale?: number;
@@ -117,13 +211,36 @@ export interface PaintOptions {
117
211
 
118
212
  /**
119
213
  * Canonical contract every backend build must satisfy. Output of `paint`.
120
- * @experimental Part of the iterative-session/canvas surface — see {@link RenderSession}.
121
214
  */
122
215
  export interface PaintResult {
123
- layoutWidth: number;
216
+ layoutWidth: number; // canvas.style.width target; independent of densityScale
124
217
  layoutHeight: number;
125
- pixelWidth: number;
218
+ pixelWidth: number; // canvas.width the painter wrote (clamped at 16384)
126
219
  pixelHeight: number;
220
+ /**
221
+ * True when `MAX_BACKING_DIMENSION` forced `densityScale` down: the page is
222
+ * painted at fewer device pixels than requested and renders soft at the same
223
+ * `canvas.style` size. Reads the clamp off the return value instead of the
224
+ * `pixelWidth < round(layoutWidth × densityScale)` derivation.
225
+ */
226
+ clamped: boolean;
227
+ /**
228
+ * The `densityScale` actually applied — equal to the requested value unless
229
+ * `clamped`, then reduced proportionally. `layoutScale × effectiveDensityScale`
230
+ * is the scale the backing store was rasterized at.
231
+ */
232
+ effectiveDensityScale: number;
233
+ }
234
+
235
+ /**
236
+ * Canonical contract every backend build must satisfy. Output of
237
+ * {@link LiveSession.apply}: `dirtyPages` lists the pages whose rendered
238
+ * content differs from the previous compile, including added pages; removed
239
+ * pages are implied by `pageCount`. Repaint `dirty ∩ visible`.
240
+ */
241
+ export interface ChangeSet {
242
+ pageCount: number;
243
+ dirtyPages: number[];
127
244
  }
128
245
 
129
246
  /**
@@ -161,16 +278,20 @@ export interface EngineOptions {
161
278
  export declare class Engine {
162
279
  constructor(options?: EngineOptions);
163
280
 
164
- /** Render `doc` against `quill` in one shot. */
281
+ /**
282
+ * Render `doc` against `quill` in one shot. Both handles are read
283
+ * synchronously before the first await, so the caller may `free()` them as
284
+ * soon as this call returns.
285
+ */
165
286
  render(quill: Quill, doc: Document, options?: RenderOptions): Promise<RenderResult>;
166
287
 
167
288
  /**
168
- * Open an iterative render session (canvas preview / per-page paint).
169
- * @experimental Ships ahead of its first production consumer (the designed
170
- * canvas live-preview path see `prose/canon/PREVIEW.md`). The session/paint
171
- * surface may change in any 0.x release; `render()` is the stable path.
289
+ * Open a live render session (canvas preview / per-page paint / `apply`).
290
+ * The `quill` and `doc` handles are read synchronously before the first
291
+ * await, so the caller may `free()` them as soon as this call returns; the
292
+ * caller owns the returned session and must `.free()` it.
172
293
  */
173
- open(quill: Quill, doc: Document): Promise<RenderSession>;
294
+ open(quill: Quill, doc: Document): Promise<LiveSession>;
174
295
 
175
296
  /**
176
297
  * Output formats `quill`'s backend can emit. An ALWAYS-free pre-render probe:
@@ -180,29 +301,126 @@ export declare class Engine {
180
301
  supportedFormats(quill: Quill): Promise<OutputFormat[]>;
181
302
 
182
303
  /**
183
- * Whether `quill`'s backend can paint sessions to a canvas. Same always-free
184
- * probe as `supportedFormats`: answered from the descriptor's required
185
- * `canvas` manifest, no binary load and no quill clone.
186
- * @experimental Probes the experimental session/canvas surface see {@link RenderSession}.
304
+ * Whether `quill`'s BACKEND can paint sessions to a canvas a pre-session
305
+ * ESTIMATE, not a fact about any particular compile. Same always-free probe
306
+ * as `supportedFormats`: answered from the descriptor's required `canvas`
307
+ * manifest, no binary load and no quill clone. Both the Typst and pdfform
308
+ * backends report `true` here unconditionally; each paints a complete page
309
+ * raster (see {@link LiveSession.paint}) — but a specific compile can still
310
+ * refuse to paint (e.g. a 0-page document), so this can answer `true` while
311
+ * the resulting {@link LiveSession.supportsCanvas} answers `false`. Gate
312
+ * mounting a canvas UI on this; gate the actual `paint` call on the session's
313
+ * getter once `open()` has run.
187
314
  */
188
315
  supportsCanvas(quill: Quill): Promise<boolean>;
189
316
  }
190
317
 
191
318
  /**
192
319
  * Iterative render session over a compiled snapshot. `free()` when done.
193
- * @experimental The whole session/canvas-paint surface (`Engine.open`,
194
- * `RenderSession`, `PaintOptions`, `PaintResult`, `PageSize`) ships ahead of
195
- * its first production consumer and may change shape in any 0.x release.
196
- * The stable render path is `Engine.render`.
320
+ *
321
+ * CANVAS PAINT IS COMPLETE. {@link LiveSession.paint} writes a complete page
322
+ * raster every piece of page content is already visible in the painted
323
+ * pixels, with NO compositing required by the caller. Both backends that
324
+ * support canvas satisfy this: Typst rasterizes its laid-out page natively;
325
+ * pdfform pre-flattens bound field values into the page content and rasterizes
326
+ * that, so field values appear in the raster on their own.
327
+ * {@link LiveSession.regions} carries schema-field geometry for interactive
328
+ * overlays / cross-navigation drawn on top of the raster; it is never needed to
329
+ * complete the picture.
197
330
  */
198
- export declare class RenderSession {
331
+ export declare class LiveSession {
199
332
  private constructor();
200
333
  readonly pageCount: number;
201
334
  readonly backendId: string;
335
+ /**
336
+ * `true` iff `paint`/`pageSize` will succeed for THIS compile — the
337
+ * authoritative answer, derived from the session's canvas seam, so it can
338
+ * never disagree with what `paint` actually does. This can be `false` even
339
+ * when {@link Engine.supportsCanvas} answered `true` for the same `quill`
340
+ * (that probe is a pre-session backend estimate; e.g. a canvas-capable
341
+ * backend compiled to a 0-page document has nothing to paint). Re-check
342
+ * this getter after `open()` rather than relying on the engine hint alone.
343
+ */
202
344
  readonly supportsCanvas: boolean;
203
345
  readonly warnings: Diagnostic[];
346
+ /**
347
+ * Recompile the session against `doc` — the edit verb of a live preview.
348
+ * Transactional: on throw every read (`render`, `paint`, `pageSize`,
349
+ * `regions`) keeps serving the last-good compile, and the session recovers
350
+ * on the next successful `apply`. On success reads serve the new compile;
351
+ * repaint `dirtyPages ∩ visible`.
352
+ */
353
+ apply(doc: Document): ChangeSet;
204
354
  render(options?: RenderOptions): RenderResult;
355
+ /**
356
+ * Schema-field geometry for this compiled session, keyed on quill schema
357
+ * field path. A session-level query: no render, no byte artifact. Read it
358
+ * to scroll to / highlight the focused field over a `paint`-ed canvas;
359
+ * the click direction is {@link fieldAt}. Empty for backends that place
360
+ * no schema fields.
361
+ *
362
+ * `field` is **not** unique: a content field surfaces its **first
363
+ * placement** as one {@link FieldRegion} per page that placement touches
364
+ * (so a highlight covers continuation pages); a scalar referenced at
365
+ * several plate sites surfaces each site; tracked content plus a
366
+ * `field:`-bound widget yields both, widget ordered first. Group by
367
+ * `field` — every entry routes to that field. Later placements of one
368
+ * content value are not enumerated; {@link fieldAt} still resolves
369
+ * clicks on them.
370
+ */
371
+ regions(): FieldRegion[];
372
+ /**
373
+ * The whole-field highlight boxes for `field` — one union rect per page,
374
+ * over the field's `span`-bearing content segments (the "highlight the
375
+ * focused field" quantity). Owns the union {@link regions} leaves derived
376
+ * (span-filter + per-page union), keeping `regions()` the low-level disjoint
377
+ * truth, so a consumer stops reimplementing it. **Content only** — a field
378
+ * placed solely as a scalar reference or a bound widget carries no `span`
379
+ * and returns `[]`; its box is a single {@link regions} rect. Reflects the
380
+ * current compile, like `regions()`.
381
+ */
382
+ fieldBoxes(field: string): FieldRegion[];
383
+ /**
384
+ * The schema field whose content is under a point on `page` — the forward
385
+ * (click → field) direction: hit-test a click against the compiled
386
+ * document and get back the field address to focus in the editor, or
387
+ * `undefined` off any field's ink. `x`/`y` are PDF points with a
388
+ * **bottom-left** origin, the same space as {@link FieldRegion.rect} —
389
+ * from a canvas click, invert the overlay transform documented there:
390
+ * `x = clickPx.x / renderScale`,
391
+ * `y = pageHeightPt - clickPx.y / renderScale`. Unlike {@link regions},
392
+ * *every* placement answers, not just the first.
393
+ */
394
+ fieldAt(page: number, x: number, y: number): string | undefined;
395
+ /**
396
+ * Fine-grained click → corpus position (caret placement). Same PDF-point
397
+ * space as {@link fieldAt}; `undefined` off all content ink.
398
+ */
399
+ positionAt(page: number, x: number, y: number): CorpusHit | undefined;
400
+ /** Corpus position → caret rect — reverse of {@link positionAt}. */
401
+ locate(field: string, pos: number): FieldRegion | undefined;
402
+ /** Page geometry in points (1/72″). Report-only; the painter sizes the canvas. */
205
403
  pageSize(page: number): PageSize;
404
+ /**
405
+ * Paint `page` into a 2D canvas context, sizing the backing store itself
406
+ * (it owns `canvas.width`/`height`; the caller owns `canvas.style.*`). The
407
+ * painted raster is COMPLETE — all page content visible, no caller-side
408
+ * compositing (Typst rasterizes natively; pdfform rasterizes its
409
+ * pre-flattened page). Effective rasterization scale is
410
+ * `layoutScale × densityScale`, clamped so neither backing dimension exceeds
411
+ * 16384 px — {@link PaintResult.clamped} reports the clamp and
412
+ * {@link PaintResult.effectiveDensityScale} the density actually applied.
413
+ *
414
+ * The write is a whole-backing-store `putImageData`, which bypasses the 2D
415
+ * context transform, `globalAlpha`, and clip: the painter owns the entire
416
+ * canvas, so give each visible page its own `` element. You cannot
417
+ * paint two pages into one canvas, paint into a sub-rect, or apply a context
418
+ * transform through this call — the raster is complete precisely so you never
419
+ * need to. Keep the per-page canvases alive while their pages stay near the
420
+ * viewport: each `paint` re-rasterizes from scratch, so reusing (pooling) a
421
+ * canvas across pages on scroll re-runs a full render, whereas an idle canvas
422
+ * retains its pixels for free.
423
+ */
206
424
  paint(
207
425
  ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
208
426
  page: number,
@@ -210,3 +428,91 @@ export declare class RenderSession {
210
428
  ): PaintResult;
211
429
  free(): void;
212
430
  }
431
+
432
+ // ── Typed writer — the tier-1 front door ────────────────────────────────────
433
+
434
+ // `quill.writer(doc)` is patched onto the re-exported `Quill` prototype (the
435
+ // class is re-exported verbatim, so the method is declared by merging into the
436
+ // core module's `Quill` rather than redeclaring the class).
437
+ declare module '../core/wasm.js' {
438
+ interface Quill {
439
+ /**
440
+ * Bind this quill's schema to `doc` for typed writes — the documented
441
+ * front door, mirroring core's `quill.writer(&mut doc)`. The schema grants
442
+ * the typing, so the quill is the factory. The returned writer holds both
443
+ * handles by reference and owns neither (nothing to `free()`); it is
444
+ * ephemeral by convention — bind, write, discard.
445
+ */
446
+ writer(doc: Document): DocumentWriter;
447
+ }
448
+ }
449
+
450
+ /**
451
+ * A `Document` bound to its `Quill` for typed writes — the tier-1 default,
452
+ * constructed via {@link Quill.writer}. Speaks names, values, and markdown; a
453
+ * consumer here never meets an `Addr`, a corpus object, or a `Delta`. Bare
454
+ * `set` / `setAll` / `setBody` / `addCard` / `card(i).set` instead of threading
455
+ * the `quill` handle through every `commit*` call. Holds both handles by
456
+ * reference and owns neither — nothing to `free()`.
457
+ *
458
+ * Typed commit is the default whenever a quill is in hand: it resolves each
459
+ * field's schema type and strict-commits it, throwing `UnknownField` for a name
460
+ * the schema does not declare — on the typed path an undeclared name is a typo,
461
+ * not a fallback. The raw `Document.setField` / `setFields` verbs remain the
462
+ * deliberate quill-free primitive (standalone data, storage/migration infra, or
463
+ * holding not-yet-conforming in-progress input).
464
+ */
465
+ export declare class DocumentWriter {
466
+ constructor(quill: Quill, doc: Document);
467
+ /** The bound document — the instance passed in, mutated in place. */
468
+ readonly document: Document;
469
+ /**
470
+ * Typed-commit one main-card field (strict coerce, mismatch throws now).
471
+ * Throws `UnknownField` for a name the schema does not declare.
472
+ */
473
+ set(name: string, value: unknown): void;
474
+ /**
475
+ * Typed-commit several main-card fields atomically — nothing is applied on
476
+ * error (throws a {@link QuillmarkError} carrying one diagnostic per
477
+ * offending field, including an `UnknownField` for each undeclared name).
478
+ */
479
+ setAll(fields: Record<string, unknown>): void;
480
+ /**
481
+ * Set the main body from markdown (edit semantics: anchors rebase), discarding
482
+ * the delta — the receipt-free body write. Use `doc.revise({}, md)` for the
483
+ * `Delta` receipt.
484
+ */
485
+ setBody(markdown: string): void;
486
+ /**
487
+ * Build a composable card of `kind`, typed-commit `fields` onto it, set its
488
+ * body from optional markdown, and append it — the fused `makeCard` + typed
489
+ * commit + `pushCard`. Transactional: a rejected field (throws a per-field
490
+ * diagnostic bundle) or an invalid kind/body leaves the document untouched.
491
+ */
492
+ addCard(kind: string, fields?: Record<string, unknown>, body?: string): void;
493
+ /** Remove the composable card at `index`, returning it (or `undefined`). */
494
+ removeCard(index: number): Card | undefined;
495
+ /**
496
+ * A {@link CardWriter} for the composable card at `index`. Index validity is
497
+ * checked lazily at commit time, so this never throws. The cursor is
498
+ * ephemeral — a `removeCard`/`addCard` between binding and writing silently
499
+ * retargets it; for durable addressing stamp `$id` and re-resolve at write.
500
+ */
501
+ card(index: number): CardWriter;
502
+ }
503
+
504
+ /**
505
+ * A composable card bound to its `Quill` for typed writes, from
506
+ * {@link DocumentWriter.card}. Same verbs as {@link DocumentWriter}, targeting
507
+ * the card at its bound index; each write throws `IndexOutOfRange` if that index
508
+ * is out of range.
509
+ */
510
+ export declare class CardWriter {
511
+ constructor(quill: Quill, doc: Document, index: number);
512
+ /** The bound card index. */
513
+ readonly index: number;
514
+ set(name: string, value: unknown): void;
515
+ setAll(fields: Record<string, unknown>): void;
516
+ /** Set this card's body from markdown (edit semantics), discarding the delta. */
517
+ setBody(markdown: string): void;
518
+ }