@quillmark/wasm 0.111.0 → 0.113.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.
@@ -1,34 +1,13 @@
1
1
  /* @ts-self-types="./runtime.d.ts" */
2
2
  //
3
- // @quillmark/wasm/runtime: the canonical consumer API.
3
+ // The canonical consumer API, and the package's sole export. Its contract is
4
+ // `runtime.d.ts`, which a consumer's TypeScript reads through the package's
5
+ // `exports.types` map; the multi-binary design it implements is
6
+ // `prose/canon/BINDINGS.md`.
4
7
  //
5
- // The package ships multiple WASM binaries with SEPARATE linear memories: a
6
- // Typst-less `core` build (small, eager) that is the canonical home of
7
- // `Quill`/`Document`, and one private binary per backend that carries an engine.
8
- // A handle from one memory cannot be used by another; this module hides that
9
- // seam behind `await init()` and a static `Engine`.
10
- //
11
- // - `Quill` and `Document` ARE the core build's classes, handed out by the
12
- // gate below, never subclasses or wrappers: that identity is what makes
13
- // `instanceof` the whole membership test, so a handle either belongs to this
14
- // copy or to another copy, and the second is always a consumer bug.
15
- // `runtime.test.js` guards it (`Quill === CoreQuill`). No backend is loaded
16
- // to use them, so the editor path never pays for a multi-MB binary.
17
- //
18
- // - `Engine` is the render dispatcher. It routes on `quill.backendId`, lazily
19
- // imports that backend's build, clones the canonical `Quill`/`Document` into
20
- // the backend's memory as data (`toTree`→`fromTree`, `toJson`→`fromJson`),
21
- // renders, and never lets the backend handles escape.
22
- //
23
- // CLONE LIFETIMES: the per-call `Document` clone is transient, since
24
- // documents are small and mutate freely. The `Quill` clone is CACHED,
25
- // because re-cloning re-serializes its whole file tree, copies it into
26
- // backend memory, and re-parses + re-validates the bundle every call. Each
27
- // `Engine` memoizes it in a `WeakMap` keyed on the canonical `Quill`, so
28
- // dropping the core quill makes the entry collectable and `--weak-refs`
29
- // frees the backend handle. The contract this buys: a `Quill` instance's
30
- // contents never change after construction — mutate by replacing the
31
- // instance.
8
+ // A `Quill` instance's contents never change after construction mutate by
9
+ // replacing the instance which is what makes the cached backend clone below
10
+ // safe to hand out for the instance's whole life.
32
11
 
33
12
  // Local bindings, so this module can augment them: `quill.writer(doc)` is
34
13
  // patched onto the prototype below, and `instanceof` reads them directly. The
@@ -43,32 +22,11 @@ import { toModuleSource } from '#quillmark-env';
43
22
  import { importMarkdown, exportMarkdown, rebase, mapPos, mapMarks } from '../core/wasm.js';
44
23
  import { parseDocPath, formatDocPath } from '../core/wasm.js';
45
24
 
46
- // ── Initialization ──────────────────────────────────────────────────────────
47
- // The builds are `--target web`: they export their classes synchronously but
48
- // carry no wasm instance until something instantiates them. This module owns
49
- // that for core, behind one awaited gate; `Engine` owns it for the backends,
50
- // inside their lazy load.
51
- //
52
- // THE GATE IS THE ONLY DOOR: `init` resolves to the core surface and nothing
53
- // here exports it statically, so a handle is unobtainable without having
54
- // awaited, and package.json's `exports` map carries exactly one entry. The
55
- // guarded surface cannot be async (`Quill.fromTree` and `quill.seedDocument`
56
- // return synchronously), so there is nowhere to hide an await except in front.
57
- //
58
- // WHAT STAYS A STATIC EXPORT is what needs no instance: `MAIN_CARD_ADDR`, the
59
- // open-set guards, and `isQuillmarkError` are pure JS over plain objects.
60
- // `Engine`, `LiveSession` and the four writer/reader classes stay static too,
61
- // gated by their ARGUMENTS instead — every verb takes a `Quill` or both
62
- // handles, so a caller who has not awaited cannot produce an argument, and the
63
- // two constructors taking no handle reach no wasm. None carries a static
64
- // method, the one member shape an argument cannot gate. `gate.test.js` drives
65
- // the whole static surface before `init`. Holding them out of the gate also
66
- // keeps them tree-shakable.
67
- //
68
- // FAILURE DELIVERY follows the FUNCTION kind, not the failure kind: a sync verb
69
- // throws, a promise-returning verb rejects, and nothing does both. `init` is the
70
- // one promise-returning export not declared `async`, because the memo is
71
- // returned by identity; its conflict guard rejects explicitly to hold the rule.
25
+ // A `--target web` build exports its classes synchronously but carries no wasm
26
+ // instance until something instantiates it. This module owns that for core,
27
+ // behind one awaited gate; `Engine` owns it for the backends, inside their lazy
28
+ // load. Which values that gate holds and which stay static exports:
29
+ // `prose/canon/BINDINGS.md`.
72
30
 
73
31
  /**
74
32
  * The gated surface: the core build's values, which are exactly the ones its
@@ -104,38 +62,8 @@ let coreInit;
104
62
  let coreInitSource;
105
63
 
106
64
  /**
107
- * Instantiate the core WASM build and resolve to its surface.
108
- *
109
- * ```js
110
- * import { init } from '@quillmark/wasm';
111
- * const { Quill, Document } = await init();
112
- * ```
113
- *
114
- * The classes and the free functions come from here and nowhere else, so the
115
- * pre-init mistake is not expressible. Destructure at each entry point (route
116
- * loader, hydration path, worker) rather than threading one result around: the
117
- * gate is memoized, so every await after the first is free.
118
- *
119
- * Identical in every environment: in a browser the binary is fetched and
120
- * streamed, under Node it is read off disk, and the call site is the same line.
121
- *
122
- * Idempotent and concurrency-safe: every non-conflicting call returns the same
123
- * promise, so several entry points cost one instantiation. A failed init clears
124
- * the memo, so a retry is possible.
125
- *
126
- * Both failures reject (§ "Initialization", FAILURE DELIVERY): one `catch`
127
- * around `await init(...)` covers `runtime::init_conflict` and
128
- * `runtime::init_failed` alike.
129
- *
130
- * @param {import('../core/wasm.js').InitInput} [source] override the binary's
131
- * source (bytes, a `Response`, a `WebAssembly.Module`, a URL) for hosts that
132
- * route assets themselves or embed the binary. Pass it on the FIRST call; a
133
- * later call passing a *different* source rejects with
134
- * `runtime::init_conflict` rather than silently ignoring it. Passing the same
135
- * value again is fine, so several entry points may each `await init(BYTES)`
136
- * against one constant.
137
- * @returns {Promise<import('./runtime.js').CoreSurface>} the core surface, once
138
- * its instance is live
65
+ * @param {import('../core/wasm.js').InitInput} [source]
66
+ * @returns {Promise<import('./runtime.js').CoreSurface>}
139
67
  */
140
68
  export function init(source) {
141
69
  if (coreInit) {
@@ -195,46 +123,17 @@ async function instantiateCore(source) {
195
123
  }
196
124
  }
197
125
 
198
- // ── The main-card address ───────────────────────────────────────────────────
199
126
  /**
200
- * The main card's address: the default target of the card-scoped verbs
201
- * (`storeFields` / `storeExt` / `commitFields` / …). A named, `CardAddr`-typed
202
- * alias for the empty address `{}`, so a main-card write names its target:
203
- * `doc.storeFields(MAIN_CARD_ADDR, fields)`. It IS `{}` (frozen), a pure alias:
204
- * `{}` and `undefined` stay equally valid. Card axis only: a card selector,
205
- * never a field address.
206
127
  * @type {import('../core/wasm.js').CardAddr}
207
128
  */
208
129
  export const MAIN_CARD_ADDR = Object.freeze({});
209
130
 
210
- // ── The variant discriminant key ────────────────────────────────────────────
211
131
  /**
212
- * The key carrying the discriminant inside a variant-bearing enum's value.
213
- *
214
- * A field declaring `variants:` rests as a container, `{value: <member>, …that
215
- * member's fields}`, so reading or writing one means naming this key:
216
- * `doc.storeFields(MAIN_CARD_ADDR, { classification: { [VARIANT_DISCRIMINANT_KEY]: 'CUI' } })`.
217
- * It crosses the boundary inside untyped container data, with no type to read
218
- * it off.
219
- *
220
- * Reserved: no variant may declare a field under it
221
- * (`quill::variant_reserved_field_name`), and `QuillFieldSchema.variants`,
222
- * keyed by member, never contains it.
223
132
  * @type {'value'}
224
133
  */
225
134
  export const VARIANT_DISCRIMINANT_KEY = 'value';
226
135
 
227
136
  /**
228
- * Narrow an unknown caught value to a `QuillmarkError`, the error every
229
- * fallible method in this package throws: a real `Error` with a non-empty
230
- * `diagnostics` array attached (same entry shape as `RenderResult.warnings`).
231
- *
232
- * Structural by necessity AND by design: the WASM layer constructs a plain
233
- * `Error` and attaches the property (there is no error class to `instanceof`),
234
- * and a structural check narrows errors from any build or WASM instance in the
235
- * page. The deliberate exception to § "Handles from another copy": an error is
236
- * data, not a handle, so nothing is gained by refusing one that crossed.
237
- *
238
137
  * @param {unknown} e
239
138
  * @returns {e is Error & { diagnostics: import('../core/wasm.js').Diagnostic[] }}
240
139
  */
@@ -258,62 +157,35 @@ function quillmarkError(code, message, hint) {
258
157
  return err;
259
158
  }
260
159
 
261
- // ── Handles from another copy: always a bug ─────────────────────────────────
262
- // A duplicate install is two `core` builds: two linear memories and two distinct
263
- // `Quill`/`Document` classes. No topology legitimately loads a multi-megabyte
264
- // WASM package twice AND needs handles to cross between the copies, so a
265
- // crossing is a consumer bug, and every seam taking a core handle says so.
266
- //
267
- // Crossing read-only handles as data is mechanically possible (`toJson` and
268
- // `toTree` serialize either way) and is not done: it leaves a package where some
269
- // verbs work and some throw, and it hides a cliff, since a crossed read is a
270
- // whole-document `toJson` + `fromJson` and a form reading fifty fields pays
271
- // fifty round trips.
272
- //
273
- // What the checks deliver is the ERROR, not the rejection. wasm-bindgen's glue
274
- // already rejects a foreign class wherever a method declares a reference
275
- // parameter, but its `_assertClass` throws a bare `Error` reading
276
- // `expected instance of Document` at a value that IS a `Document`, so
277
- // `isQuillmarkError` returns false and the failure leaves this package's error
278
- // contract. The checks front-run it with a `QuillmarkError` naming both cause
279
- // and cure, and cover the seams with no `_assertClass` to front-run: `Engine`
280
- // and `LiveSession.update` cross into backend memory as data, where a foreign
281
- // handle would silently work at the price of that round trip and a per-copy
282
- // split of the quill clone cache.
160
+ // These checks deliver the ERROR, not the rejection, at the seams that cross
161
+ // into backend memory as DATA `Engine` and `LiveSession.update` where a
162
+ // foreign handle would otherwise work silently at the price of a whole-document
163
+ // round trip and a per-copy split of the quill clone cache, and at the four
164
+ // writer/reader binds, whose constructors hold both handles. A method declaring
165
+ // a reference parameter needs none of this: wasm-bindgen's own `_assertClass`
166
+ // refuses a foreign class there.
283
167
 
284
- /** Per class: the code and hint for "not one at all", and the from-another-copy probe. */
168
+ /** Per class: the diagnostic for a value that is not one of THIS copy's handles. */
285
169
  const HANDLE_KINDS = {
286
170
  Quill: {
287
171
  code: 'runtime::not_a_quill',
288
- probe: 'toTree',
289
- hint: 'Pass a Quill built by Quill.fromTree.'
172
+ hint: 'Pass a Quill built by Quill.fromTree. A Quill from another copy of @quillmark/wasm is refused too — each copy is its own WASM linear memory and its own class — so run `npm ls @quillmark/wasm` and dedupe to one.'
290
173
  },
291
174
  Document: {
292
175
  code: 'runtime::not_a_document',
293
- probe: 'toJson',
294
- hint: 'Pass a Document built by Document.fromMarkdown / fromJson or quill.seedDocument.'
176
+ hint: 'Pass a Document built by Document.fromMarkdown / fromStored or quill.seedDocument. A Document from another copy of @quillmark/wasm is refused too — each copy is its own WASM linear memory and its own class — so run `npm ls @quillmark/wasm` and dedupe to one.'
295
177
  }
296
178
  };
297
179
 
298
180
  /**
299
- * The rejection for a value that is not one of this copy's handles. Two cures,
300
- * so two diagnostics: a value carrying the class's serializer is that class
301
- * from ANOTHER copy (dedupe the install), anything else is the wrong argument
302
- * (fix the call).
181
+ * The rejection for a value that is not one of this copy's handles.
303
182
  * @param {unknown} value
304
183
  * @param {string} method
305
184
  * @param {'Quill' | 'Document'} className
306
185
  * @returns {Error & { diagnostics: import('../core/wasm.js').Diagnostic[] }}
307
186
  */
308
187
  function notLocal(value, method, className) {
309
- const { code, probe, hint } = HANDLE_KINDS[className];
310
- if (value && typeof (/** @type {any} */ (value)[probe]) === 'function') {
311
- return quillmarkError(
312
- 'runtime::foreign_handle',
313
- `${method}: the ${className} belongs to a different copy of @quillmark/wasm. Handles never cross between copies: each copy is its own WASM linear memory and its own ${className} class.`,
314
- 'Two copies of @quillmark/wasm are installed. Run `npm ls @quillmark/wasm` and dedupe to one.'
315
- );
316
- }
188
+ const { code, hint } = HANDLE_KINDS[className];
317
189
  return quillmarkError(
318
190
  code,
319
191
  `${method}: expected a ${className}, got ${value === null ? 'null' : typeof value}.`,
@@ -343,213 +215,27 @@ function requireLocalQuill(quill, method) {
343
215
  throw notLocal(quill, method, 'Quill');
344
216
  }
345
217
 
346
- // Marker for the patches below. `Symbol.for`, not a module-local `Symbol()`:
347
- // re-evaluating THIS module (Vite HMR, a Vitest worker sharing a module graph)
348
- // against an already-patched (because cached) core build must see the existing
349
- // marker, or each pass wraps the previous wrapper.
350
- const HANDLE_CHECKED = Symbol.for('@quillmark/wasm:handle-checked');
351
-
352
- /**
353
- * Replace `proto[name]` with `wrap(original)`, once.
354
- * @param {object} proto
355
- * @param {string} name
356
- * @param {(original: Function) => Function} wrap
357
- */
358
- function patchHandleChecked(proto, name, wrap) {
359
- const original = /** @type {any} */ (proto)[name];
360
- if (typeof original !== 'function' || original[HANDLE_CHECKED]) return;
361
- const patched = wrap(original);
362
- /** @type {any} */ (patched)[HANDLE_CHECKED] = true;
363
- // Keep the method's name so a stack trace still reads `Quill.validate`.
364
- Object.defineProperty(patched, 'name', { value: name, configurable: true });
365
- /** @type {any} */ (proto)[name] = patched;
366
- }
367
-
368
- // The core methods declaring a `&Document` parameter. Each already refuses a
369
- // foreign handle inside `_assertClass`; the patch makes the refusal legible.
370
- patchHandleChecked(Document.prototype, 'equals', (original) =>
371
- function equals(/** @type {any} */ other) {
372
- requireLocalDoc(other, 'Document.equals');
373
- return original.call(this, other);
374
- }
375
- );
376
- for (const name of /** @type {const} */ (['validate', 'resolve', 'conform'])) {
377
- // Named once per patch, not per call: `validate` runs per keystroke.
378
- const method = `Quill.${name}`;
379
- patchHandleChecked(Quill.prototype, name, (original) =>
380
- function (/** @type {any} */ doc) {
381
- requireLocalDoc(doc, method);
382
- return original.call(this, doc);
383
- }
384
- );
385
- }
386
-
387
- // The typed writer/reader primitives (`Document._commitField` and friends) take
388
- // the QUILL by reference, so they hit the same `_assertClass` from the other
389
- // direction. Checked at the four writer/reader classes below, not patched onto
390
- // `Document`: a foreign document carries its OWN prototype, so patching this
391
- // copy's would never run.
392
-
393
- // ── Open-set discriminant guards ────────────────────────────────────────────
394
- // `ContentIsland.type`, `ContentMark.type`, `ContentLine.kind`, and
395
- // `ContentContainer.container` are OPEN sets: each union carries a residual
396
- // `{ …: string; … }` arm, so a bare `x.type === 'table'` check never narrows the
397
- // payload, TS keeps the residual arm live (a `string` can be `'table'`),
398
- // leaving `props` / the mark payload / `level` opaque at every consumer. These
399
- // are the checked narrowing path: on the true branch the payload's pinned shape
400
- // is asserted. Only the payload-carrying arms get a guard: an island always
401
- // carries `props`, a `link` mark carries `url`, an `anchor` mark carries `id`, a
402
- // `heading` line carries `level` and a `code` line `lang`, a `list_item`
403
- // container its shape; the payload-free arms (`strong`/`emph`/`underline`/
404
- // `strike`/`code` marks, `para`/`island`/`rule` lines, `quote`) narrow to
405
- // nothing. An unrecognized discriminant fails every guard and keeps its opaque
406
- // `attrs`/`props`.
407
-
408
- /**
409
- * @param {import('../core/wasm.js').ContentIsland} island
410
- * @returns {island is import('../core/wasm.js').ContentIsland & { type: 'table'; props: import('../core/wasm.js').TableProps }}
411
- */
412
- export function isTableIsland(island) {
413
- return island.type === 'table';
414
- }
415
-
416
- /**
417
- * @param {import('../core/wasm.js').ContentIsland} island
418
- * @returns {island is import('../core/wasm.js').ContentIsland & { type: 'image'; props: import('../core/wasm.js').ImageProps }}
419
- */
420
- export function isImageIsland(island) {
421
- return island.type === 'image';
422
- }
423
-
424
- /**
425
- * @param {import('../core/wasm.js').ContentMark} mark
426
- * @returns {mark is import('../core/wasm.js').ContentMark & { type: 'link'; url: string }}
427
- */
428
- export function isLinkMark(mark) {
429
- return mark.type === 'link';
430
- }
431
-
432
- /**
433
- * @param {import('../core/wasm.js').ContentMark} mark
434
- * @returns {mark is import('../core/wasm.js').ContentMark & { type: 'anchor'; id: string }}
435
- */
436
- export function isAnchorMark(mark) {
437
- return mark.type === 'anchor';
438
- }
439
-
440
- /**
441
- * @param {import('../core/wasm.js').ContentLine} line
442
- * @returns {line is import('../core/wasm.js').ContentLine & { kind: 'heading'; level: number }}
443
- */
444
- export function isHeadingLine(line) {
445
- return line.kind === 'heading';
446
- }
447
-
448
- /**
449
- * @param {import('../core/wasm.js').ContentLine} line
450
- * @returns {line is import('../core/wasm.js').ContentLine & { kind: 'code'; lang?: string }}
451
- */
452
- export function isCodeLine(line) {
453
- return line.kind === 'code';
454
- }
455
-
456
- /**
457
- * @param {import('../core/wasm.js').ContentContainer} container
458
- * @returns {container is import('../core/wasm.js').ContentContainer & { container: 'list_item'; ordered: boolean; start: number; ordinal: number; instance?: number }}
459
- */
460
- export function isListItemContainer(container) {
461
- return container.container === 'list_item';
462
- }
463
-
464
- // ── Open-set membership guards ──────────────────────────────────────────────
465
- // The guards above each answer "is this arm X". These four answer "is this a
466
- // value this build knows?", the question any read-modify-write consumer must
467
- // ask, since lowering an edit restates every line's kind and containers. A
468
- // predicate rather than an exported name list, because the tables below are
469
- // upstream's business: they are pinned against the Rust source by
470
- // `tests/known_names_drift.rs`, so adding a built-in means editing there, here,
471
- // and the TS unions in `src/engine.rs` in one commit.
472
- //
473
- // These classify unknown *tags*, not unknown *payloads on known tags*. A future
474
- // `kind: "footnote"` with a sibling `ref` loses `ref` at a consumer that predates
475
- // it either way.
476
-
477
- const KNOWN_LINE_KINDS = new Set(['para', 'heading', 'code', 'island', 'rule']);
478
- const KNOWN_CONTAINERS = new Set(['list_item', 'quote']);
479
- const KNOWN_MARK_TYPES = new Set(['strong', 'emph', 'underline', 'strike', 'code', 'link', 'anchor']);
480
- const KNOWN_ISLAND_TYPES = new Set(['table', 'image']);
481
-
482
- /**
483
- * @param {import('../core/wasm.js').ContentLine} line
484
- * @returns {line is import('../core/wasm.js').ContentLine & { kind: string; attrs: unknown }}
485
- */
486
- export function isUnknownLine(line) {
487
- return typeof line?.kind === 'string' && !KNOWN_LINE_KINDS.has(line.kind);
488
- }
489
-
490
- /**
491
- * @param {import('../core/wasm.js').ContentContainer} container
492
- * @returns {container is import('../core/wasm.js').ContentContainer & { container: string; attrs: unknown }}
493
- */
494
- export function isUnknownContainer(container) {
495
- return typeof container?.container === 'string' && !KNOWN_CONTAINERS.has(container.container);
496
- }
497
-
498
- /**
499
- * @param {import('../core/wasm.js').ContentMark} mark
500
- * @returns {mark is import('../core/wasm.js').ContentMark & { type: string; attrs: unknown }}
501
- */
502
- export function isUnknownMark(mark) {
503
- return typeof mark?.type === 'string' && !KNOWN_MARK_TYPES.has(mark.type);
504
- }
505
-
506
- /**
507
- * @param {import('../core/wasm.js').ContentIsland} island
508
- * @returns {island is import('../core/wasm.js').ContentIsland & { type: string; props: unknown }}
509
- */
510
- export function isUnknownIsland(island) {
511
- return typeof island?.type === 'string' && !KNOWN_ISLAND_TYPES.has(island.type);
512
- }
513
-
514
- // ── Container run boundaries ────────────────────────────────────────────────
515
- // `ContentContainer.instance` is what keeps two adjacent runs of one shape
516
- // apart, and only a writer knows where a boundary is: the flat `containers`
517
- // form cannot tell a list ending beside another from one list of two items, so
518
- // an omitted discriminator welds them and nothing reports it.
519
- //
520
- // WELD_KEYS is the rule `Container::same_weld` owns upstream: which fields two
521
- // adjacent runs must share for the markdown projection to read them as one, and
522
- // therefore for the canonical form to have to spend a discriminator. `start` is
523
- // not among them, since CommonMark reads only a list's first number. A table
218
+ // WELD_KEYS is the rule `Container::same_weld` owns upstream: which `attrs`
219
+ // entries two adjacent runs must share for the markdown projection to read them
220
+ // as one. `start` is not among them, since CommonMark reads only a list's first
221
+ // number — a subset, which is why every container needs its own entry. A table
524
222
  // rather than a switch, so `tests/known_names_drift.rs` can pin it against the
525
223
  // Rust predicate.
526
224
 
527
225
  const WELD_KEYS = { list_item: ['ordered'], quote: [] };
528
226
 
529
- function sameJson(a, b) {
530
- if (a === b) return true;
531
- if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;
532
- if (Array.isArray(a) !== Array.isArray(b)) return false;
533
- const ka = Object.keys(a);
534
- return (
535
- ka.length === Object.keys(b).length &&
536
- ka.every((k) => Object.hasOwn(b, k) && sameJson(a[k], b[k]))
537
- );
538
- }
539
-
540
227
  /**
541
228
  * @param {import('../core/wasm.js').ContentContainer} a
542
229
  * @param {import('../core/wasm.js').ContentContainer} b
543
230
  * @returns {boolean}
544
231
  */
545
232
  function weldsWith(a, b) {
546
- // A malformed value welds with nothing. The membership guards' posture:
547
- // answer rather than throw.
233
+ // A malformed value welds with nothing: answer rather than throw.
548
234
  if (typeof a?.container !== 'string' || a.container !== b?.container) return false;
549
- // `hasOwn`, so a tag colliding with an `Object.prototype` member reaches the
550
- // unknown branch rather than a function.
551
- if (!Object.hasOwn(WELD_KEYS, a.container)) return sameJson(a.attrs, b.attrs);
552
- return WELD_KEYS[a.container].every((k) => a[k] === b[k]);
235
+ // `hasOwn`, so a tag colliding with an `Object.prototype` member answers
236
+ // `false` rather than reaching a function.
237
+ if (!Object.hasOwn(WELD_KEYS, a.container)) return false;
238
+ return WELD_KEYS[a.container].every((k) => a.attrs?.[k] === b.attrs?.[k]);
553
239
  }
554
240
 
555
241
  /**
@@ -578,7 +264,7 @@ export function assignInstances(runs) {
578
264
  * generated entry's own `wasm !== undefined` guard only catches a call arriving
579
265
  * after one finished, not one already in flight.
580
266
  *
581
- * @param {string} id backend id, for the failure message
267
+ * @param {string} id the build's name, for the failure message
582
268
  * @param {() => Promise<any>} importThunk the dynamic `import()`
583
269
  * @param {() => URL} wasmUrl the build's binary, resolved at call time
584
270
  * @returns {() => Promise<any>} resolves to a ready-to-use module
@@ -597,108 +283,89 @@ function backendLoad(id, importThunk, wasmUrl) {
597
283
  throw Object.assign(
598
284
  quillmarkError(
599
285
  'runtime::backend_load_failed',
600
- `Engine: could not load the '${id}' backend: ${
286
+ `Engine: could not load the '${id}' build: ${
601
287
  /** @type {any} */ (cause)?.message ?? cause
602
288
  }`,
603
- 'The backend binary ships beside the package files; check the network tab for a 404 or an HTML response.'
289
+ 'The build ships beside the package files; check the network tab for a 404 or an HTML response.'
604
290
  ),
605
291
  { cause }
606
292
  );
607
293
  }));
608
294
  }
609
295
 
610
- // Backend builds are NEVER statically imported here: that would pull a multi-MB
611
- // binary into the eager graph and defeat lazy loading. Each entry is a
612
- // DESCRIPTOR: `load` dynamically imports and instantiates a backend's chunk, so
613
- // the binary is fetched only when something renders against it; `formats` and
614
- // `canvas` are the required static capability manifest, so the probes
615
- // (`supportedFormats` / `supportsCanvas`) answer without loading the binary or
616
- // cloning the quill. The manifest mirrors each backend's Rust `SUPPORTED_FORMATS`
617
- // (and `formats_support_canvas`: true iff the list includes `svg` or `png`),
618
- // pinned by a `runtime.test.js` drift guard that renders once and compares.
296
+ // The render build is NEVER statically imported here: that would pull a
297
+ // multi-MB binary into the eager graph and defeat lazy loading. Both built-in
298
+ // backends ship in it, so they share one `load`; each `formats` manifest
299
+ // mirrors that backend's Rust `SUPPORTED_FORMATS`, pinned by a
300
+ // `runtime.test.js` drift guard that renders once and compares.
301
+ const RENDER_BUILD = backendLoad(
302
+ 'render',
303
+ () => import('../render/wasm.js'),
304
+ () => new URL('../render/wasm_bg.wasm', import.meta.url)
305
+ );
619
306
  const DEFAULT_BACKENDS = {
620
307
  typst: {
621
- load: backendLoad(
622
- 'typst',
623
- () => import('../backends/typst/wasm.js'),
624
- () => new URL('../backends/typst/wasm_bg.wasm', import.meta.url)
625
- ),
626
- formats: ['pdf', 'svg', 'png'], // crates/backends/typst/src/lib.rs SUPPORTED_FORMATS
627
- canvas: true // has svg/png → formats_support_canvas == true
308
+ load: RENDER_BUILD,
309
+ formats: ['pdf', 'svg', 'png'] // crates/backends/typst/src/lib.rs SUPPORTED_FORMATS
628
310
  },
629
- pdfform: {
630
- load: backendLoad(
631
- 'pdfform',
632
- () => import('../backends/pdfform/wasm.js'),
633
- () => new URL('../backends/pdfform/wasm_bg.wasm', import.meta.url)
634
- ),
635
- // crates/backends/pdfform/src/lib.rs SUPPORTED_FORMATS == [Pdf, Svg, Png]
636
- formats: ['pdf', 'svg', 'png'],
637
- canvas: true // has svg/png → formats_support_canvas == true
311
+ acroform: {
312
+ load: RENDER_BUILD,
313
+ formats: ['pdf'] // crates/backends/acroform/src/lib.rs SUPPORTED_FORMATS
638
314
  }
639
315
  };
640
316
 
641
317
  /**
642
318
  * Validate a backend registry descriptor, naming the backend id on any
643
319
  * malformed entry. Failing at construction rather than deep inside a render is
644
- * what lets the capability probes answer from the manifest unconditionally.
320
+ * what lets `supportedFormats` answer from the manifest unconditionally.
645
321
  * @param {string} id
646
322
  * @param {unknown} entry
647
- * @returns {{ load: () => Promise<unknown>, formats: string[], canvas: boolean }}
323
+ * @returns {{ load: () => Promise<unknown>, formats: string[] }}
648
324
  */
649
325
  function validateBackend(id, entry) {
650
326
  if (!entry || typeof entry !== 'object') {
651
- throw new Error(
652
- `Engine: backend '${id}' must be a descriptor { load, formats, canvas }.`
653
- );
327
+ throw new Error(`Engine: backend '${id}' must be a descriptor { load, formats }.`);
654
328
  }
655
- const { load, formats, canvas } = /** @type {any} */ (entry);
329
+ const { load, formats } = /** @type {any} */ (entry);
656
330
  if (typeof load !== 'function') {
657
331
  throw new Error(`Engine: backend '${id}' descriptor needs a callable 'load'.`);
658
332
  }
659
333
  if (!Array.isArray(formats)) {
660
334
  throw new Error(`Engine: backend '${id}' descriptor needs a 'formats' array.`);
661
335
  }
662
- if (typeof canvas !== 'boolean') {
663
- throw new Error(`Engine: backend '${id}' descriptor needs a boolean 'canvas'.`);
664
- }
665
- return { load, formats, canvas };
336
+ return { load, formats };
666
337
  }
667
338
 
668
- /**
669
- * Render dispatcher over the canonical `Quill`/`Document`. One `Engine`
670
- * instance can drive every backend; it resolves the right backend build from
671
- * each quill's declared `backendId` and loads it lazily on first use.
672
- */
673
339
  export class Engine {
674
- /** backendId → Promise<backend module>, memoized so each build loads once. */
340
+ /**
341
+ * The three caches below key on a descriptor's `load` thunk, the build's
342
+ * identity: two backend ids sharing one build (the built-ins) share one
343
+ * module, one engine and one clone cache, and a consumer's override build
344
+ * gets its own.
345
+ */
346
+ /** load → Promise<build module>, memoized so each build loads once. */
675
347
  #modules = new Map();
676
- /** backendId → that backend's engine instance (the WASM backend registry). */
348
+ /** load → that build's engine instance (the WASM backend registry). */
677
349
  #engines = new Map();
678
- /** backendId → descriptor `{ load, formats, canvas }`. */
350
+ /** backendId → descriptor `{ load, formats }`. */
679
351
  #loaders;
680
352
  /**
681
- * backendId → WeakMap<canonical Quill, backend-memory clone>, caching the
353
+ * load → WeakMap<canonical Quill, build-memory clone>, caching the
682
354
  * expensive materialization. WeakMap so dropping the canonical quill makes
683
355
  * its clone collectable, and wasm-bindgen weak-refs then free the handle.
684
- * @type {Map<string, WeakMap<object, any>>}
356
+ * @type {Map<() => Promise<unknown>, WeakMap<object, any>>}
685
357
  */
686
358
  #quillClones = new Map();
687
359
 
688
360
  /**
689
- * @param {{ backends?: Record<string, { load: () => Promise<unknown>, formats: string[], canvas: boolean }> }} [options]
690
- * Extra or overriding backend descriptors, merged over the built-ins. Each
691
- * is `{ load, formats, canvas }` with the manifest REQUIRED, since that is
692
- * what makes `supportedFormats` / `supportsCanvas` free; malformed entries
693
- * throw here, at construction.
694
- *
695
- * `load` resolves to a READY module: a registrant shipping its own
696
- * `--target web` build instantiates inside the thunk. More than one
697
- * `Engine` may call it, so memoize (the built-ins do, at module scope).
361
+ * `load` must resolve to a READY module: a registrant shipping its own
362
+ * `--target web` build instantiates inside the thunk, and more than one
363
+ * `Engine` may call it, so memoize (the built-ins do, at module scope).
364
+ * @param {{ backends?: Record<string, { load: () => Promise<unknown>, formats: string[] }> }} [options]
698
365
  */
699
366
  constructor(options) {
700
367
  const merged = { ...DEFAULT_BACKENDS, ...(options?.backends ?? {}) };
701
- /** @type {Record<string, { load: () => Promise<unknown>, formats: string[], canvas: boolean }>} */
368
+ /** @type {Record<string, { load: () => Promise<unknown>, formats: string[] }>} */
702
369
  const loaders = {};
703
370
  for (const [id, entry] of Object.entries(merged)) {
704
371
  loaders[id] = validateBackend(id, entry);
@@ -707,17 +374,19 @@ export class Engine {
707
374
  }
708
375
 
709
376
  /**
710
- * The registered descriptor for `backendId`, or the "no backend registered"
711
- * throw. Touches no binary.
377
+ * The registered descriptor for `backendId`, or the
378
+ * `engine::backend_not_found` throw core raises for the same condition.
379
+ * Touches no binary.
712
380
  * @param {string} backendId
713
- * @returns {{ load: () => Promise<unknown>, formats: string[], canvas: boolean }}
381
+ * @returns {{ load: () => Promise<unknown>, formats: string[] }}
714
382
  */
715
383
  #descriptorFor(backendId) {
716
384
  const descriptor = this.#loaders[backendId];
717
385
  if (!descriptor) {
718
- throw new Error(
719
- `Engine: no backend registered for '${backendId}'. ` +
720
- `Known backends: ${Object.keys(this.#loaders).join(', ') || '(none)'}.`
386
+ throw quillmarkError(
387
+ 'engine::backend_not_found',
388
+ `Engine: backend '${backendId}' not registered or not enabled.`,
389
+ `Available backends: ${Object.keys(this.#loaders).join(', ') || '(none)'}`
721
390
  );
722
391
  }
723
392
  return descriptor;
@@ -737,51 +406,52 @@ export class Engine {
737
406
  }
738
407
 
739
408
  /**
740
- * Resolve (and lazily load) the backend module + its engine for `backendId`.
409
+ * Resolve (and lazily load) the build module + its engine for `backendId`.
741
410
  * @param {string} backendId
742
411
  * @returns {Promise<{ mod: any, engine: any }>}
743
412
  */
744
413
  async #resolveBackend(backendId) {
745
- const descriptor = this.#descriptorFor(backendId);
414
+ const { load } = this.#descriptorFor(backendId);
746
415
 
747
- let modPromise = this.#modules.get(backendId);
416
+ let modPromise = this.#modules.get(load);
748
417
  if (!modPromise) {
749
418
  // Set the promise synchronously (before any await) so concurrent first
750
419
  // renders share ONE import. Self-heal on failure so a transient load
751
420
  // error doesn't poison every later attempt.
752
421
  modPromise = Promise.resolve()
753
- .then(descriptor.load)
422
+ .then(load)
754
423
  .catch((err) => {
755
- this.#modules.delete(backendId);
424
+ this.#modules.delete(load);
756
425
  throw err;
757
426
  });
758
- this.#modules.set(backendId, modPromise);
427
+ this.#modules.set(load, modPromise);
759
428
  }
760
429
  const mod = await modPromise;
761
430
 
762
- let engine = this.#engines.get(backendId);
431
+ let engine = this.#engines.get(load);
763
432
  if (!engine) {
764
433
  engine = new mod.Quillmark();
765
- this.#engines.set(backendId, engine);
434
+ this.#engines.set(load, engine);
766
435
  }
767
436
  return { mod, engine };
768
437
  }
769
438
 
770
439
  /**
771
- * Get (or materialize-and-cache) the backend-memory `Quill` clone for `quill`
772
- * under `backendId`. On a miss the clone is built from `tree`, the caller's
773
- * pre-await snapshot, since the canonical handle may be freed by now.
774
- * @param {any} mod the backend build module
440
+ * Get (or materialize-and-cache) the build-memory `Quill` clone for `quill`
441
+ * under `backendId`'s build. On a miss the clone is built from `tree`, the
442
+ * caller's pre-await snapshot, since the canonical handle may be freed by now.
443
+ * @param {any} mod the build module
775
444
  * @param {string} backendId
776
445
  * @param {object} quill the canonical instance (cache key only)
777
446
  * @param {Map<string, Uint8Array> | null} tree pre-await snapshot; `null` on a cache hit
778
- * @returns {any} the backend-memory quill clone
447
+ * @returns {any} the build-memory quill clone
779
448
  */
780
449
  #cachedQuillClone(mod, backendId, quill, tree) {
781
- let perQuill = this.#quillClones.get(backendId);
450
+ const { load } = this.#descriptorFor(backendId);
451
+ let perQuill = this.#quillClones.get(load);
782
452
  if (!perQuill) {
783
453
  perQuill = new WeakMap();
784
- this.#quillClones.set(backendId, perQuill);
454
+ this.#quillClones.set(load, perQuill);
785
455
  }
786
456
  let backendQuill = perQuill.get(quill);
787
457
  if (!backendQuill) {
@@ -796,11 +466,15 @@ export class Engine {
796
466
  * memory and run `fn` against the backend engine. Only `render`/`open` call
797
467
  * this, so `doc` is always present.
798
468
  *
799
- * OWNERSHIP WINDOW: both caller handles are snapshotted (`doc.toJson()`, and
800
- * `quill.toTree()` on a clone-cache miss) BEFORE the first await. The backend
801
- * load below is a real suspension point, so reading the handles after it
802
- * would race a caller that `free()`s them as soon as this call returns its
803
- * promise ("null pointer passed to rust").
469
+ * OWNERSHIP WINDOW: both caller handles are snapshotted (`doc.toStored()` and
470
+ * `doc.warnings`, and `quill.toTree()` on a clone-cache miss) BEFORE the first
471
+ * await. The backend load below is a real suspension point, so reading the
472
+ * handles after it would race a caller that `free()`s them as soon as this
473
+ * call returns its promise ("null pointer passed to rust").
474
+ *
475
+ * `docWarnings` rides the context because the storage DTO does not carry
476
+ * them: `fromStored` clears the load's warnings, so the backend clone knows
477
+ * nothing of them and `render` splices the snapshot back in.
804
478
  *
805
479
  * Clone lifetimes differ by design: the `doc` clone is TRANSIENT, freed in
806
480
  * the `finally` of every call, while the `quill` clone is CACHED and is not
@@ -810,13 +484,16 @@ export class Engine {
810
484
  * @param {string} method the caller's name, for the rejection message
811
485
  * @param {Quill} quill
812
486
  * @param {Document} doc
813
- * @param {(ctx: { mod: any, engine: any, quill: any, doc: any }) => any} fn
487
+ * @param {(ctx: { mod: any, engine: any, quill: any, doc: any, docWarnings: any[] }) => any} fn
814
488
  */
815
489
  async #withClones(method, quill, doc, fn) {
816
490
  const backendId = this.#backendOf(quill, method);
817
491
  requireLocalDoc(doc, method);
818
- const docJson = doc.toJson();
819
- const quillTree = this.#quillClones.get(backendId)?.has(quill) ? null : quill.toTree();
492
+ const docJson = doc.toStored();
493
+ const docWarnings = doc.warnings;
494
+ const quillTree = this.#quillClones.get(this.#descriptorFor(backendId).load)?.has(quill)
495
+ ? null
496
+ : quill.toTree();
820
497
  const { mod, engine } = await this.#resolveBackend(backendId);
821
498
  // The doc clone and `fn` share one try so the clone is freed even if a
822
499
  // later step throws; the cached quill clone is intentionally not freed.
@@ -824,31 +501,33 @@ export class Engine {
824
501
  const backendQuill = this.#cachedQuillClone(mod, backendId, quill, quillTree);
825
502
  let backendDoc = null;
826
503
  try {
827
- backendDoc = mod.Document.fromJson(docJson);
828
- return fn({ mod, engine, quill: backendQuill, doc: backendDoc });
504
+ backendDoc = mod.Document.fromStored(docJson);
505
+ return fn({ mod, engine, quill: backendQuill, doc: backendDoc, docWarnings });
829
506
  } finally {
830
507
  backendDoc?.free();
831
508
  }
832
509
  }
833
510
 
834
511
  /**
835
- * Render `doc` against `quill` in one shot. Both handles are read
836
- * synchronously before the first await.
837
512
  * @param {Quill} quill
838
513
  * @param {Document} doc
839
- * @param {object} [options] render options (`{ format, ppi, pages, producer }`)
514
+ * @param {object} [options] render options (`{ format, ppi, pages, regions }`)
840
515
  * @returns {Promise<import('./runtime.js').RenderResult>}
841
516
  */
842
517
  async render(quill, doc, options) {
843
- return this.#withClones('engine.render(quill, doc)', quill, doc, ({ engine, quill: q, doc: d }) =>
844
- engine.render(q, d, options ?? undefined)
518
+ return this.#withClones(
519
+ 'engine.render(quill, doc)',
520
+ quill,
521
+ doc,
522
+ ({ engine, quill: q, doc: d, docWarnings }) => {
523
+ const result = engine.render(q, d, options ?? undefined);
524
+ result.warnings = docWarnings.concat(result.warnings);
525
+ return result;
526
+ }
845
527
  );
846
528
  }
847
529
 
848
530
  /**
849
- * Open a live render session. It retains what `update` needs, so the
850
- * transient clones are freed before this returns; the caller owns the session
851
- * and must `.free()` it.
852
531
  * @param {Quill} quill
853
532
  * @param {Document} doc
854
533
  * @returns {Promise<LiveSession>}
@@ -863,8 +542,6 @@ export class Engine {
863
542
  }
864
543
 
865
544
  /**
866
- * The output formats `quill`'s backend can emit: an always-free probe over
867
- * the descriptor's manifest. `async` for API stability; it awaits nothing.
868
545
  * @param {Quill} quill
869
546
  * @returns {Promise<import('./runtime.js').OutputFormat[]>}
870
547
  */
@@ -873,29 +550,17 @@ export class Engine {
873
550
  // Defensive copy so callers can't mutate the shared manifest.
874
551
  return descriptor.formats.slice();
875
552
  }
876
-
877
- /**
878
- * Whether `quill`'s backend can paint to a canvas: a pre-session estimate over
879
- * the descriptor's manifest, so it can answer `true` where the resulting
880
- * `LiveSession.supportsCanvas` answers `false`.
881
- * @param {Quill} quill
882
- * @returns {Promise<boolean>}
883
- */
884
- async supportsCanvas(quill) {
885
- const descriptor = this.#descriptorFor(this.#backendOf(quill, 'engine.supportsCanvas(quill)'));
886
- return descriptor.canvas;
887
- }
888
553
  }
889
554
 
890
555
  /**
891
- * Thin wrapper over a backend's live render session; see `runtime.d.ts` for the
892
- * contract. The quill/document clones it was opened from have already been
893
- * freed: the session retains what `update` needs.
556
+ * Thin wrapper over a backend's live render session. The quill/document clones
557
+ * it was opened from have already been freed: the session retains what `update`
558
+ * needs.
894
559
  */
895
560
  export class LiveSession {
896
561
  /**
897
- * @param {{ pageCount: number, backendId: string, supportsCanvas: boolean, warnings: any[], update: Function, render: Function, regions: Function, pageSize: Function, paint: Function, free: Function }} inner backend-build LiveSession (typst or pdfform)
898
- * @param {{ Document: { fromJson(json: string): any } }} mod the session's backend build, used to materialize `update` documents in its linear memory
562
+ * @param {object} inner the render build's LiveSession, whose members the delegations below name
563
+ * @param {{ Document: { fromStored(json: string): any } }} mod the session's backend build, used to materialize `update` documents in its linear memory
899
564
  */
900
565
  constructor(inner, mod) {
901
566
  this.#inner = inner;
@@ -912,7 +577,7 @@ export class LiveSession {
912
577
  requireLocalDoc(doc, 'session.update(doc)');
913
578
  let backendDoc = null;
914
579
  try {
915
- backendDoc = this.#mod.Document.fromJson(doc.toJson());
580
+ backendDoc = this.#mod.Document.fromStored(doc.toStored());
916
581
  return this.#inner.update(backendDoc);
917
582
  } finally {
918
583
  backendDoc?.free();
@@ -925,15 +590,6 @@ export class LiveSession {
925
590
  get backendId() {
926
591
  return this.#inner.backendId;
927
592
  }
928
- /**
929
- * `true` iff `paint`/`pageSize` will succeed for THIS compile: the
930
- * authoritative answer, which can be `false` where `Engine.supportsCanvas`
931
- * answered `true` for the same quill.
932
- * @returns {boolean}
933
- */
934
- get supportsCanvas() {
935
- return this.#inner.supportsCanvas;
936
- }
937
593
  get warnings() {
938
594
  return this.#inner.warnings;
939
595
  }
@@ -962,20 +618,22 @@ export class LiveSession {
962
618
  * @param {number} page
963
619
  * @param {number} x
964
620
  * @param {number} y
621
+ * @param {number} [tolPt]
965
622
  * @returns {string | undefined}
966
623
  */
967
- fieldAt(page, x, y) {
968
- return this.#inner.fieldAt(page, x, y);
624
+ fieldAt(page, x, y, tolPt) {
625
+ return this.#inner.fieldAt(page, x, y, tolPt);
969
626
  }
970
627
 
971
628
  /**
972
629
  * @param {number} page
973
630
  * @param {number} x
974
631
  * @param {number} y
632
+ * @param {number} [tolPt]
975
633
  * @returns {import('./runtime.d.ts').ContentHit | undefined}
976
634
  */
977
- positionAt(page, x, y) {
978
- return this.#inner.positionAt(page, x, y);
635
+ positionAt(page, x, y, tolPt) {
636
+ return this.#inner.positionAt(page, x, y, tolPt);
979
637
  }
980
638
 
981
639
  /**
@@ -1006,28 +664,11 @@ export class LiveSession {
1006
664
  }
1007
665
  }
1008
666
 
1009
- // ── Typed-writer sugar: bind the quill once ─────────────────────────────────
1010
- // Rust exposes `quill.writer(&mut doc)` so a caller issues bare `set` / `set_all`
1011
- // without threading the schema per write. The WASM `commit*` verbs can't borrow
1012
- // like that: a `Document` carries only a `$quill` REFERENCE, not the resolved
1013
- // schema, so each `commit*` method takes the `quill` handle as its first
1014
- // argument. These pure-JS classes restore the Rust ergonomics: bind `quill` +
1015
- // `doc` once, then issue `set` / `setAll` / `card(i).set`.
1016
- //
1017
- // They hold JS references to the caller's EXISTING handles (no WASM object of
1018
- // their own, no `free()` burden, no second owner of either handle) and every
1019
- // write delegates straight to the underlying `commit*` verb: a schema field is
1020
- // typed-committed (coerced to canonical form, mismatch throws now), and a name
1021
- // the schema does not declare throws `UnknownField` rather than falling to the
1022
- // opaque store, on the typed path an undeclared name is a typo. Opaque storage
1023
- // stays available through the raw addressed `Document.storeField` verb.
667
+ // These pure-JS classes bind a `quill` + `doc` pair once and delegate to the
668
+ // `_commit*` / `_reader*` verbs, which take the quill per call. They hold JS
669
+ // references to the caller's existing handles, so there is no WASM object of
670
+ // their own, no `free()` burden, and no second owner of either handle.
1024
671
 
1025
- /**
1026
- * A {@link Document} bound to its {@link Quill} for typed writes: the JS twin
1027
- * of Rust's `quill.writer(&mut doc)`. Writes target the main card; use
1028
- * {@link card} for a composable card. Holds both handles by reference and owns
1029
- * neither, so there is nothing to `free()`.
1030
- */
1031
672
  export class DocumentWriter {
1032
673
  #quill;
1033
674
  #doc;
@@ -1041,13 +682,10 @@ export class DocumentWriter {
1041
682
  this.#quill = quill;
1042
683
  this.#doc = doc;
1043
684
  }
1044
- /** The bound document: the same instance passed in, mutated in place. */
1045
685
  get document() {
1046
686
  return this.#doc;
1047
687
  }
1048
688
  /**
1049
- * Typed-commit one main-card field (strict coerce, mismatch throws now).
1050
- * Throws `UnknownField` for a name the schema does not declare.
1051
689
  * @param {string} name
1052
690
  * @param {unknown} value
1053
691
  * @returns {void}
@@ -1056,9 +694,6 @@ export class DocumentWriter {
1056
694
  return this.#doc._commitField(this.#quill, name, value);
1057
695
  }
1058
696
  /**
1059
- * Typed-commit several main-card fields atomically: nothing is applied on
1060
- * error (throws a per-field diagnostic bundle, including an `UnknownField`
1061
- * for each undeclared name).
1062
697
  * @param {Record<string, unknown>} fields
1063
698
  * @returns {void}
1064
699
  */
@@ -1066,8 +701,6 @@ export class DocumentWriter {
1066
701
  return this.#doc._commitFields(this.#quill, MAIN_CARD_ADDR, fields);
1067
702
  }
1068
703
  /**
1069
- * Revise the main body from markdown; anchors rebase. A body carries no field
1070
- * schema, so this is the content lane's `revise` reached through the writer.
1071
704
  * @param {string} markdown
1072
705
  * @returns {import('../core/wasm.js').Delta}
1073
706
  */
@@ -1075,10 +708,6 @@ export class DocumentWriter {
1075
708
  return this.#doc.revise({}, markdown);
1076
709
  }
1077
710
  /**
1078
- * Revise the content main-card field `name` from authored text: typed *and*
1079
- * anchor-preserving. Anchors rebase, then the diffed result is
1080
- * schema-conformed. The codec comes from the declared type: `richtext` diffs
1081
- * markdown, `plaintext` the literal text.
1082
711
  * @param {string} name
1083
712
  * @param {string} text
1084
713
  * @returns {import('../core/wasm.js').Delta}
@@ -1087,10 +716,6 @@ export class DocumentWriter {
1087
716
  return this.#doc._reviseField(this.#quill, name, text);
1088
717
  }
1089
718
  /**
1090
- * Build a composable card of `kind`, typed-commit `fields` onto it, set its
1091
- * body from optional markdown, and place it. Transactional: the card is
1092
- * committed in full before it joins the document, so a rejected field, kind,
1093
- * body, or position leaves the document untouched.
1094
719
  * @param {string} kind
1095
720
  * @param {Record<string, unknown>} [fields]
1096
721
  * @param {string} [body]
@@ -1108,9 +733,6 @@ export class DocumentWriter {
1108
733
  return this.#doc.removeCard(index);
1109
734
  }
1110
735
  /**
1111
- * A {@link CardWriter} bound to the composable card at `index`, checked
1112
- * lazily at the write. It holds `index`, not the card, so a
1113
- * `removeCard`/`addCard` between binding and writing silently retargets it.
1114
736
  * @param {number} index
1115
737
  * @returns {CardWriter}
1116
738
  */
@@ -1119,11 +741,6 @@ export class DocumentWriter {
1119
741
  }
1120
742
  }
1121
743
 
1122
- /**
1123
- * A single composable card bound to its {@link Quill} for typed writes, from
1124
- * {@link DocumentWriter.card}. Same `set` / `setAll` verbs as
1125
- * {@link DocumentWriter}, targeting the card at its bound index.
1126
- */
1127
744
  export class CardWriter {
1128
745
  #quill;
1129
746
  #doc;
@@ -1140,22 +757,16 @@ export class CardWriter {
1140
757
  this.#doc = doc;
1141
758
  this.#index = index;
1142
759
  }
1143
- /** The bound card index. */
1144
760
  get index() {
1145
761
  return this.#index;
1146
762
  }
1147
763
  /**
1148
- * The bound card's `$kind`, empty string when it carries none. Throws
1149
- * `IndexOutOfRange` for a bad bound index.
1150
764
  * @returns {string}
1151
765
  */
1152
766
  get kind() {
1153
767
  return this.#doc.card(this.#index).kind;
1154
768
  }
1155
769
  /**
1156
- * Typed-commit one field on this card, addressed at `{ card, field }`. Throws
1157
- * `UnknownField` for an undeclared name and `IndexOutOfRange` if the bound
1158
- * index is out of range.
1159
770
  * @param {string} name
1160
771
  * @param {unknown} value
1161
772
  * @returns {void}
@@ -1164,9 +775,6 @@ export class CardWriter {
1164
775
  return this.#doc._commitField(this.#quill, { card: this.#index, field: name }, value);
1165
776
  }
1166
777
  /**
1167
- * Typed-commit several fields on this card atomically, addressed at
1168
- * `{ card }`. Throws a per-field diagnostic bundle on error and
1169
- * `IndexOutOfRange` if the bound index is out of range.
1170
778
  * @param {Record<string, unknown>} fields
1171
779
  * @returns {void}
1172
780
  */
@@ -1174,7 +782,6 @@ export class CardWriter {
1174
782
  return this.#doc._commitFields(this.#quill, { card: this.#index }, fields);
1175
783
  }
1176
784
  /**
1177
- * The card twin of {@link DocumentWriter.reviseBody}.
1178
785
  * @param {string} markdown
1179
786
  * @returns {import('../core/wasm.js').Delta}
1180
787
  */
@@ -1182,8 +789,6 @@ export class CardWriter {
1182
789
  return this.#doc.revise({ card: this.#index }, markdown);
1183
790
  }
1184
791
  /**
1185
- * The card twin of {@link DocumentWriter.reviseField}. Throws `UnknownField`
1186
- * for an undeclared name and `IndexOutOfRange` for a bad bound index.
1187
792
  * @param {string} name
1188
793
  * @param {string} text
1189
794
  * @returns {import('../core/wasm.js').Delta}
@@ -1193,13 +798,10 @@ export class CardWriter {
1193
798
  }
1194
799
  }
1195
800
 
1196
- // ── `quill.writer(doc)`, the typed front door ──────────────────────────────
1197
801
  // Patched onto the re-exported `Quill` prototype rather than wrapped, so
1198
802
  // `Quill === CoreQuill` stays true: this only adds a method constructing the
1199
803
  // pure-JS writer, which owns no WASM handle.
1200
804
  /**
1201
- * A {@link DocumentWriter} binding this quill's schema to `doc`. It holds both
1202
- * handles by reference and owns neither: bind, write, discard.
1203
805
  * @this {Quill}
1204
806
  * @param {Document} doc the document to mutate, held by reference (not owned)
1205
807
  * @returns {DocumentWriter}
@@ -1208,17 +810,10 @@ Quill.prototype.writer = function writer(doc) {
1208
810
  return new DocumentWriter(this, doc);
1209
811
  };
1210
812
 
1211
- // ── Typed-reader sugar: the schema-plane read surface ─────────────────────────
1212
813
  // The transport `Document.getStored` is schema-free, so an unknown field name
1213
814
  // reads back `undefined` rather than as the typo it is. Binding the quill's
1214
- // schema lets one `get` interpret by declared type and throw `UnknownField` on a
1215
- // name the schema does not declare.
815
+ // schema lets one `get` interpret by declared type and throw `UnknownField`.
1216
816
 
1217
- /**
1218
- * A {@link Document} bound to its {@link Quill} for typed reads, the read
1219
- * counterpart of {@link DocumentWriter}. Reads target the main card; use
1220
- * {@link card} for a composable one. Owns neither handle.
1221
- */
1222
817
  export class DocumentReader {
1223
818
  #quill;
1224
819
  #doc;
@@ -1232,17 +827,10 @@ export class DocumentReader {
1232
827
  this.#quill = quill;
1233
828
  this.#doc = doc;
1234
829
  }
1235
- /** The bound document: the same instance passed in. */
1236
830
  get document() {
1237
831
  return this.#doc;
1238
832
  }
1239
833
  /**
1240
- * Read the value at `addr`, interpreted by its declared type: a richtext field
1241
- * to markdown, every other type verbatim. A bare string is `Addr` shorthand
1242
- * for `{ field }`; an absent `addr.field` reads the body markdown. `undefined`
1243
- * for an absent field; throws `UnknownField` for a name the schema does not
1244
- * declare, `FieldDecode` for a richtext field holding an undecodable
1245
- * value, and `IndexOutOfRange` for a bad `addr.card`.
1246
834
  * @param {import('../core/wasm.js').Addr | string} addr
1247
835
  * @returns {unknown}
1248
836
  */
@@ -1250,11 +838,6 @@ export class DocumentReader {
1250
838
  return this.#doc._readerGet(this.#quill, addr);
1251
839
  }
1252
840
  /**
1253
- * Read the content field at `addr` as canonical `Content`: the twin of
1254
- * {@link get}, which projects. An absent `addr.field` reads the body
1255
- * `Content`. `undefined` for an absent field; throws `UnknownField`,
1256
- * `FieldNotContent` for a type that is not a content leaf, `FieldDecode` for
1257
- * an undecodable value, and `IndexOutOfRange` for a bad `addr.card`.
1258
841
  * @param {import('../core/wasm.js').Addr | string} addr
1259
842
  * @returns {import('../core/wasm.js').Content | undefined}
1260
843
  */
@@ -1262,13 +845,6 @@ export class DocumentReader {
1262
845
  return this.#doc._readerGetContent(this.#quill, addr);
1263
846
  }
1264
847
  /**
1265
- * Read the `Content` nested inside the composite field at `addr`, at `path`:
1266
- * `[0]` an `array<richtext>` element, `["motto"]` an object's content property,
1267
- * `[1, "notes"]` a leaf under both, `["controlled_by"]` a variant's cell. The
1268
- * codec is the leaf's declared type's.
1269
- * `undefined` for an absent field and for a path that names nothing stored;
1270
- * throws `UnknownField`, `FieldNotContent` when `path` resolves to no content
1271
- * leaf, `FieldDecode` anchored at the addressed path, and `IndexOutOfRange`.
1272
848
  * @param {import('../core/wasm.js').Addr | string} addr
1273
849
  * @param {import('../core/wasm.js').PathStep[]} path
1274
850
  * @returns {import('../core/wasm.js').Content | undefined}
@@ -1277,16 +853,18 @@ export class DocumentReader {
1277
853
  return this.#doc._readerGetContentAt(this.#quill, addr, path);
1278
854
  }
1279
855
  /**
1280
- * The main body's markdown: the quill-free body read. Equals `get({})`.
1281
856
  * @returns {string}
1282
857
  */
1283
858
  bodyMarkdown() {
1284
859
  return this.#doc._readerGet(this.#quill, {});
1285
860
  }
1286
861
  /**
1287
- * A {@link CardReader} bound to the composable card at `index`, checked lazily
1288
- * at the read. It holds `index`, not the card, so a `removeCard`/`addCard`
1289
- * between binding and reading silently retargets it.
862
+ * @returns {Resolved}
863
+ */
864
+ resolve() {
865
+ return this.#quill._resolve(this.#doc);
866
+ }
867
+ /**
1290
868
  * @param {number} index
1291
869
  * @returns {CardReader}
1292
870
  */
@@ -1295,11 +873,6 @@ export class DocumentReader {
1295
873
  }
1296
874
  }
1297
875
 
1298
- /**
1299
- * A single composable card bound to its {@link Quill} for typed reads, from
1300
- * {@link DocumentReader.card}. Same `get` / `bodyMarkdown` verbs as
1301
- * {@link DocumentReader}, reading the card at its bound index.
1302
- */
1303
876
  export class CardReader {
1304
877
  #quill;
1305
878
  #doc;
@@ -1316,22 +889,16 @@ export class CardReader {
1316
889
  this.#doc = doc;
1317
890
  this.#index = index;
1318
891
  }
1319
- /** The bound card index. */
1320
892
  get index() {
1321
893
  return this.#index;
1322
894
  }
1323
895
  /**
1324
- * The bound card's `$kind` (empty string when it carries none), read through
1325
- * the document. Throws `IndexOutOfRange` if the bound index is out of range.
1326
896
  * @returns {string}
1327
897
  */
1328
898
  get kind() {
1329
899
  return this.#doc.card(this.#index).kind;
1330
900
  }
1331
901
  /**
1332
- * Read the field `name` on this card, interpreted by its declared type,
1333
- * addressed at `{ card, field }`. `undefined` when absent; throws
1334
- * `UnknownField` for an undeclared name and `IndexOutOfRange` for a bad index.
1335
902
  * @param {string} name
1336
903
  * @returns {unknown}
1337
904
  */
@@ -1339,7 +906,6 @@ export class CardReader {
1339
906
  return this.#doc._readerGet(this.#quill, { card: this.#index, field: name });
1340
907
  }
1341
908
  /**
1342
- * The card twin of {@link DocumentReader.getContent}.
1343
909
  * @param {string} name
1344
910
  * @returns {import('../core/wasm.js').Content | undefined}
1345
911
  */
@@ -1347,7 +913,6 @@ export class CardReader {
1347
913
  return this.#doc._readerGetContent(this.#quill, { card: this.#index, field: name });
1348
914
  }
1349
915
  /**
1350
- * The card twin of {@link DocumentReader.getContentAt}.
1351
916
  * @param {string} name
1352
917
  * @param {import('../core/wasm.js').PathStep[]} path
1353
918
  * @returns {import('../core/wasm.js').Content | undefined}
@@ -1356,7 +921,6 @@ export class CardReader {
1356
921
  return this.#doc._readerGetContentAt(this.#quill, { card: this.#index, field: name }, path);
1357
922
  }
1358
923
  /**
1359
- * The card twin of {@link DocumentReader.bodyMarkdown}.
1360
924
  * @returns {string}
1361
925
  */
1362
926
  bodyMarkdown() {
@@ -1364,11 +928,8 @@ export class CardReader {
1364
928
  }
1365
929
  }
1366
930
 
1367
- // ── `quill.reader(doc)`: the schema-plane read front door ─────────────────────
1368
931
  // Patched onto the same re-exported `Quill` prototype as `writer`.
1369
932
  /**
1370
- * A {@link DocumentReader} binding this quill's schema to `doc`. It holds both
1371
- * handles by reference and owns neither: bind, read, discard.
1372
933
  * @this {Quill}
1373
934
  * @param {Document} doc the document to read, held by reference (not owned)
1374
935
  * @returns {DocumentReader}