@quillmark/wasm 0.112.0 → 0.114.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
@@ -41,34 +20,13 @@ import initCore, { Quill, Document } from '../core/wasm.js';
41
20
  // Resolution-time, so `node:fs` never enters a browser graph.
42
21
  import { toModuleSource } from '#quillmark-env';
43
22
  import { importMarkdown, exportMarkdown, rebase, mapPos, mapMarks } from '../core/wasm.js';
44
- import { parseDocPath, formatDocPath } from '../core/wasm.js';
23
+ import { parseDocPath, formatDocPath, formatDiagnostic } 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
@@ -92,7 +50,8 @@ const CORE_SURFACE = Object.freeze({
92
50
  mapPos,
93
51
  mapMarks,
94
52
  parseDocPath,
95
- formatDocPath
53
+ formatDocPath,
54
+ formatDiagnostic
96
55
  });
97
56
 
98
57
  /** The in-flight or settled core instantiation, resolving to `CORE_SURFACE`.
@@ -104,38 +63,8 @@ let coreInit;
104
63
  let coreInitSource;
105
64
 
106
65
  /**
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
66
+ * @param {import('../core/wasm.js').InitInput} [source]
67
+ * @returns {Promise<import('./runtime.js').CoreSurface>}
139
68
  */
140
69
  export function init(source) {
141
70
  if (coreInit) {
@@ -195,46 +124,17 @@ async function instantiateCore(source) {
195
124
  }
196
125
  }
197
126
 
198
- // ── The main-card address ───────────────────────────────────────────────────
199
127
  /**
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
128
  * @type {import('../core/wasm.js').CardAddr}
207
129
  */
208
130
  export const MAIN_CARD_ADDR = Object.freeze({});
209
131
 
210
- // ── The variant discriminant key ────────────────────────────────────────────
211
132
  /**
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
133
  * @type {'value'}
224
134
  */
225
135
  export const VARIANT_DISCRIMINANT_KEY = 'value';
226
136
 
227
137
  /**
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
138
  * @param {unknown} e
239
139
  * @returns {e is Error & { diagnostics: import('../core/wasm.js').Diagnostic[] }}
240
140
  */
@@ -258,62 +158,35 @@ function quillmarkError(code, message, hint) {
258
158
  return err;
259
159
  }
260
160
 
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.
161
+ // These checks deliver the ERROR, not the rejection, at the seams that cross
162
+ // into backend memory as DATA `Engine` and `LiveSession.update` where a
163
+ // foreign handle would otherwise work silently at the price of a whole-document
164
+ // round trip and a per-copy split of the quill clone cache, and at the four
165
+ // writer/reader binds, whose constructors hold both handles. A method declaring
166
+ // a reference parameter needs none of this: wasm-bindgen's own `_assertClass`
167
+ // refuses a foreign class there.
283
168
 
284
- /** Per class: the code and hint for "not one at all", and the from-another-copy probe. */
169
+ /** Per class: the diagnostic for a value that is not one of THIS copy's handles. */
285
170
  const HANDLE_KINDS = {
286
171
  Quill: {
287
172
  code: 'runtime::not_a_quill',
288
- probe: 'toTree',
289
- hint: 'Pass a Quill built by Quill.fromTree.'
173
+ 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
174
  },
291
175
  Document: {
292
176
  code: 'runtime::not_a_document',
293
- probe: 'toJson',
294
- hint: 'Pass a Document built by Document.fromMarkdown / fromJson or quill.seedDocument.'
177
+ 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
178
  }
296
179
  };
297
180
 
298
181
  /**
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).
182
+ * The rejection for a value that is not one of this copy's handles.
303
183
  * @param {unknown} value
304
184
  * @param {string} method
305
185
  * @param {'Quill' | 'Document'} className
306
186
  * @returns {Error & { diagnostics: import('../core/wasm.js').Diagnostic[] }}
307
187
  */
308
188
  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
- }
189
+ const { code, hint } = HANDLE_KINDS[className];
317
190
  return quillmarkError(
318
191
  code,
319
192
  `${method}: expected a ${className}, got ${value === null ? 'null' : typeof value}.`,
@@ -343,213 +216,26 @@ function requireLocalQuill(quill, method) {
343
216
  throw notLocal(quill, method, 'Quill');
344
217
  }
345
218
 
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
219
  // WELD_KEYS is the rule `Container::same_weld` owns upstream: which `attrs`
521
220
  // 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.
221
+ // as one. `start` is not among them, since CommonMark reads only a list's first
222
+ // number a subset, which is why every container needs its own entry. A table
223
+ // rather than a switch, so `tests/known_names_drift.rs` can pin it against the
224
+ // Rust predicate.
527
225
 
528
226
  const WELD_KEYS = { list_item: ['ordered'], quote: [] };
529
227
 
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
228
  /**
542
229
  * @param {import('../core/wasm.js').ContentContainer} a
543
230
  * @param {import('../core/wasm.js').ContentContainer} b
544
231
  * @returns {boolean}
545
232
  */
546
233
  function weldsWith(a, b) {
547
- // A malformed value welds with nothing. The membership guards' posture:
548
- // answer rather than throw.
234
+ // A malformed value welds with nothing: answer rather than throw.
549
235
  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);
236
+ // `hasOwn`, so a tag colliding with an `Object.prototype` member answers
237
+ // `false` rather than reaching a function.
238
+ if (!Object.hasOwn(WELD_KEYS, a.container)) return false;
553
239
  return WELD_KEYS[a.container].every((k) => a.attrs?.[k] === b.attrs?.[k]);
554
240
  }
555
241
 
@@ -579,7 +265,7 @@ export function assignInstances(runs) {
579
265
  * generated entry's own `wasm !== undefined` guard only catches a call arriving
580
266
  * after one finished, not one already in flight.
581
267
  *
582
- * @param {string} id backend id, for the failure message
268
+ * @param {string} id the build's name, for the failure message
583
269
  * @param {() => Promise<any>} importThunk the dynamic `import()`
584
270
  * @param {() => URL} wasmUrl the build's binary, resolved at call time
585
271
  * @returns {() => Promise<any>} resolves to a ready-to-use module
@@ -598,108 +284,89 @@ function backendLoad(id, importThunk, wasmUrl) {
598
284
  throw Object.assign(
599
285
  quillmarkError(
600
286
  'runtime::backend_load_failed',
601
- `Engine: could not load the '${id}' backend: ${
287
+ `Engine: could not load the '${id}' build: ${
602
288
  /** @type {any} */ (cause)?.message ?? cause
603
289
  }`,
604
- 'The backend binary ships beside the package files; check the network tab for a 404 or an HTML response.'
290
+ 'The build ships beside the package files; check the network tab for a 404 or an HTML response.'
605
291
  ),
606
292
  { cause }
607
293
  );
608
294
  }));
609
295
  }
610
296
 
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.
297
+ // The render build is NEVER statically imported here: that would pull a
298
+ // multi-MB binary into the eager graph and defeat lazy loading. Both built-in
299
+ // backends ship in it, so they share one `load`; each `formats` manifest
300
+ // mirrors that backend's Rust `SUPPORTED_FORMATS`, pinned by a
301
+ // `runtime.test.js` drift guard that renders once and compares.
302
+ const RENDER_BUILD = backendLoad(
303
+ 'render',
304
+ () => import('../render/wasm.js'),
305
+ () => new URL('../render/wasm_bg.wasm', import.meta.url)
306
+ );
620
307
  const DEFAULT_BACKENDS = {
621
308
  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
309
+ load: RENDER_BUILD,
310
+ formats: ['pdf', 'svg', 'png'] // crates/backends/typst/src/lib.rs SUPPORTED_FORMATS
629
311
  },
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
312
+ acroform: {
313
+ load: RENDER_BUILD,
314
+ formats: ['pdf'] // crates/backends/acroform/src/lib.rs SUPPORTED_FORMATS
639
315
  }
640
316
  };
641
317
 
642
318
  /**
643
319
  * Validate a backend registry descriptor, naming the backend id on any
644
320
  * malformed entry. Failing at construction rather than deep inside a render is
645
- * what lets the capability probes answer from the manifest unconditionally.
321
+ * what lets `supportedFormats` answer from the manifest unconditionally.
646
322
  * @param {string} id
647
323
  * @param {unknown} entry
648
- * @returns {{ load: () => Promise<unknown>, formats: string[], canvas: boolean }}
324
+ * @returns {{ load: () => Promise<unknown>, formats: string[] }}
649
325
  */
650
326
  function validateBackend(id, entry) {
651
327
  if (!entry || typeof entry !== 'object') {
652
- throw new Error(
653
- `Engine: backend '${id}' must be a descriptor { load, formats, canvas }.`
654
- );
328
+ throw new Error(`Engine: backend '${id}' must be a descriptor { load, formats }.`);
655
329
  }
656
- const { load, formats, canvas } = /** @type {any} */ (entry);
330
+ const { load, formats } = /** @type {any} */ (entry);
657
331
  if (typeof load !== 'function') {
658
332
  throw new Error(`Engine: backend '${id}' descriptor needs a callable 'load'.`);
659
333
  }
660
334
  if (!Array.isArray(formats)) {
661
335
  throw new Error(`Engine: backend '${id}' descriptor needs a 'formats' array.`);
662
336
  }
663
- if (typeof canvas !== 'boolean') {
664
- throw new Error(`Engine: backend '${id}' descriptor needs a boolean 'canvas'.`);
665
- }
666
- return { load, formats, canvas };
337
+ return { load, formats };
667
338
  }
668
339
 
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
340
  export class Engine {
675
- /** backendId → Promise<backend module>, memoized so each build loads once. */
341
+ /**
342
+ * The three caches below key on a descriptor's `load` thunk, the build's
343
+ * identity: two backend ids sharing one build (the built-ins) share one
344
+ * module, one engine and one clone cache, and a consumer's override build
345
+ * gets its own.
346
+ */
347
+ /** load → Promise<build module>, memoized so each build loads once. */
676
348
  #modules = new Map();
677
- /** backendId → that backend's engine instance (the WASM backend registry). */
349
+ /** load → that build's engine instance (the WASM backend registry). */
678
350
  #engines = new Map();
679
- /** backendId → descriptor `{ load, formats, canvas }`. */
351
+ /** backendId → descriptor `{ load, formats }`. */
680
352
  #loaders;
681
353
  /**
682
- * backendId → WeakMap<canonical Quill, backend-memory clone>, caching the
354
+ * load → WeakMap<canonical Quill, build-memory clone>, caching the
683
355
  * expensive materialization. WeakMap so dropping the canonical quill makes
684
356
  * its clone collectable, and wasm-bindgen weak-refs then free the handle.
685
- * @type {Map<string, WeakMap<object, any>>}
357
+ * @type {Map<() => Promise<unknown>, WeakMap<object, any>>}
686
358
  */
687
359
  #quillClones = new Map();
688
360
 
689
361
  /**
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).
362
+ * `load` must resolve to a READY module: a registrant shipping its own
363
+ * `--target web` build instantiates inside the thunk, and more than one
364
+ * `Engine` may call it, so memoize (the built-ins do, at module scope).
365
+ * @param {{ backends?: Record<string, { load: () => Promise<unknown>, formats: string[] }> }} [options]
699
366
  */
700
367
  constructor(options) {
701
368
  const merged = { ...DEFAULT_BACKENDS, ...(options?.backends ?? {}) };
702
- /** @type {Record<string, { load: () => Promise<unknown>, formats: string[], canvas: boolean }>} */
369
+ /** @type {Record<string, { load: () => Promise<unknown>, formats: string[] }>} */
703
370
  const loaders = {};
704
371
  for (const [id, entry] of Object.entries(merged)) {
705
372
  loaders[id] = validateBackend(id, entry);
@@ -708,17 +375,19 @@ export class Engine {
708
375
  }
709
376
 
710
377
  /**
711
- * The registered descriptor for `backendId`, or the "no backend registered"
712
- * throw. Touches no binary.
378
+ * The registered descriptor for `backendId`, or the
379
+ * `engine::backend_not_found` throw core raises for the same condition.
380
+ * Touches no binary.
713
381
  * @param {string} backendId
714
- * @returns {{ load: () => Promise<unknown>, formats: string[], canvas: boolean }}
382
+ * @returns {{ load: () => Promise<unknown>, formats: string[] }}
715
383
  */
716
384
  #descriptorFor(backendId) {
717
385
  const descriptor = this.#loaders[backendId];
718
386
  if (!descriptor) {
719
- throw new Error(
720
- `Engine: no backend registered for '${backendId}'. ` +
721
- `Known backends: ${Object.keys(this.#loaders).join(', ') || '(none)'}.`
387
+ throw quillmarkError(
388
+ 'engine::backend_not_found',
389
+ `Engine: backend '${backendId}' not registered or not enabled.`,
390
+ `Available backends: ${Object.keys(this.#loaders).join(', ') || '(none)'}`
722
391
  );
723
392
  }
724
393
  return descriptor;
@@ -738,51 +407,52 @@ export class Engine {
738
407
  }
739
408
 
740
409
  /**
741
- * Resolve (and lazily load) the backend module + its engine for `backendId`.
410
+ * Resolve (and lazily load) the build module + its engine for `backendId`.
742
411
  * @param {string} backendId
743
412
  * @returns {Promise<{ mod: any, engine: any }>}
744
413
  */
745
414
  async #resolveBackend(backendId) {
746
- const descriptor = this.#descriptorFor(backendId);
415
+ const { load } = this.#descriptorFor(backendId);
747
416
 
748
- let modPromise = this.#modules.get(backendId);
417
+ let modPromise = this.#modules.get(load);
749
418
  if (!modPromise) {
750
419
  // Set the promise synchronously (before any await) so concurrent first
751
420
  // renders share ONE import. Self-heal on failure so a transient load
752
421
  // error doesn't poison every later attempt.
753
422
  modPromise = Promise.resolve()
754
- .then(descriptor.load)
423
+ .then(load)
755
424
  .catch((err) => {
756
- this.#modules.delete(backendId);
425
+ this.#modules.delete(load);
757
426
  throw err;
758
427
  });
759
- this.#modules.set(backendId, modPromise);
428
+ this.#modules.set(load, modPromise);
760
429
  }
761
430
  const mod = await modPromise;
762
431
 
763
- let engine = this.#engines.get(backendId);
432
+ let engine = this.#engines.get(load);
764
433
  if (!engine) {
765
434
  engine = new mod.Quillmark();
766
- this.#engines.set(backendId, engine);
435
+ this.#engines.set(load, engine);
767
436
  }
768
437
  return { mod, engine };
769
438
  }
770
439
 
771
440
  /**
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
441
+ * Get (or materialize-and-cache) the build-memory `Quill` clone for `quill`
442
+ * under `backendId`'s build. On a miss the clone is built from `tree`, the
443
+ * caller's pre-await snapshot, since the canonical handle may be freed by now.
444
+ * @param {any} mod the build module
776
445
  * @param {string} backendId
777
446
  * @param {object} quill the canonical instance (cache key only)
778
447
  * @param {Map<string, Uint8Array> | null} tree pre-await snapshot; `null` on a cache hit
779
- * @returns {any} the backend-memory quill clone
448
+ * @returns {any} the build-memory quill clone
780
449
  */
781
450
  #cachedQuillClone(mod, backendId, quill, tree) {
782
- let perQuill = this.#quillClones.get(backendId);
451
+ const { load } = this.#descriptorFor(backendId);
452
+ let perQuill = this.#quillClones.get(load);
783
453
  if (!perQuill) {
784
454
  perQuill = new WeakMap();
785
- this.#quillClones.set(backendId, perQuill);
455
+ this.#quillClones.set(load, perQuill);
786
456
  }
787
457
  let backendQuill = perQuill.get(quill);
788
458
  if (!backendQuill) {
@@ -797,11 +467,15 @@ export class Engine {
797
467
  * memory and run `fn` against the backend engine. Only `render`/`open` call
798
468
  * this, so `doc` is always present.
799
469
  *
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").
470
+ * OWNERSHIP WINDOW: both caller handles are snapshotted (`doc.toStored()` and
471
+ * `doc.warnings`, and `quill.toTree()` on a clone-cache miss) BEFORE the first
472
+ * await. The backend load below is a real suspension point, so reading the
473
+ * handles after it would race a caller that `free()`s them as soon as this
474
+ * call returns its promise ("null pointer passed to rust").
475
+ *
476
+ * `docWarnings` rides the context because the storage DTO does not carry
477
+ * them: `fromStored` clears the load's warnings, so the backend clone knows
478
+ * nothing of them and `render` splices the snapshot back in.
805
479
  *
806
480
  * Clone lifetimes differ by design: the `doc` clone is TRANSIENT, freed in
807
481
  * the `finally` of every call, while the `quill` clone is CACHED and is not
@@ -811,13 +485,16 @@ export class Engine {
811
485
  * @param {string} method the caller's name, for the rejection message
812
486
  * @param {Quill} quill
813
487
  * @param {Document} doc
814
- * @param {(ctx: { mod: any, engine: any, quill: any, doc: any }) => any} fn
488
+ * @param {(ctx: { mod: any, engine: any, quill: any, doc: any, docWarnings: any[] }) => any} fn
815
489
  */
816
490
  async #withClones(method, quill, doc, fn) {
817
491
  const backendId = this.#backendOf(quill, method);
818
492
  requireLocalDoc(doc, method);
819
- const docJson = doc.toJson();
820
- const quillTree = this.#quillClones.get(backendId)?.has(quill) ? null : quill.toTree();
493
+ const docJson = doc.toStored();
494
+ const docWarnings = doc.warnings;
495
+ const quillTree = this.#quillClones.get(this.#descriptorFor(backendId).load)?.has(quill)
496
+ ? null
497
+ : quill.toTree();
821
498
  const { mod, engine } = await this.#resolveBackend(backendId);
822
499
  // The doc clone and `fn` share one try so the clone is freed even if a
823
500
  // later step throws; the cached quill clone is intentionally not freed.
@@ -825,31 +502,33 @@ export class Engine {
825
502
  const backendQuill = this.#cachedQuillClone(mod, backendId, quill, quillTree);
826
503
  let backendDoc = null;
827
504
  try {
828
- backendDoc = mod.Document.fromJson(docJson);
829
- return fn({ mod, engine, quill: backendQuill, doc: backendDoc });
505
+ backendDoc = mod.Document.fromStored(docJson);
506
+ return fn({ mod, engine, quill: backendQuill, doc: backendDoc, docWarnings });
830
507
  } finally {
831
508
  backendDoc?.free();
832
509
  }
833
510
  }
834
511
 
835
512
  /**
836
- * Render `doc` against `quill` in one shot. Both handles are read
837
- * synchronously before the first await.
838
513
  * @param {Quill} quill
839
514
  * @param {Document} doc
840
- * @param {object} [options] render options (`{ format, ppi, pages, producer }`)
515
+ * @param {object} [options] render options (`{ format, ppi, pages, regions }`)
841
516
  * @returns {Promise<import('./runtime.js').RenderResult>}
842
517
  */
843
518
  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)
519
+ return this.#withClones(
520
+ 'engine.render(quill, doc)',
521
+ quill,
522
+ doc,
523
+ ({ engine, quill: q, doc: d, docWarnings }) => {
524
+ const result = engine.render(q, d, options ?? undefined);
525
+ result.warnings = docWarnings.concat(result.warnings);
526
+ return result;
527
+ }
846
528
  );
847
529
  }
848
530
 
849
531
  /**
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
532
  * @param {Quill} quill
854
533
  * @param {Document} doc
855
534
  * @returns {Promise<LiveSession>}
@@ -864,8 +543,6 @@ export class Engine {
864
543
  }
865
544
 
866
545
  /**
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
546
  * @param {Quill} quill
870
547
  * @returns {Promise<import('./runtime.js').OutputFormat[]>}
871
548
  */
@@ -874,29 +551,17 @@ export class Engine {
874
551
  // Defensive copy so callers can't mutate the shared manifest.
875
552
  return descriptor.formats.slice();
876
553
  }
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
554
  }
890
555
 
891
556
  /**
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.
557
+ * Thin wrapper over a backend's live render session. The quill/document clones
558
+ * it was opened from have already been freed: the session retains what `update`
559
+ * needs.
895
560
  */
896
561
  export class LiveSession {
897
562
  /**
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
563
+ * @param {object} inner the render build's LiveSession, whose members the delegations below name
564
+ * @param {{ Document: { fromStored(json: string): any } }} mod the session's backend build, used to materialize `update` documents in its linear memory
900
565
  */
901
566
  constructor(inner, mod) {
902
567
  this.#inner = inner;
@@ -913,7 +578,7 @@ export class LiveSession {
913
578
  requireLocalDoc(doc, 'session.update(doc)');
914
579
  let backendDoc = null;
915
580
  try {
916
- backendDoc = this.#mod.Document.fromJson(doc.toJson());
581
+ backendDoc = this.#mod.Document.fromStored(doc.toStored());
917
582
  return this.#inner.update(backendDoc);
918
583
  } finally {
919
584
  backendDoc?.free();
@@ -926,15 +591,6 @@ export class LiveSession {
926
591
  get backendId() {
927
592
  return this.#inner.backendId;
928
593
  }
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
594
  get warnings() {
939
595
  return this.#inner.warnings;
940
596
  }
@@ -1009,28 +665,11 @@ export class LiveSession {
1009
665
  }
1010
666
  }
1011
667
 
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.
668
+ // These pure-JS classes bind a `quill` + `doc` pair once and delegate to the
669
+ // `_commit*` / `_reader*` verbs, which take the quill per call. They hold JS
670
+ // references to the caller's existing handles, so there is no WASM object of
671
+ // their own, no `free()` burden, and no second owner of either handle.
1027
672
 
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
673
  export class DocumentWriter {
1035
674
  #quill;
1036
675
  #doc;
@@ -1044,13 +683,10 @@ export class DocumentWriter {
1044
683
  this.#quill = quill;
1045
684
  this.#doc = doc;
1046
685
  }
1047
- /** The bound document: the same instance passed in, mutated in place. */
1048
686
  get document() {
1049
687
  return this.#doc;
1050
688
  }
1051
689
  /**
1052
- * Typed-commit one main-card field (strict coerce, mismatch throws now).
1053
- * Throws `UnknownField` for a name the schema does not declare.
1054
690
  * @param {string} name
1055
691
  * @param {unknown} value
1056
692
  * @returns {void}
@@ -1059,9 +695,6 @@ export class DocumentWriter {
1059
695
  return this.#doc._commitField(this.#quill, name, value);
1060
696
  }
1061
697
  /**
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
698
  * @param {Record<string, unknown>} fields
1066
699
  * @returns {void}
1067
700
  */
@@ -1069,8 +702,6 @@ export class DocumentWriter {
1069
702
  return this.#doc._commitFields(this.#quill, MAIN_CARD_ADDR, fields);
1070
703
  }
1071
704
  /**
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
705
  * @param {string} markdown
1075
706
  * @returns {import('../core/wasm.js').Delta}
1076
707
  */
@@ -1078,10 +709,6 @@ export class DocumentWriter {
1078
709
  return this.#doc.revise({}, markdown);
1079
710
  }
1080
711
  /**
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
712
  * @param {string} name
1086
713
  * @param {string} text
1087
714
  * @returns {import('../core/wasm.js').Delta}
@@ -1090,10 +717,6 @@ export class DocumentWriter {
1090
717
  return this.#doc._reviseField(this.#quill, name, text);
1091
718
  }
1092
719
  /**
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
720
  * @param {string} kind
1098
721
  * @param {Record<string, unknown>} [fields]
1099
722
  * @param {string} [body]
@@ -1111,9 +734,6 @@ export class DocumentWriter {
1111
734
  return this.#doc.removeCard(index);
1112
735
  }
1113
736
  /**
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
737
  * @param {number} index
1118
738
  * @returns {CardWriter}
1119
739
  */
@@ -1122,11 +742,6 @@ export class DocumentWriter {
1122
742
  }
1123
743
  }
1124
744
 
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
745
  export class CardWriter {
1131
746
  #quill;
1132
747
  #doc;
@@ -1143,22 +758,16 @@ export class CardWriter {
1143
758
  this.#doc = doc;
1144
759
  this.#index = index;
1145
760
  }
1146
- /** The bound card index. */
1147
761
  get index() {
1148
762
  return this.#index;
1149
763
  }
1150
764
  /**
1151
- * The bound card's `$kind`, empty string when it carries none. Throws
1152
- * `IndexOutOfRange` for a bad bound index.
1153
765
  * @returns {string}
1154
766
  */
1155
767
  get kind() {
1156
768
  return this.#doc.card(this.#index).kind;
1157
769
  }
1158
770
  /**
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
771
  * @param {string} name
1163
772
  * @param {unknown} value
1164
773
  * @returns {void}
@@ -1167,9 +776,6 @@ export class CardWriter {
1167
776
  return this.#doc._commitField(this.#quill, { card: this.#index, field: name }, value);
1168
777
  }
1169
778
  /**
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
779
  * @param {Record<string, unknown>} fields
1174
780
  * @returns {void}
1175
781
  */
@@ -1177,7 +783,6 @@ export class CardWriter {
1177
783
  return this.#doc._commitFields(this.#quill, { card: this.#index }, fields);
1178
784
  }
1179
785
  /**
1180
- * The card twin of {@link DocumentWriter.reviseBody}.
1181
786
  * @param {string} markdown
1182
787
  * @returns {import('../core/wasm.js').Delta}
1183
788
  */
@@ -1185,8 +790,6 @@ export class CardWriter {
1185
790
  return this.#doc.revise({ card: this.#index }, markdown);
1186
791
  }
1187
792
  /**
1188
- * The card twin of {@link DocumentWriter.reviseField}. Throws `UnknownField`
1189
- * for an undeclared name and `IndexOutOfRange` for a bad bound index.
1190
793
  * @param {string} name
1191
794
  * @param {string} text
1192
795
  * @returns {import('../core/wasm.js').Delta}
@@ -1196,13 +799,10 @@ export class CardWriter {
1196
799
  }
1197
800
  }
1198
801
 
1199
- // ── `quill.writer(doc)`, the typed front door ──────────────────────────────
1200
802
  // Patched onto the re-exported `Quill` prototype rather than wrapped, so
1201
803
  // `Quill === CoreQuill` stays true: this only adds a method constructing the
1202
804
  // pure-JS writer, which owns no WASM handle.
1203
805
  /**
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
806
  * @this {Quill}
1207
807
  * @param {Document} doc the document to mutate, held by reference (not owned)
1208
808
  * @returns {DocumentWriter}
@@ -1211,17 +811,10 @@ Quill.prototype.writer = function writer(doc) {
1211
811
  return new DocumentWriter(this, doc);
1212
812
  };
1213
813
 
1214
- // ── Typed-reader sugar: the schema-plane read surface ─────────────────────────
1215
814
  // The transport `Document.getStored` is schema-free, so an unknown field name
1216
815
  // 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.
816
+ // schema lets one `get` interpret by declared type and throw `UnknownField`.
1219
817
 
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
818
  export class DocumentReader {
1226
819
  #quill;
1227
820
  #doc;
@@ -1235,17 +828,10 @@ export class DocumentReader {
1235
828
  this.#quill = quill;
1236
829
  this.#doc = doc;
1237
830
  }
1238
- /** The bound document: the same instance passed in. */
1239
831
  get document() {
1240
832
  return this.#doc;
1241
833
  }
1242
834
  /**
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
835
  * @param {import('../core/wasm.js').Addr | string} addr
1250
836
  * @returns {unknown}
1251
837
  */
@@ -1253,11 +839,6 @@ export class DocumentReader {
1253
839
  return this.#doc._readerGet(this.#quill, addr);
1254
840
  }
1255
841
  /**
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
842
  * @param {import('../core/wasm.js').Addr | string} addr
1262
843
  * @returns {import('../core/wasm.js').Content | undefined}
1263
844
  */
@@ -1265,13 +846,6 @@ export class DocumentReader {
1265
846
  return this.#doc._readerGetContent(this.#quill, addr);
1266
847
  }
1267
848
  /**
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
849
  * @param {import('../core/wasm.js').Addr | string} addr
1276
850
  * @param {import('../core/wasm.js').PathStep[]} path
1277
851
  * @returns {import('../core/wasm.js').Content | undefined}
@@ -1280,16 +854,18 @@ export class DocumentReader {
1280
854
  return this.#doc._readerGetContentAt(this.#quill, addr, path);
1281
855
  }
1282
856
  /**
1283
- * The main body's markdown: the quill-free body read. Equals `get({})`.
1284
857
  * @returns {string}
1285
858
  */
1286
859
  bodyMarkdown() {
1287
860
  return this.#doc._readerGet(this.#quill, {});
1288
861
  }
1289
862
  /**
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.
863
+ * @returns {Resolved}
864
+ */
865
+ resolve() {
866
+ return this.#quill._resolve(this.#doc);
867
+ }
868
+ /**
1293
869
  * @param {number} index
1294
870
  * @returns {CardReader}
1295
871
  */
@@ -1298,11 +874,6 @@ export class DocumentReader {
1298
874
  }
1299
875
  }
1300
876
 
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
877
  export class CardReader {
1307
878
  #quill;
1308
879
  #doc;
@@ -1319,22 +890,16 @@ export class CardReader {
1319
890
  this.#doc = doc;
1320
891
  this.#index = index;
1321
892
  }
1322
- /** The bound card index. */
1323
893
  get index() {
1324
894
  return this.#index;
1325
895
  }
1326
896
  /**
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
897
  * @returns {string}
1330
898
  */
1331
899
  get kind() {
1332
900
  return this.#doc.card(this.#index).kind;
1333
901
  }
1334
902
  /**
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
903
  * @param {string} name
1339
904
  * @returns {unknown}
1340
905
  */
@@ -1342,7 +907,6 @@ export class CardReader {
1342
907
  return this.#doc._readerGet(this.#quill, { card: this.#index, field: name });
1343
908
  }
1344
909
  /**
1345
- * The card twin of {@link DocumentReader.getContent}.
1346
910
  * @param {string} name
1347
911
  * @returns {import('../core/wasm.js').Content | undefined}
1348
912
  */
@@ -1350,7 +914,6 @@ export class CardReader {
1350
914
  return this.#doc._readerGetContent(this.#quill, { card: this.#index, field: name });
1351
915
  }
1352
916
  /**
1353
- * The card twin of {@link DocumentReader.getContentAt}.
1354
917
  * @param {string} name
1355
918
  * @param {import('../core/wasm.js').PathStep[]} path
1356
919
  * @returns {import('../core/wasm.js').Content | undefined}
@@ -1359,7 +922,6 @@ export class CardReader {
1359
922
  return this.#doc._readerGetContentAt(this.#quill, { card: this.#index, field: name }, path);
1360
923
  }
1361
924
  /**
1362
- * The card twin of {@link DocumentReader.bodyMarkdown}.
1363
925
  * @returns {string}
1364
926
  */
1365
927
  bodyMarkdown() {
@@ -1367,11 +929,8 @@ export class CardReader {
1367
929
  }
1368
930
  }
1369
931
 
1370
- // ── `quill.reader(doc)`: the schema-plane read front door ─────────────────────
1371
932
  // Patched onto the same re-exported `Quill` prototype as `writer`.
1372
933
  /**
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
934
  * @this {Quill}
1376
935
  * @param {Document} doc the document to read, held by reference (not owned)
1377
936
  * @returns {DocumentReader}