@quillmark/wasm 0.112.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,26 @@ 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`, and a `link`/`anchor` mark, a `heading`/`code` line and a
402
- // `list_item` container each carry their payload in `attrs`; the payload-free
403
- // arms (`strong`/`emph`/`underline`/`strike`/`code` marks, `para`/`island`/
404
- // `rule` lines, `quote`) omit `attrs` and narrow to nothing. An unrecognized
405
- // discriminant fails every guard and carries the same `attrs` a known one
406
- // would.
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'; attrs: { 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'; attrs: { 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'; attrs: { 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'; attrs?: { 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'; attrs: { 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
218
  // WELD_KEYS is the rule `Container::same_weld` owns upstream: which `attrs`
521
219
  // entries two adjacent runs must share for the markdown projection to read them
522
- // as one, and therefore for the canonical form to have to spend a
523
- // discriminator. `start` is not among them, since CommonMark reads only a
524
- // list's first number — a subset, which is why a built-in needs an entry rather
525
- // than the unknown branch's whole-bag compare. A table rather than a switch, so
526
- // `tests/known_names_drift.rs` can pin it against the Rust predicate.
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
222
+ // rather than a switch, so `tests/known_names_drift.rs` can pin it against the
223
+ // Rust predicate.
527
224
 
528
225
  const WELD_KEYS = { list_item: ['ordered'], quote: [] };
529
226
 
530
- function sameJson(a, b) {
531
- if (a === b) return true;
532
- if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;
533
- if (Array.isArray(a) !== Array.isArray(b)) return false;
534
- const ka = Object.keys(a);
535
- return (
536
- ka.length === Object.keys(b).length &&
537
- ka.every((k) => Object.hasOwn(b, k) && sameJson(a[k], b[k]))
538
- );
539
- }
540
-
541
227
  /**
542
228
  * @param {import('../core/wasm.js').ContentContainer} a
543
229
  * @param {import('../core/wasm.js').ContentContainer} b
544
230
  * @returns {boolean}
545
231
  */
546
232
  function weldsWith(a, b) {
547
- // A malformed value welds with nothing. The membership guards' posture:
548
- // answer rather than throw.
233
+ // A malformed value welds with nothing: answer rather than throw.
549
234
  if (typeof a?.container !== 'string' || a.container !== b?.container) return false;
550
- // `hasOwn`, so a tag colliding with an `Object.prototype` member reaches the
551
- // unknown branch rather than a function.
552
- if (!Object.hasOwn(WELD_KEYS, a.container)) return sameJson(a.attrs, b.attrs);
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;
553
238
  return WELD_KEYS[a.container].every((k) => a.attrs?.[k] === b.attrs?.[k]);
554
239
  }
555
240
 
@@ -579,7 +264,7 @@ export function assignInstances(runs) {
579
264
  * generated entry's own `wasm !== undefined` guard only catches a call arriving
580
265
  * after one finished, not one already in flight.
581
266
  *
582
- * @param {string} id backend id, for the failure message
267
+ * @param {string} id the build's name, for the failure message
583
268
  * @param {() => Promise<any>} importThunk the dynamic `import()`
584
269
  * @param {() => URL} wasmUrl the build's binary, resolved at call time
585
270
  * @returns {() => Promise<any>} resolves to a ready-to-use module
@@ -598,108 +283,89 @@ function backendLoad(id, importThunk, wasmUrl) {
598
283
  throw Object.assign(
599
284
  quillmarkError(
600
285
  'runtime::backend_load_failed',
601
- `Engine: could not load the '${id}' backend: ${
286
+ `Engine: could not load the '${id}' build: ${
602
287
  /** @type {any} */ (cause)?.message ?? cause
603
288
  }`,
604
- '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.'
605
290
  ),
606
291
  { cause }
607
292
  );
608
293
  }));
609
294
  }
610
295
 
611
- // Backend builds are NEVER statically imported here: that would pull a multi-MB
612
- // binary into the eager graph and defeat lazy loading. Each entry is a
613
- // DESCRIPTOR: `load` dynamically imports and instantiates a backend's chunk, so
614
- // the binary is fetched only when something renders against it; `formats` and
615
- // `canvas` are the required static capability manifest, so the probes
616
- // (`supportedFormats` / `supportsCanvas`) answer without loading the binary or
617
- // cloning the quill. The manifest mirrors each backend's Rust `SUPPORTED_FORMATS`
618
- // (and `formats_support_canvas`: true iff the list includes `svg` or `png`),
619
- // 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
+ );
620
306
  const DEFAULT_BACKENDS = {
621
307
  typst: {
622
- load: backendLoad(
623
- 'typst',
624
- () => import('../backends/typst/wasm.js'),
625
- () => new URL('../backends/typst/wasm_bg.wasm', import.meta.url)
626
- ),
627
- formats: ['pdf', 'svg', 'png'], // crates/backends/typst/src/lib.rs SUPPORTED_FORMATS
628
- 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
629
310
  },
630
- pdfform: {
631
- load: backendLoad(
632
- 'pdfform',
633
- () => import('../backends/pdfform/wasm.js'),
634
- () => new URL('../backends/pdfform/wasm_bg.wasm', import.meta.url)
635
- ),
636
- // crates/backends/pdfform/src/lib.rs SUPPORTED_FORMATS == [Pdf, Svg, Png]
637
- formats: ['pdf', 'svg', 'png'],
638
- 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
639
314
  }
640
315
  };
641
316
 
642
317
  /**
643
318
  * Validate a backend registry descriptor, naming the backend id on any
644
319
  * malformed entry. Failing at construction rather than deep inside a render is
645
- * what lets the capability probes answer from the manifest unconditionally.
320
+ * what lets `supportedFormats` answer from the manifest unconditionally.
646
321
  * @param {string} id
647
322
  * @param {unknown} entry
648
- * @returns {{ load: () => Promise<unknown>, formats: string[], canvas: boolean }}
323
+ * @returns {{ load: () => Promise<unknown>, formats: string[] }}
649
324
  */
650
325
  function validateBackend(id, entry) {
651
326
  if (!entry || typeof entry !== 'object') {
652
- throw new Error(
653
- `Engine: backend '${id}' must be a descriptor { load, formats, canvas }.`
654
- );
327
+ throw new Error(`Engine: backend '${id}' must be a descriptor { load, formats }.`);
655
328
  }
656
- const { load, formats, canvas } = /** @type {any} */ (entry);
329
+ const { load, formats } = /** @type {any} */ (entry);
657
330
  if (typeof load !== 'function') {
658
331
  throw new Error(`Engine: backend '${id}' descriptor needs a callable 'load'.`);
659
332
  }
660
333
  if (!Array.isArray(formats)) {
661
334
  throw new Error(`Engine: backend '${id}' descriptor needs a 'formats' array.`);
662
335
  }
663
- if (typeof canvas !== 'boolean') {
664
- throw new Error(`Engine: backend '${id}' descriptor needs a boolean 'canvas'.`);
665
- }
666
- return { load, formats, canvas };
336
+ return { load, formats };
667
337
  }
668
338
 
669
- /**
670
- * Render dispatcher over the canonical `Quill`/`Document`. One `Engine`
671
- * instance can drive every backend; it resolves the right backend build from
672
- * each quill's declared `backendId` and loads it lazily on first use.
673
- */
674
339
  export class Engine {
675
- /** 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. */
676
347
  #modules = new Map();
677
- /** backendId → that backend's engine instance (the WASM backend registry). */
348
+ /** load → that build's engine instance (the WASM backend registry). */
678
349
  #engines = new Map();
679
- /** backendId → descriptor `{ load, formats, canvas }`. */
350
+ /** backendId → descriptor `{ load, formats }`. */
680
351
  #loaders;
681
352
  /**
682
- * backendId → WeakMap<canonical Quill, backend-memory clone>, caching the
353
+ * load → WeakMap<canonical Quill, build-memory clone>, caching the
683
354
  * expensive materialization. WeakMap so dropping the canonical quill makes
684
355
  * its clone collectable, and wasm-bindgen weak-refs then free the handle.
685
- * @type {Map<string, WeakMap<object, any>>}
356
+ * @type {Map<() => Promise<unknown>, WeakMap<object, any>>}
686
357
  */
687
358
  #quillClones = new Map();
688
359
 
689
360
  /**
690
- * @param {{ backends?: Record<string, { load: () => Promise<unknown>, formats: string[], canvas: boolean }> }} [options]
691
- * Extra or overriding backend descriptors, merged over the built-ins. Each
692
- * is `{ load, formats, canvas }` with the manifest REQUIRED, since that is
693
- * what makes `supportedFormats` / `supportsCanvas` free; malformed entries
694
- * throw here, at construction.
695
- *
696
- * `load` resolves to a READY module: a registrant shipping its own
697
- * `--target web` build instantiates inside the thunk. More than one
698
- * `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]
699
365
  */
700
366
  constructor(options) {
701
367
  const merged = { ...DEFAULT_BACKENDS, ...(options?.backends ?? {}) };
702
- /** @type {Record<string, { load: () => Promise<unknown>, formats: string[], canvas: boolean }>} */
368
+ /** @type {Record<string, { load: () => Promise<unknown>, formats: string[] }>} */
703
369
  const loaders = {};
704
370
  for (const [id, entry] of Object.entries(merged)) {
705
371
  loaders[id] = validateBackend(id, entry);
@@ -708,17 +374,19 @@ export class Engine {
708
374
  }
709
375
 
710
376
  /**
711
- * The registered descriptor for `backendId`, or the "no backend registered"
712
- * 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.
713
380
  * @param {string} backendId
714
- * @returns {{ load: () => Promise<unknown>, formats: string[], canvas: boolean }}
381
+ * @returns {{ load: () => Promise<unknown>, formats: string[] }}
715
382
  */
716
383
  #descriptorFor(backendId) {
717
384
  const descriptor = this.#loaders[backendId];
718
385
  if (!descriptor) {
719
- throw new Error(
720
- `Engine: no backend registered for '${backendId}'. ` +
721
- `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)'}`
722
390
  );
723
391
  }
724
392
  return descriptor;
@@ -738,51 +406,52 @@ export class Engine {
738
406
  }
739
407
 
740
408
  /**
741
- * Resolve (and lazily load) the backend module + its engine for `backendId`.
409
+ * Resolve (and lazily load) the build module + its engine for `backendId`.
742
410
  * @param {string} backendId
743
411
  * @returns {Promise<{ mod: any, engine: any }>}
744
412
  */
745
413
  async #resolveBackend(backendId) {
746
- const descriptor = this.#descriptorFor(backendId);
414
+ const { load } = this.#descriptorFor(backendId);
747
415
 
748
- let modPromise = this.#modules.get(backendId);
416
+ let modPromise = this.#modules.get(load);
749
417
  if (!modPromise) {
750
418
  // Set the promise synchronously (before any await) so concurrent first
751
419
  // renders share ONE import. Self-heal on failure so a transient load
752
420
  // error doesn't poison every later attempt.
753
421
  modPromise = Promise.resolve()
754
- .then(descriptor.load)
422
+ .then(load)
755
423
  .catch((err) => {
756
- this.#modules.delete(backendId);
424
+ this.#modules.delete(load);
757
425
  throw err;
758
426
  });
759
- this.#modules.set(backendId, modPromise);
427
+ this.#modules.set(load, modPromise);
760
428
  }
761
429
  const mod = await modPromise;
762
430
 
763
- let engine = this.#engines.get(backendId);
431
+ let engine = this.#engines.get(load);
764
432
  if (!engine) {
765
433
  engine = new mod.Quillmark();
766
- this.#engines.set(backendId, engine);
434
+ this.#engines.set(load, engine);
767
435
  }
768
436
  return { mod, engine };
769
437
  }
770
438
 
771
439
  /**
772
- * Get (or materialize-and-cache) the backend-memory `Quill` clone for `quill`
773
- * under `backendId`. On a miss the clone is built from `tree`, the caller's
774
- * pre-await snapshot, since the canonical handle may be freed by now.
775
- * @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
776
444
  * @param {string} backendId
777
445
  * @param {object} quill the canonical instance (cache key only)
778
446
  * @param {Map<string, Uint8Array> | null} tree pre-await snapshot; `null` on a cache hit
779
- * @returns {any} the backend-memory quill clone
447
+ * @returns {any} the build-memory quill clone
780
448
  */
781
449
  #cachedQuillClone(mod, backendId, quill, tree) {
782
- let perQuill = this.#quillClones.get(backendId);
450
+ const { load } = this.#descriptorFor(backendId);
451
+ let perQuill = this.#quillClones.get(load);
783
452
  if (!perQuill) {
784
453
  perQuill = new WeakMap();
785
- this.#quillClones.set(backendId, perQuill);
454
+ this.#quillClones.set(load, perQuill);
786
455
  }
787
456
  let backendQuill = perQuill.get(quill);
788
457
  if (!backendQuill) {
@@ -797,11 +466,15 @@ export class Engine {
797
466
  * memory and run `fn` against the backend engine. Only `render`/`open` call
798
467
  * this, so `doc` is always present.
799
468
  *
800
- * OWNERSHIP WINDOW: both caller handles are snapshotted (`doc.toJson()`, and
801
- * `quill.toTree()` on a clone-cache miss) BEFORE the first await. The backend
802
- * load below is a real suspension point, so reading the handles after it
803
- * would race a caller that `free()`s them as soon as this call returns its
804
- * 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.
805
478
  *
806
479
  * Clone lifetimes differ by design: the `doc` clone is TRANSIENT, freed in
807
480
  * the `finally` of every call, while the `quill` clone is CACHED and is not
@@ -811,13 +484,16 @@ export class Engine {
811
484
  * @param {string} method the caller's name, for the rejection message
812
485
  * @param {Quill} quill
813
486
  * @param {Document} doc
814
- * @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
815
488
  */
816
489
  async #withClones(method, quill, doc, fn) {
817
490
  const backendId = this.#backendOf(quill, method);
818
491
  requireLocalDoc(doc, method);
819
- const docJson = doc.toJson();
820
- 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();
821
497
  const { mod, engine } = await this.#resolveBackend(backendId);
822
498
  // The doc clone and `fn` share one try so the clone is freed even if a
823
499
  // later step throws; the cached quill clone is intentionally not freed.
@@ -825,31 +501,33 @@ export class Engine {
825
501
  const backendQuill = this.#cachedQuillClone(mod, backendId, quill, quillTree);
826
502
  let backendDoc = null;
827
503
  try {
828
- backendDoc = mod.Document.fromJson(docJson);
829
- return fn({ mod, engine, quill: backendQuill, doc: backendDoc });
504
+ backendDoc = mod.Document.fromStored(docJson);
505
+ return fn({ mod, engine, quill: backendQuill, doc: backendDoc, docWarnings });
830
506
  } finally {
831
507
  backendDoc?.free();
832
508
  }
833
509
  }
834
510
 
835
511
  /**
836
- * Render `doc` against `quill` in one shot. Both handles are read
837
- * synchronously before the first await.
838
512
  * @param {Quill} quill
839
513
  * @param {Document} doc
840
- * @param {object} [options] render options (`{ format, ppi, pages, producer }`)
514
+ * @param {object} [options] render options (`{ format, ppi, pages, regions }`)
841
515
  * @returns {Promise<import('./runtime.js').RenderResult>}
842
516
  */
843
517
  async render(quill, doc, options) {
844
- return this.#withClones('engine.render(quill, doc)', quill, doc, ({ engine, quill: q, doc: d }) =>
845
- 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
+ }
846
527
  );
847
528
  }
848
529
 
849
530
  /**
850
- * Open a live render session. It retains what `update` needs, so the
851
- * transient clones are freed before this returns; the caller owns the session
852
- * and must `.free()` it.
853
531
  * @param {Quill} quill
854
532
  * @param {Document} doc
855
533
  * @returns {Promise<LiveSession>}
@@ -864,8 +542,6 @@ export class Engine {
864
542
  }
865
543
 
866
544
  /**
867
- * The output formats `quill`'s backend can emit: an always-free probe over
868
- * the descriptor's manifest. `async` for API stability; it awaits nothing.
869
545
  * @param {Quill} quill
870
546
  * @returns {Promise<import('./runtime.js').OutputFormat[]>}
871
547
  */
@@ -874,29 +550,17 @@ export class Engine {
874
550
  // Defensive copy so callers can't mutate the shared manifest.
875
551
  return descriptor.formats.slice();
876
552
  }
877
-
878
- /**
879
- * Whether `quill`'s backend can paint to a canvas: a pre-session estimate over
880
- * the descriptor's manifest, so it can answer `true` where the resulting
881
- * `LiveSession.supportsCanvas` answers `false`.
882
- * @param {Quill} quill
883
- * @returns {Promise<boolean>}
884
- */
885
- async supportsCanvas(quill) {
886
- const descriptor = this.#descriptorFor(this.#backendOf(quill, 'engine.supportsCanvas(quill)'));
887
- return descriptor.canvas;
888
- }
889
553
  }
890
554
 
891
555
  /**
892
- * Thin wrapper over a backend's live render session; see `runtime.d.ts` for the
893
- * contract. The quill/document clones it was opened from have already been
894
- * 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.
895
559
  */
896
560
  export class LiveSession {
897
561
  /**
898
- * @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)
899
- * @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
900
564
  */
901
565
  constructor(inner, mod) {
902
566
  this.#inner = inner;
@@ -913,7 +577,7 @@ export class LiveSession {
913
577
  requireLocalDoc(doc, 'session.update(doc)');
914
578
  let backendDoc = null;
915
579
  try {
916
- backendDoc = this.#mod.Document.fromJson(doc.toJson());
580
+ backendDoc = this.#mod.Document.fromStored(doc.toStored());
917
581
  return this.#inner.update(backendDoc);
918
582
  } finally {
919
583
  backendDoc?.free();
@@ -926,15 +590,6 @@ export class LiveSession {
926
590
  get backendId() {
927
591
  return this.#inner.backendId;
928
592
  }
929
- /**
930
- * `true` iff `paint`/`pageSize` will succeed for THIS compile: the
931
- * authoritative answer, which can be `false` where `Engine.supportsCanvas`
932
- * answered `true` for the same quill.
933
- * @returns {boolean}
934
- */
935
- get supportsCanvas() {
936
- return this.#inner.supportsCanvas;
937
- }
938
593
  get warnings() {
939
594
  return this.#inner.warnings;
940
595
  }
@@ -1009,28 +664,11 @@ export class LiveSession {
1009
664
  }
1010
665
  }
1011
666
 
1012
- // ── Typed-writer sugar: bind the quill once ─────────────────────────────────
1013
- // Rust exposes `quill.writer(&mut doc)` so a caller issues bare `set` / `set_all`
1014
- // without threading the schema per write. The WASM `commit*` verbs can't borrow
1015
- // like that: a `Document` carries only a `$quill` REFERENCE, not the resolved
1016
- // schema, so each `commit*` method takes the `quill` handle as its first
1017
- // argument. These pure-JS classes restore the Rust ergonomics: bind `quill` +
1018
- // `doc` once, then issue `set` / `setAll` / `card(i).set`.
1019
- //
1020
- // They hold JS references to the caller's EXISTING handles (no WASM object of
1021
- // their own, no `free()` burden, no second owner of either handle) and every
1022
- // write delegates straight to the underlying `commit*` verb: a schema field is
1023
- // typed-committed (coerced to canonical form, mismatch throws now), and a name
1024
- // the schema does not declare throws `UnknownField` rather than falling to the
1025
- // opaque store, on the typed path an undeclared name is a typo. Opaque storage
1026
- // 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.
1027
671
 
1028
- /**
1029
- * A {@link Document} bound to its {@link Quill} for typed writes: the JS twin
1030
- * of Rust's `quill.writer(&mut doc)`. Writes target the main card; use
1031
- * {@link card} for a composable card. Holds both handles by reference and owns
1032
- * neither, so there is nothing to `free()`.
1033
- */
1034
672
  export class DocumentWriter {
1035
673
  #quill;
1036
674
  #doc;
@@ -1044,13 +682,10 @@ export class DocumentWriter {
1044
682
  this.#quill = quill;
1045
683
  this.#doc = doc;
1046
684
  }
1047
- /** The bound document: the same instance passed in, mutated in place. */
1048
685
  get document() {
1049
686
  return this.#doc;
1050
687
  }
1051
688
  /**
1052
- * Typed-commit one main-card field (strict coerce, mismatch throws now).
1053
- * Throws `UnknownField` for a name the schema does not declare.
1054
689
  * @param {string} name
1055
690
  * @param {unknown} value
1056
691
  * @returns {void}
@@ -1059,9 +694,6 @@ export class DocumentWriter {
1059
694
  return this.#doc._commitField(this.#quill, name, value);
1060
695
  }
1061
696
  /**
1062
- * Typed-commit several main-card fields atomically: nothing is applied on
1063
- * error (throws a per-field diagnostic bundle, including an `UnknownField`
1064
- * for each undeclared name).
1065
697
  * @param {Record<string, unknown>} fields
1066
698
  * @returns {void}
1067
699
  */
@@ -1069,8 +701,6 @@ export class DocumentWriter {
1069
701
  return this.#doc._commitFields(this.#quill, MAIN_CARD_ADDR, fields);
1070
702
  }
1071
703
  /**
1072
- * Revise the main body from markdown; anchors rebase. A body carries no field
1073
- * schema, so this is the content lane's `revise` reached through the writer.
1074
704
  * @param {string} markdown
1075
705
  * @returns {import('../core/wasm.js').Delta}
1076
706
  */
@@ -1078,10 +708,6 @@ export class DocumentWriter {
1078
708
  return this.#doc.revise({}, markdown);
1079
709
  }
1080
710
  /**
1081
- * Revise the content main-card field `name` from authored text: typed *and*
1082
- * anchor-preserving. Anchors rebase, then the diffed result is
1083
- * schema-conformed. The codec comes from the declared type: `richtext` diffs
1084
- * markdown, `plaintext` the literal text.
1085
711
  * @param {string} name
1086
712
  * @param {string} text
1087
713
  * @returns {import('../core/wasm.js').Delta}
@@ -1090,10 +716,6 @@ export class DocumentWriter {
1090
716
  return this.#doc._reviseField(this.#quill, name, text);
1091
717
  }
1092
718
  /**
1093
- * Build a composable card of `kind`, typed-commit `fields` onto it, set its
1094
- * body from optional markdown, and place it. Transactional: the card is
1095
- * committed in full before it joins the document, so a rejected field, kind,
1096
- * body, or position leaves the document untouched.
1097
719
  * @param {string} kind
1098
720
  * @param {Record<string, unknown>} [fields]
1099
721
  * @param {string} [body]
@@ -1111,9 +733,6 @@ export class DocumentWriter {
1111
733
  return this.#doc.removeCard(index);
1112
734
  }
1113
735
  /**
1114
- * A {@link CardWriter} bound to the composable card at `index`, checked
1115
- * lazily at the write. It holds `index`, not the card, so a
1116
- * `removeCard`/`addCard` between binding and writing silently retargets it.
1117
736
  * @param {number} index
1118
737
  * @returns {CardWriter}
1119
738
  */
@@ -1122,11 +741,6 @@ export class DocumentWriter {
1122
741
  }
1123
742
  }
1124
743
 
1125
- /**
1126
- * A single composable card bound to its {@link Quill} for typed writes, from
1127
- * {@link DocumentWriter.card}. Same `set` / `setAll` verbs as
1128
- * {@link DocumentWriter}, targeting the card at its bound index.
1129
- */
1130
744
  export class CardWriter {
1131
745
  #quill;
1132
746
  #doc;
@@ -1143,22 +757,16 @@ export class CardWriter {
1143
757
  this.#doc = doc;
1144
758
  this.#index = index;
1145
759
  }
1146
- /** The bound card index. */
1147
760
  get index() {
1148
761
  return this.#index;
1149
762
  }
1150
763
  /**
1151
- * The bound card's `$kind`, empty string when it carries none. Throws
1152
- * `IndexOutOfRange` for a bad bound index.
1153
764
  * @returns {string}
1154
765
  */
1155
766
  get kind() {
1156
767
  return this.#doc.card(this.#index).kind;
1157
768
  }
1158
769
  /**
1159
- * Typed-commit one field on this card, addressed at `{ card, field }`. Throws
1160
- * `UnknownField` for an undeclared name and `IndexOutOfRange` if the bound
1161
- * index is out of range.
1162
770
  * @param {string} name
1163
771
  * @param {unknown} value
1164
772
  * @returns {void}
@@ -1167,9 +775,6 @@ export class CardWriter {
1167
775
  return this.#doc._commitField(this.#quill, { card: this.#index, field: name }, value);
1168
776
  }
1169
777
  /**
1170
- * Typed-commit several fields on this card atomically, addressed at
1171
- * `{ card }`. Throws a per-field diagnostic bundle on error and
1172
- * `IndexOutOfRange` if the bound index is out of range.
1173
778
  * @param {Record<string, unknown>} fields
1174
779
  * @returns {void}
1175
780
  */
@@ -1177,7 +782,6 @@ export class CardWriter {
1177
782
  return this.#doc._commitFields(this.#quill, { card: this.#index }, fields);
1178
783
  }
1179
784
  /**
1180
- * The card twin of {@link DocumentWriter.reviseBody}.
1181
785
  * @param {string} markdown
1182
786
  * @returns {import('../core/wasm.js').Delta}
1183
787
  */
@@ -1185,8 +789,6 @@ export class CardWriter {
1185
789
  return this.#doc.revise({ card: this.#index }, markdown);
1186
790
  }
1187
791
  /**
1188
- * The card twin of {@link DocumentWriter.reviseField}. Throws `UnknownField`
1189
- * for an undeclared name and `IndexOutOfRange` for a bad bound index.
1190
792
  * @param {string} name
1191
793
  * @param {string} text
1192
794
  * @returns {import('../core/wasm.js').Delta}
@@ -1196,13 +798,10 @@ export class CardWriter {
1196
798
  }
1197
799
  }
1198
800
 
1199
- // ── `quill.writer(doc)`, the typed front door ──────────────────────────────
1200
801
  // Patched onto the re-exported `Quill` prototype rather than wrapped, so
1201
802
  // `Quill === CoreQuill` stays true: this only adds a method constructing the
1202
803
  // pure-JS writer, which owns no WASM handle.
1203
804
  /**
1204
- * A {@link DocumentWriter} binding this quill's schema to `doc`. It holds both
1205
- * handles by reference and owns neither: bind, write, discard.
1206
805
  * @this {Quill}
1207
806
  * @param {Document} doc the document to mutate, held by reference (not owned)
1208
807
  * @returns {DocumentWriter}
@@ -1211,17 +810,10 @@ Quill.prototype.writer = function writer(doc) {
1211
810
  return new DocumentWriter(this, doc);
1212
811
  };
1213
812
 
1214
- // ── Typed-reader sugar: the schema-plane read surface ─────────────────────────
1215
813
  // The transport `Document.getStored` is schema-free, so an unknown field name
1216
814
  // reads back `undefined` rather than as the typo it is. Binding the quill's
1217
- // schema lets one `get` interpret by declared type and throw `UnknownField` on a
1218
- // name the schema does not declare.
815
+ // schema lets one `get` interpret by declared type and throw `UnknownField`.
1219
816
 
1220
- /**
1221
- * A {@link Document} bound to its {@link Quill} for typed reads, the read
1222
- * counterpart of {@link DocumentWriter}. Reads target the main card; use
1223
- * {@link card} for a composable one. Owns neither handle.
1224
- */
1225
817
  export class DocumentReader {
1226
818
  #quill;
1227
819
  #doc;
@@ -1235,17 +827,10 @@ export class DocumentReader {
1235
827
  this.#quill = quill;
1236
828
  this.#doc = doc;
1237
829
  }
1238
- /** The bound document: the same instance passed in. */
1239
830
  get document() {
1240
831
  return this.#doc;
1241
832
  }
1242
833
  /**
1243
- * Read the value at `addr`, interpreted by its declared type: a richtext field
1244
- * to markdown, every other type verbatim. A bare string is `Addr` shorthand
1245
- * for `{ field }`; an absent `addr.field` reads the body markdown. `undefined`
1246
- * for an absent field; throws `UnknownField` for a name the schema does not
1247
- * declare, `FieldDecode` for a richtext field holding an undecodable
1248
- * value, and `IndexOutOfRange` for a bad `addr.card`.
1249
834
  * @param {import('../core/wasm.js').Addr | string} addr
1250
835
  * @returns {unknown}
1251
836
  */
@@ -1253,11 +838,6 @@ export class DocumentReader {
1253
838
  return this.#doc._readerGet(this.#quill, addr);
1254
839
  }
1255
840
  /**
1256
- * Read the content field at `addr` as canonical `Content`: the twin of
1257
- * {@link get}, which projects. An absent `addr.field` reads the body
1258
- * `Content`. `undefined` for an absent field; throws `UnknownField`,
1259
- * `FieldNotContent` for a type that is not a content leaf, `FieldDecode` for
1260
- * an undecodable value, and `IndexOutOfRange` for a bad `addr.card`.
1261
841
  * @param {import('../core/wasm.js').Addr | string} addr
1262
842
  * @returns {import('../core/wasm.js').Content | undefined}
1263
843
  */
@@ -1265,13 +845,6 @@ export class DocumentReader {
1265
845
  return this.#doc._readerGetContent(this.#quill, addr);
1266
846
  }
1267
847
  /**
1268
- * Read the `Content` nested inside the composite field at `addr`, at `path`:
1269
- * `[0]` an `array<richtext>` element, `["motto"]` an object's content property,
1270
- * `[1, "notes"]` a leaf under both, `["controlled_by"]` a variant's cell. The
1271
- * codec is the leaf's declared type's.
1272
- * `undefined` for an absent field and for a path that names nothing stored;
1273
- * throws `UnknownField`, `FieldNotContent` when `path` resolves to no content
1274
- * leaf, `FieldDecode` anchored at the addressed path, and `IndexOutOfRange`.
1275
848
  * @param {import('../core/wasm.js').Addr | string} addr
1276
849
  * @param {import('../core/wasm.js').PathStep[]} path
1277
850
  * @returns {import('../core/wasm.js').Content | undefined}
@@ -1280,16 +853,18 @@ export class DocumentReader {
1280
853
  return this.#doc._readerGetContentAt(this.#quill, addr, path);
1281
854
  }
1282
855
  /**
1283
- * The main body's markdown: the quill-free body read. Equals `get({})`.
1284
856
  * @returns {string}
1285
857
  */
1286
858
  bodyMarkdown() {
1287
859
  return this.#doc._readerGet(this.#quill, {});
1288
860
  }
1289
861
  /**
1290
- * A {@link CardReader} bound to the composable card at `index`, checked lazily
1291
- * at the read. It holds `index`, not the card, so a `removeCard`/`addCard`
1292
- * between binding and reading silently retargets it.
862
+ * @returns {Resolved}
863
+ */
864
+ resolve() {
865
+ return this.#quill._resolve(this.#doc);
866
+ }
867
+ /**
1293
868
  * @param {number} index
1294
869
  * @returns {CardReader}
1295
870
  */
@@ -1298,11 +873,6 @@ export class DocumentReader {
1298
873
  }
1299
874
  }
1300
875
 
1301
- /**
1302
- * A single composable card bound to its {@link Quill} for typed reads, from
1303
- * {@link DocumentReader.card}. Same `get` / `bodyMarkdown` verbs as
1304
- * {@link DocumentReader}, reading the card at its bound index.
1305
- */
1306
876
  export class CardReader {
1307
877
  #quill;
1308
878
  #doc;
@@ -1319,22 +889,16 @@ export class CardReader {
1319
889
  this.#doc = doc;
1320
890
  this.#index = index;
1321
891
  }
1322
- /** The bound card index. */
1323
892
  get index() {
1324
893
  return this.#index;
1325
894
  }
1326
895
  /**
1327
- * The bound card's `$kind` (empty string when it carries none), read through
1328
- * the document. Throws `IndexOutOfRange` if the bound index is out of range.
1329
896
  * @returns {string}
1330
897
  */
1331
898
  get kind() {
1332
899
  return this.#doc.card(this.#index).kind;
1333
900
  }
1334
901
  /**
1335
- * Read the field `name` on this card, interpreted by its declared type,
1336
- * addressed at `{ card, field }`. `undefined` when absent; throws
1337
- * `UnknownField` for an undeclared name and `IndexOutOfRange` for a bad index.
1338
902
  * @param {string} name
1339
903
  * @returns {unknown}
1340
904
  */
@@ -1342,7 +906,6 @@ export class CardReader {
1342
906
  return this.#doc._readerGet(this.#quill, { card: this.#index, field: name });
1343
907
  }
1344
908
  /**
1345
- * The card twin of {@link DocumentReader.getContent}.
1346
909
  * @param {string} name
1347
910
  * @returns {import('../core/wasm.js').Content | undefined}
1348
911
  */
@@ -1350,7 +913,6 @@ export class CardReader {
1350
913
  return this.#doc._readerGetContent(this.#quill, { card: this.#index, field: name });
1351
914
  }
1352
915
  /**
1353
- * The card twin of {@link DocumentReader.getContentAt}.
1354
916
  * @param {string} name
1355
917
  * @param {import('../core/wasm.js').PathStep[]} path
1356
918
  * @returns {import('../core/wasm.js').Content | undefined}
@@ -1359,7 +921,6 @@ export class CardReader {
1359
921
  return this.#doc._readerGetContentAt(this.#quill, { card: this.#index, field: name }, path);
1360
922
  }
1361
923
  /**
1362
- * The card twin of {@link DocumentReader.bodyMarkdown}.
1363
924
  * @returns {string}
1364
925
  */
1365
926
  bodyMarkdown() {
@@ -1367,11 +928,8 @@ export class CardReader {
1367
928
  }
1368
929
  }
1369
930
 
1370
- // ── `quill.reader(doc)`: the schema-plane read front door ─────────────────────
1371
931
  // Patched onto the same re-exported `Quill` prototype as `writer`.
1372
932
  /**
1373
- * A {@link DocumentReader} binding this quill's schema to `doc`. It holds both
1374
- * handles by reference and owns neither: bind, read, discard.
1375
933
  * @this {Quill}
1376
934
  * @param {Document} doc the document to read, held by reference (not owned)
1377
935
  * @returns {DocumentReader}