@quillmark/wasm 0.103.0 → 0.105.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.
@@ -2,115 +2,73 @@
2
2
  //
3
3
  // @quillmark/wasm/runtime: the canonical consumer API.
4
4
  //
5
- // Consumers reach `Quill` and `Document` through `await init()` and `Engine`
6
- // as a static export, and never touch the build-specific subpaths. The package
7
- // ships multiple WASM binaries with SEPARATE linear memories: a Typst-less
8
- // `core` build (small, eager) that is the canonical home of `Quill`/`Document`,
9
- // and one private backend binary per backend (`backends/typst/` today; more
10
- // later) that carries an engine. A handle from one memory cannot be used by
11
- // another. This module hides that seam and is exposed at the package root
12
- // (`@quillmark/wasm`):
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`.
13
10
  //
14
11
  // - `Quill` and `Document` ARE the core build's classes, handed out by the
15
- // gate "Initialization"). They
16
- // hold the canonical data and the full sync surface (schema / validate /
17
- // seed / mutate / toJson / toTree). No backend is loaded to use them, so
18
- // the editor/validation path never pays for a multi-MB backend binary.
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.
19
17
  //
20
18
  // - `Engine` is the render dispatcher. It routes on `quill.backendId`, lazily
21
- // imports that backend's build (so a consumer that never renders never
22
- // loads it), clones the canonical `Quill`/`Document` into the backend's
23
- // memory ON DEMAND as data (`toTree`→`fromTree`, `toJson`→`fromJson`),
24
- // renders, and the backend handles never escape.
25
- //
26
- // CLONE LIFETIMES (not all transient): the per-call `Document` clone IS
27
- // transient, built and freed inside each call, because documents are small
28
- // and mutate freely. The `Quill` clone is CACHED instead: re-cloning a quill
29
- // per call means re-serializing its whole file tree Rust→JS, copying it into
30
- // backend memory, and re-parsing + re-validating the bundle every time, and
31
- // quills are validated, effectively-immutable bundles. So each `Engine`
32
- // memoizes the backend-memory quill per (engine, backendId, canonical quill
33
- // instance) in a `WeakMap` keyed on the canonical `Quill`: when the consumer
34
- // drops the core quill the cache entry becomes collectable and wasm-bindgen
35
- // weak-refs (`--weak-refs`) free the backend handle. The CONTRACT this buys:
36
- // a `Quill` instance's contents never change after construction, mutate by
37
- // replacing the instance (the clone is dropped with it via WeakMap +
38
- // weak-refs).
39
- //
40
- // The cross-memory crossing is therefore invisible: a consumer hands canonical
41
- // `Quill`/`Document` to `engine.render(...)` and gets a `RenderResult` back.
42
-
43
- // ── CANONICAL INVARIANT: hand out the core build's classes, never wrap ──────
44
- // The `Quill`/`Document` a consumer holds ARE the core build's classes, NOT
45
- // subclasses or wrappers. `init` resolves to them (§ "Initialization"); which
46
- // door they come through changes nothing about the identity. The only boundary
47
- // that needs crossing is core→backend (a separate WASM memory), which `Engine`
48
- // does internally as data (`toTree`/`toJson`).
49
- //
50
- // Do NOT hand out a wrapper: that breaks the identity and turns a structural
51
- // fact into a converted type (a breaking design change, not a refactor). The
52
- // `runtime.test.js` "hands out the internal core build classes verbatim" case
53
- // (`Quill === CoreQuill`) is the executable guard for this invariant.
54
- //
55
- // The identity is what makes `instanceof` the whole membership test: a handle
56
- // either belongs to this copy's classes or it belongs to another copy, and the
57
- // second is always a consumer bug. `Engine` is NOT duck-typed on its inputs; it
58
- // checks them. See § "Handles from another copy" below.
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.
59
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.
32
+
60
33
  // Local bindings, so this module can augment them: `quill.writer(doc)` is
61
- // patched onto the prototype below, and `instanceof` reads them directly.
62
- //
63
- // The default import is the core build's generated instantiation entry
64
- // (`--target web`); `init` below is the only thing that calls it.
34
+ // patched onto the prototype below, and `instanceof` reads them directly. The
35
+ // default import is the core build's generated instantiation entry; `init` is
36
+ // the only thing that calls it.
65
37
  import initCore, { Quill, Document } from '../core/wasm.js';
66
38
  // The wasm byte source, resolved per environment by package.json's `imports`
67
39
  // map: a pass-through in a browser (the glue fetches and streams the URL
68
40
  // itself), a `node:fs` read under Node, whose `fetch` rejects `file:` URLs.
69
41
  // Resolution-time, so `node:fs` never enters a browser graph.
70
42
  import { toModuleSource } from '#quillmark-env';
71
- // The document-free content codec: `exportMarkdown(body)` (the on-demand
72
- // markdown projection), `importMarkdown`, and the position-mapping pair
73
- // (`rebase`, `mapPos`).
74
43
  import { importMarkdown, exportMarkdown, rebase, mapPos } from '../core/wasm.js';
75
- // The document-model path parser/serializer: `parseDocPath(str) => DocPathSeg[]`
76
- // and its inverse `formatDocPath`, so a consumer routes on `Diagnostic.path`
77
- // segments instead of reverse-engineering the grammar.
78
44
  import { parseDocPath, formatDocPath } from '../core/wasm.js';
79
45
 
80
46
  // ── Initialization ──────────────────────────────────────────────────────────
81
47
  // The builds are `--target web`: they export their classes synchronously but
82
48
  // carry no wasm instance until something instantiates them. This module owns
83
49
  // that for core, behind one awaited gate; `Engine` owns it for the backends,
84
- // inside their lazy load, so a consumer never initializes a backend by hand.
85
- //
86
- // THE GATE IS THE ONLY DOOR. `init` resolves to the core surface, and this
87
- // module exports none of it statically, so a handle is unobtainable without
88
- // having awaited: the precondition is structural rather than a convention the
89
- // caller has to know. package.json's `exports` map carries exactly one entry,
90
- // so there is no subpath around the gate either.
91
- //
92
- // The gate is the shape the lazy-backend idiom (§ DEFAULT_BACKENDS) takes when
93
- // the surface it guards cannot be async: `Quill.fromTree` and `seedDocument`
94
- // are sync and static, so there is nowhere to hide an await except in front.
50
+ // inside their lazy load.
95
51
  //
96
- // WHAT STAYS STATIC is what needs no instance. `MAIN_CARD_ADDR`, the open-set
97
- // guards and `isQuillmarkError` are pure JS over plain objects; gating them
98
- // would cost a consumer of one an await it has no use for.
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.
99
57
  //
100
- // The classes stay static too, and are gated by their ARGUMENTS. Every `Engine`
101
- // verb takes a `Quill` first (`#backendOf` is the single reader), and the
102
- // writer/reader constructors take both handles, so a caller who has not awaited
103
- // cannot produce an argument to call them with. `new Engine()` alone touches no
104
- // wasm: it validates a descriptor map. Holding them out of the gate keeps them
105
- // tree-shakable, so the editor path drops the dispatcher it never calls.
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.
106
67
  //
107
68
  // FAILURE DELIVERY follows the FUNCTION kind, not the failure kind: a sync verb
108
- // throws, a promise-returning verb rejects, and nothing does both. A
109
- // programming error reached through a promise-returning verb
110
- // (`runtime::foreign_handle` inside `Engine.render`) rejects like any other.
111
- // `init` is the one promise-returning export not declared `async`, because the
112
- // memo is returned by identity; its conflict guard rejects explicitly to hold
113
- // the rule, which the return type cannot declare and a `.catch` would not see.
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.
114
72
 
115
73
  /**
116
74
  * The gated surface: the core build's values, which are exactly the ones its
@@ -283,38 +241,29 @@ function quillmarkError(code, message, hint) {
283
241
  }
284
242
 
285
243
  // ── Handles from another copy: always a bug ─────────────────────────────────
286
- // A duplicate install (two copies of this package in one `node_modules` tree)
287
- // is two `core` builds: two linear memories and two distinct `Quill`/`Document`
288
- // classes. No topology legitimately loads a multi-megabyte WASM package twice
289
- // AND needs handles to cross between the copies, so a crossing is a consumer
290
- // bug. Every seam taking a core handle says so, uniformly, at the crossing.
244
+ // A duplicate install is two `core` builds: two linear memories and two distinct
245
+ // `Quill`/`Document` classes. No topology legitimately loads a multi-megabyte
246
+ // WASM package twice AND needs handles to cross between the copies, so a
247
+ // crossing is a consumer bug, and every seam taking a core handle says so.
291
248
  //
292
249
  // Crossing read-only handles as data is mechanically possible (`toJson` and
293
- // `toTree` serialize either way) and is not done. It leaves a package where
294
- // some verbs work and some throw, and it hides a cliff: a crossed read is a
295
- // whole-document `toJson` + `fromJson`, so a form reading fifty fields pays
296
- // fifty round trips and the symptom is "the editor got slow". A duplicate
297
- // install that limps is a duplicate install nobody removes.
250
+ // `toTree` serialize either way) and is not done: it leaves a package where some
251
+ // verbs work and some throw, and it hides a cliff, since a crossed read is a
252
+ // whole-document `toJson` + `fromJson` and a form reading fifty fields pays
253
+ // fifty round trips.
298
254
  //
299
- // What the checks deliver is the ERROR, not the rejection. wasm-bindgen's
300
- // generated glue already rejects a foreign class on every method declaring a
301
- // reference parameter (`Document.equals`, `Quill.validate`, `Quill.resolve`,
302
- // the `&Quill`-taking `Document._commitField` and friends): its `_assertClass`
303
- // is emitted unconditionally and runs before any of our code. It throws a bare
304
- // `Error` reading `expected instance of Document` at a value that IS a
305
- // `Document`, so `isQuillmarkError` returns false and the failure leaves this
306
- // package's error contract, naming neither the cause nor the cure. The checks
307
- // front-run it with a `QuillmarkError` that names both.
308
- //
309
- // They also cover the seams with NO `_assertClass` to front-run: `Engine` and
310
- // `LiveSession.update` cross into backend memory as data (`toTree`/`toJson`), so
311
- // a foreign handle there would silently work, at the price of the round-trip
312
- // above and a quill clone cache split per copy.
313
- //
314
- // Not an API widening in either direction: the declared parameter types stay
315
- // `Quill`/`Document`, and the accepted set is exactly this copy's instances.
316
-
317
- /** Per class: the code and hint for "not one at all", and the probe for "from another copy". */
255
+ // What the checks deliver is the ERROR, not the rejection. wasm-bindgen's glue
256
+ // already rejects a foreign class wherever a method declares a reference
257
+ // parameter, but its `_assertClass` throws a bare `Error` reading
258
+ // `expected instance of Document` at a value that IS a `Document`, so
259
+ // `isQuillmarkError` returns false and the failure leaves this package's error
260
+ // contract. The checks front-run it with a `QuillmarkError` naming both cause
261
+ // and cure, and cover the seams with no `_assertClass` to front-run: `Engine`
262
+ // and `LiveSession.update` cross into backend memory as data, where a foreign
263
+ // handle would silently work at the price of that round trip and a per-copy
264
+ // split of the quill clone cache.
265
+
266
+ /** Per class: the code and hint for "not one at all", and the from-another-copy probe. */
318
267
  const HANDLE_KINDS = {
319
268
  Quill: {
320
269
  code: 'runtime::not_a_quill',
@@ -398,10 +347,8 @@ function patchHandleChecked(proto, name, wrap) {
398
347
  /** @type {any} */ (proto)[name] = patched;
399
348
  }
400
349
 
401
- // The three core methods declaring a `&Document` parameter. Each already refuses
402
- // a foreign handle inside `_assertClass`; the patch is what makes the refusal
403
- // legible. Only the argument can be foreign, the receiver being whichever copy
404
- // the caller reached for.
350
+ // The core methods declaring a `&Document` parameter. Each already refuses a
351
+ // foreign handle inside `_assertClass`; the patch makes the refusal legible.
405
352
  patchHandleChecked(Document.prototype, 'equals', (original) =>
406
353
  function equals(/** @type {any} */ other) {
407
354
  requireLocalDoc(other, 'Document.equals');
@@ -497,22 +444,13 @@ export function isListItemContainer(container) {
497
444
  }
498
445
 
499
446
  // ── Open-set membership guards ──────────────────────────────────────────────
500
- // The guards above each answer "is this arm X": one pinned arm at a time. These
501
- // four answer the other question: "is this a value this build knows?" A consumer
502
- // that must branch known-vs-unknown, any read-modify-write consumer, since
503
- // lowering an edit restates every line's kind and containers, otherwise
504
- // enumerates the built-in names in its own source, recreating the closed-set
505
- // coupling the open set exists to remove. That list is correct until the release
506
- // that adds a built-in, at which point the new construct is misclassified as
507
- // unknown and round-trips through the consumer's unknown carrier, losing any
508
- // sibling-key payload.
509
- //
510
- // A predicate rather than an exported name list, because the known tables below
511
- // are upstream's business. They are pinned against the Rust source
512
- // (`Content::RESERVED_*` and `KnownIslandType`) by the
513
- // `known_open_set_names_are_pinned` drift-guard test in
514
- // `crates/content/src/model.rs`: adding a built-in means editing there, here, and
515
- // the TS unions in `crates/bindings/wasm/src/engine.rs` in one commit.
447
+ // The guards above each answer "is this arm X". These four answer "is this a
448
+ // value this build knows?", the question any read-modify-write consumer must
449
+ // ask, since lowering an edit restates every line's kind and containers. A
450
+ // predicate rather than an exported name list, because the tables below are
451
+ // upstream's business: they are pinned against the Rust source by
452
+ // `tests/known_names_drift.rs`, so adding a built-in means editing there, here,
453
+ // and the TS unions in `src/engine.rs` in one commit.
516
454
  //
517
455
  // These classify unknown *tags*, not unknown *payloads on known tags*. A future
518
456
  // `kind: "footnote"` with a sibling `ref` loses `ref` at a consumer that predates
@@ -558,12 +496,11 @@ export function isUnknownIsland(island) {
558
496
  /**
559
497
  * Build a `load` thunk: dynamic-import a backend build, then instantiate it.
560
498
  *
561
- * Under `--target web` a freshly imported build is inert (the import resolves
562
- * before there is a wasm instance behind the classes), so instantiation is part
563
- * of loading and the consumer never sees it. Memoized at MODULE scope, not per
564
- * `Engine`: two engines issuing their first render concurrently must share one
565
- * instantiation, and the generated entry's own `wasm !== undefined` guard only
566
- * catches a call that arrives after one finished, not one already in flight.
499
+ * Under `--target web` a freshly imported build is inert, so instantiation is
500
+ * part of loading. Memoized at MODULE scope, not per `Engine`: two engines
501
+ * issuing their first render concurrently must share one instantiation, and the
502
+ * generated entry's own `wasm !== undefined` guard only catches a call arriving
503
+ * after one finished, not one already in flight.
567
504
  *
568
505
  * @param {string} id backend id, for the failure message
569
506
  * @param {() => Promise<any>} importThunk the dynamic `import()`
@@ -594,19 +531,15 @@ function backendLoad(id, importThunk, wasmUrl) {
594
531
  }));
595
532
  }
596
533
 
597
- // Backend builds are NEVER statically imported here: that would pull a
598
- // multi-MB binary into the eager graph and defeat lazy loading. Each entry is a
599
- // DESCRIPTOR: `load` is a thunk that dynamically imports a backend's chunk and
600
- // instantiates it, so the binary is fetched only when something actually renders
601
- // against that backend and is ready to use when the promise resolves;
602
- // `formats`/`canvas` are the REQUIRED static capability manifest so the
603
- // cheap probes (`supportedFormats`/`supportsCanvas`) ALWAYS answer without
604
- // loading the binary or cloning the quill. The manifest values are verified
605
- // against each backend's Rust source (`crates/backends/<id>/src/lib.rs`
606
- // `SUPPORTED_FORMATS`) and pinned by the `runtime.test.js` drift-guard test,
607
- // which renders once and asserts the loaded backend reports the same list.
608
- // `canvas` mirrors `quillmark_core::formats_support_canvas`: true iff the
609
- // format list includes a visual-page format (`svg` or `png`).
534
+ // Backend builds are NEVER statically imported here: that would pull a multi-MB
535
+ // binary into the eager graph and defeat lazy loading. Each entry is a
536
+ // DESCRIPTOR: `load` dynamically imports and instantiates a backend's chunk, so
537
+ // the binary is fetched only when something renders against it; `formats` and
538
+ // `canvas` are the required static capability manifest, so the probes
539
+ // (`supportedFormats` / `supportsCanvas`) answer without loading the binary or
540
+ // cloning the quill. The manifest mirrors each backend's Rust `SUPPORTED_FORMATS`
541
+ // (and `formats_support_canvas`: true iff the list includes `svg` or `png`),
542
+ // pinned by a `runtime.test.js` drift guard that renders once and compares.
610
543
  const DEFAULT_BACKENDS = {
611
544
  typst: {
612
545
  load: backendLoad(
@@ -630,11 +563,9 @@ const DEFAULT_BACKENDS = {
630
563
  };
631
564
 
632
565
  /**
633
- * Validate a backend registry descriptor, throwing a clear error naming the
634
- * backend id on any malformed entry. Descriptors are the ONLY accepted form:
635
- * `{ load, formats, canvas }` with a callable `load`, a `formats` array, and a
636
- * boolean `canvas`. Failing at construction (not deep inside a render) keeps the
637
- * capability probes free; they can answer from the manifest unconditionally.
566
+ * Validate a backend registry descriptor, naming the backend id on any
567
+ * malformed entry. Failing at construction rather than deep inside a render is
568
+ * what lets the capability probes answer from the manifest unconditionally.
638
569
  * @param {string} id
639
570
  * @param {unknown} entry
640
571
  * @returns {{ load: () => Promise<unknown>, formats: string[], canvas: boolean }}
@@ -671,10 +602,9 @@ export class Engine {
671
602
  /** backendId → descriptor `{ load, formats, canvas }`. */
672
603
  #loaders;
673
604
  /**
674
- * backendId → WeakMap<canonical Quill, backend-memory Quill clone>. Caches
675
- * the expensive quill materialization per (engine, backend, canonical quill
676
- * instance). WeakMap so dropping the canonical quill makes its clone
677
- * collectable; the backend handle is then freed by wasm-bindgen weak-refs.
605
+ * backendId → WeakMap<canonical Quill, backend-memory clone>, caching the
606
+ * expensive materialization. WeakMap so dropping the canonical quill makes
607
+ * its clone collectable, and wasm-bindgen weak-refs then free the handle.
678
608
  * @type {Map<string, WeakMap<object, any>>}
679
609
  */
680
610
  #quillClones = new Map();
@@ -682,11 +612,9 @@ export class Engine {
682
612
  /**
683
613
  * @param {{ backends?: Record<string, { load: () => Promise<unknown>, formats: string[], canvas: boolean }> }} [options]
684
614
  * Extra or overriding backend descriptors, merged over the built-ins. Each
685
- * entry is a descriptor (`{ load, formats, canvas }`) with `formats` and
686
- * `canvas` REQUIRED: that static manifest is what makes
687
- * `supportedFormats`/`supportsCanvas` always free (no binary load, no quill
688
- * clone). Malformed entries throw here, at construction. The default
689
- * registry maps `"typst"` to the bundled Typst build.
615
+ * is `{ load, formats, canvas }` with the manifest REQUIRED, since that is
616
+ * what makes `supportedFormats` / `supportsCanvas` free; malformed entries
617
+ * throw here, at construction.
690
618
  *
691
619
  * `load` resolves to a READY module: a registrant shipping its own
692
620
  * `--target web` build instantiates inside the thunk. More than one
@@ -703,8 +631,8 @@ export class Engine {
703
631
  }
704
632
 
705
633
  /**
706
- * Look up the registered descriptor for `backendId`, throwing the canonical
707
- * "no backend registered" error if none. Pure, touches no binary.
634
+ * The registered descriptor for `backendId`, or the "no backend registered"
635
+ * throw. Touches no binary.
708
636
  * @param {string} backendId
709
637
  * @returns {{ load: () => Promise<unknown>, formats: string[], canvas: boolean }}
710
638
  */
@@ -722,8 +650,7 @@ export class Engine {
722
650
  /**
723
651
  * `quill`'s backend id, after checking the handle. The ONE way an `Engine`
724
652
  * verb reaches `backendId`, so "no verb touches a foreign quill" is
725
- * structural rather than four remembered calls. See § "Handles from another
726
- * copy".
653
+ * structural rather than four remembered calls.
727
654
  * @param {Quill} quill
728
655
  * @param {string} method the caller's name, for the rejection message
729
656
  * @returns {string}
@@ -765,12 +692,9 @@ export class Engine {
765
692
  }
766
693
 
767
694
  /**
768
- * Get (or materialize-and-cache) the backend-memory `Quill` clone for
769
- * `quill` under `backendId`. On a cache miss the clone is built from `tree`,
770
- * the caller's pre-await `toTree()` snapshot; the canonical handle may be
771
- * freed by now, and stored in the per-backend `WeakMap` keyed on the
772
- * canonical `Quill` instance, so a later call with the same instance reuses
773
- * it (the hot-path fix: no re-serialize / re-copy / re-validate per call).
695
+ * Get (or materialize-and-cache) the backend-memory `Quill` clone for `quill`
696
+ * under `backendId`. On a miss the clone is built from `tree`, the caller's
697
+ * pre-await snapshot, since the canonical handle may be freed by now.
774
698
  * @param {any} mod the backend build module
775
699
  * @param {string} backendId
776
700
  * @param {object} quill the canonical instance (cache key only)
@@ -798,22 +722,15 @@ export class Engine {
798
722
  *
799
723
  * OWNERSHIP WINDOW: both caller handles are snapshotted (`doc.toJson()`, and
800
724
  * `quill.toTree()` on a clone-cache miss) BEFORE the first await. The backend
801
- * load below is a real suspension point (a multi-MB `import()` on first
802
- * render) so reading the handles after it would race a caller that
803
- * `free()`s them as soon as this call returns its promise ("null pointer
804
- * passed to rust"). The snapshot makes that natural calling pattern correct.
725
+ * load below is a real suspension point, so reading the handles after it
726
+ * would race a caller that `free()`s them as soon as this call returns its
727
+ * promise ("null pointer passed to rust").
805
728
  *
806
729
  * Clone lifetimes differ by design: the `doc` clone is TRANSIENT, freed in
807
- * the `finally` of every call. The `quill` clone is CACHED per (engine,
808
- * backend, canonical quill instance) and is NOT freed here; a `Quill`
809
- * instance's contents never change after construction, so it is dropped with
810
- * the canonical quill (WeakMap collection → wasm-bindgen weak-ref free) when
811
- * the consumer replaces the instance. A cache miss materializes it once;
812
- * subsequent calls reuse it.
813
- *
814
- * Both handles are checked here, so the crossing is core→backend (always two
815
- * memories, always as data); core→core is a duplicate install and never gets
816
- * past the first line. See § "Handles from another copy".
730
+ * the `finally` of every call, while the `quill` clone is CACHED and is not
731
+ * freed here a `Quill` instance's contents never change after
732
+ * construction, so it is dropped with the canonical quill when the consumer
733
+ * replaces the instance.
817
734
  * @param {string} method the caller's name, for the rejection message
818
735
  * @param {Quill} quill
819
736
  * @param {Document} doc
@@ -825,12 +742,9 @@ export class Engine {
825
742
  const docJson = doc.toJson();
826
743
  const quillTree = this.#quillClones.get(backendId)?.has(quill) ? null : quill.toTree();
827
744
  const { mod, engine } = await this.#resolveBackend(backendId);
828
- // The quill clone is cached (see #cachedQuillClone); only the per-call doc
829
- // clone is transient. Bring the doc clone + `fn` under one try so the doc
830
- // clone is freed even if a later step throws. The cached quill clone is
831
- // intentionally NOT freed here. `fn` MUST be synchronous: the doc clone is
832
- // freed as soon as it returns, so an async `fn` would have it freed
833
- // mid-flight.
745
+ // The doc clone and `fn` share one try so the clone is freed even if a
746
+ // later step throws; the cached quill clone is intentionally not freed.
747
+ // `fn` MUST be synchronous: an async one would run against a freed clone.
834
748
  const backendQuill = this.#cachedQuillClone(mod, backendId, quill, quillTree);
835
749
  let backendDoc = null;
836
750
  try {
@@ -842,9 +756,8 @@ export class Engine {
842
756
  }
843
757
 
844
758
  /**
845
- * Render `doc` against `quill` in one shot, returning a `RenderResult`.
846
- * Both handles are read synchronously before the first await, so the caller
847
- * may `free()` them as soon as this call returns.
759
+ * Render `doc` against `quill` in one shot. Both handles are read
760
+ * synchronously before the first await.
848
761
  * @param {Quill} quill
849
762
  * @param {Document} doc
850
763
  * @param {object} [options] render options (`{ format, ppi, pages, producer }`)
@@ -857,12 +770,9 @@ export class Engine {
857
770
  }
858
771
 
859
772
  /**
860
- * Open a live render session (canvas preview / per-page paint / `update`).
861
- * The session is self-contained (it retains what it needs for `update`), so
862
- * the transient quill and document clones are freed before this returns;
863
- * the caller owns the returned session and must `.free()` it. The `quill`
864
- * and `doc` handles are read synchronously before the first await, so the
865
- * caller may `free()` them as soon as this call returns.
773
+ * Open a live render session. It retains what `update` needs, so the
774
+ * transient clones are freed before this returns; the caller owns the session
775
+ * and must `.free()` it.
866
776
  * @param {Quill} quill
867
777
  * @param {Document} doc
868
778
  * @returns {Promise<LiveSession>}
@@ -877,10 +787,8 @@ export class Engine {
877
787
  }
878
788
 
879
789
  /**
880
- * The output formats `quill`'s backend can emit. A cheap, non-failing,
881
- * ALWAYS-free pre-render probe: it answers from the descriptor's required
882
- * `formats` manifest (NO binary load and NO quill clone) depending only on
883
- * `quill.backendId`. Stays `async` for API stability (it never awaits a load).
790
+ * The output formats `quill`'s backend can emit: an always-free probe over
791
+ * the descriptor's manifest. `async` for API stability; it awaits nothing.
884
792
  * @param {Quill} quill
885
793
  * @returns {Promise<import('./runtime.js').OutputFormat[]>}
886
794
  */
@@ -891,13 +799,9 @@ export class Engine {
891
799
  }
892
800
 
893
801
  /**
894
- * Whether `quill`'s BACKEND can paint sessions to a canvas: a pre-session
895
- * ESTIMATE, not a fact about any particular compile. Same ALWAYS-free probe
896
- * as `supportedFormats`: answered from the descriptor's required `canvas`
897
- * manifest, no load and no clone. A specific compile can still refuse to
898
- * paint (e.g. a 0-page document), so this can answer `true` while the
899
- * resulting `LiveSession.supportsCanvas` answers `false`: gate mounting a
900
- * canvas UI on this, gate the actual `paint` call on the session's getter.
802
+ * Whether `quill`'s backend can paint to a canvas: a pre-session estimate over
803
+ * the descriptor's manifest, so it can answer `true` where the resulting
804
+ * `LiveSession.supportsCanvas` answers `false`.
901
805
  * @param {Quill} quill
902
806
  * @returns {Promise<boolean>}
903
807
  */
@@ -908,20 +812,9 @@ export class Engine {
908
812
  }
909
813
 
910
814
  /**
911
- * Thin wrapper over a backend's live render session. Reads serve the current
912
- * compile; `update(doc)` recompiles in place (transactional: on throw, reads
913
- * keep serving the last-good compile). The quill/document clones it was
914
- * opened from have already been freed: the session retains what `update`
915
- * needs.
916
- *
917
- * Geometry reads (`regions`, `positionAt`, `locate`) resolve against the
918
- * current compile; anchoring a caret or selection across edits is the editor's
919
- * job (its own transaction mapping): re-read geometry after each committed
920
- * `update`.
921
- *
922
- * `paint` writes a COMPLETE page raster (all content visible, no caller-side
923
- * compositing) for every backend that supports canvas (Typst rasterizes
924
- * natively; pdfform rasterizes its pre-flattened page). See `runtime.d.ts`.
815
+ * Thin wrapper over a backend's live render session; see `runtime.d.ts` for the
816
+ * contract. The quill/document clones it was opened from have already been
817
+ * freed: the session retains what `update` needs.
925
818
  */
926
819
  export class LiveSession {
927
820
  /**
@@ -936,9 +829,6 @@ export class LiveSession {
936
829
  #mod;
937
830
 
938
831
  /**
939
- * Recompile the session against `doc`: the edit verb of a live preview.
940
- * Transactional: on throw every read keeps serving the last-good compile.
941
- * On success reads serve the new compile; repaint `dirtyPages ∩ visible`.
942
832
  * @param {Document} doc
943
833
  * @returns {import('./runtime.d.ts').ChangeSet}
944
834
  */
@@ -961,12 +851,8 @@ export class LiveSession {
961
851
  }
962
852
  /**
963
853
  * `true` iff `paint`/`pageSize` will succeed for THIS compile: the
964
- * authoritative answer, derived from the session's canvas seam, so it can
965
- * never disagree with what `paint` actually does. This can be `false` even
966
- * when `Engine.supportsCanvas` answered `true` for the same `quill` (that
967
- * probe is a pre-session backend estimate; e.g. a canvas-capable backend
968
- * compiled to a 0-page document has nothing to paint). Re-check this getter
969
- * after `open()` rather than relying on the engine hint alone.
854
+ * authoritative answer, which can be `false` where `Engine.supportsCanvas`
855
+ * answered `true` for the same quill.
970
856
  * @returns {boolean}
971
857
  */
972
858
  get supportsCanvas() {
@@ -982,10 +868,6 @@ export class LiveSession {
982
868
  }
983
869
 
984
870
  /**
985
- * Schema-field geometry for this compiled session: one region per
986
- * schema-bound field, keyed on its quill schema field path. A session-level
987
- * query (no render); read it to place field overlays / cross-navigation over
988
- * a `paint`-ed canvas.
989
871
  * @returns {import('./runtime.d.ts').FieldRegion[]}
990
872
  */
991
873
  regions() {
@@ -993,12 +875,6 @@ export class LiveSession {
993
875
  }
994
876
 
995
877
  /**
996
- * The whole-field highlight boxes for `field`: one union rect per page,
997
- * over the field's `span`-bearing content segments. Owns the union
998
- * `regions()` leaves derived (span-filter + per-page union), so a "highlight
999
- * the focused field" consumer stops reimplementing it. Content only: a field
1000
- * placed solely as a scalar reference or a bound widget returns `[]`, its
1001
- * box is a single `regions()` rect.
1002
878
  * @param {string} field
1003
879
  * @returns {import('./runtime.d.ts').FieldRegion[]}
1004
880
  */
@@ -1007,11 +883,6 @@ export class LiveSession {
1007
883
  }
1008
884
 
1009
885
  /**
1010
- * The schema field whose content is under a point on `page`: the forward
1011
- * (click → field) direction, resolving *every* placement, not just the first
1012
- * that `regions` enumerates. `x`/`y` are PDF points with a bottom-left origin
1013
- * (the `FieldRegion.rect` space). See `runtime.d.ts` for the click-to-point
1014
- * inverse transform.
1015
886
  * @param {number} page
1016
887
  * @param {number} x
1017
888
  * @param {number} y
@@ -1046,10 +917,6 @@ export class LiveSession {
1046
917
  }
1047
918
 
1048
919
  /**
1049
- * Paint `page` into a 2D canvas context. The painted raster is COMPLETE
1050
- * (all page content visible, no caller-side compositing) for both the Typst
1051
- * and pdfform backends. See `runtime.d.ts` for the DPR/clamp math and the
1052
- * region-overlay coordinate transform.
1053
920
  * @param {CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D} ctx
1054
921
  * @param {number} page
1055
922
  * @param {object} [options]
@@ -1123,10 +990,8 @@ export class DocumentWriter {
1123
990
  return this.#doc._commitFields(this.#quill, MAIN_CARD_ADDR, fields);
1124
991
  }
1125
992
  /**
1126
- * Revise the main body from markdown (edit semantics: surviving anchors
1127
- * rebase), returning the text {@link Delta}. The content lane's `revise`
1128
- * reached through the writer: a body carries no field schema, so there is
1129
- * nothing for a typed verb to type, and the receipt is the content lane's.
993
+ * Revise the main body from markdown; anchors rebase. A body carries no field
994
+ * schema, so this is the content lane's `revise` reached through the writer.
1130
995
  * @param {string} markdown
1131
996
  * @returns {import('../core/wasm.js').Delta}
1132
997
  */
@@ -1135,14 +1000,9 @@ export class DocumentWriter {
1135
1000
  }
1136
1001
  /**
1137
1002
  * Revise the content main-card field `name` from authored text: typed *and*
1138
- * anchor-preserving. Surviving anchors rebase, then the diffed result is
1139
- * schema-conformed (`richtext(inline)` rejects a multi-block result). Throws
1140
- * `UnknownField` for a name the schema does not declare. Returns the text
1141
- * {@link Delta}.
1142
- *
1143
- * The codec comes from the declared type: `richtext` diffs markdown, while
1144
- * `plaintext` diffs the literal text and never imports markdown, so a
1145
- * byte-identical revise of a value carrying escapes is a byte no-op.
1003
+ * anchor-preserving. Anchors rebase, then the diffed result is
1004
+ * schema-conformed. The codec comes from the declared type: `richtext` diffs
1005
+ * markdown, `plaintext` the literal text.
1146
1006
  * @param {string} name
1147
1007
  * @param {string} text
1148
1008
  * @returns {import('../core/wasm.js').Delta}
@@ -1152,13 +1012,9 @@ export class DocumentWriter {
1152
1012
  }
1153
1013
  /**
1154
1014
  * Build a composable card of `kind`, typed-commit `fields` onto it, set its
1155
- * body from optional markdown, and place it: the fused `makeCard` + typed
1156
- * commit + insertion. `at` picks the position: omitted appends, a number
1157
- * inserts at that index (`0..=cardCount`), so a positioned typed insert is one
1158
- * atomic call rather than `addCard` + `moveCard`. Transactional: the card is
1159
- * committed in full before it joins the document, so a rejected field (throws
1160
- * a per-field diagnostic bundle, `UnknownField` per undeclared name) or an
1161
- * invalid kind/body/position leaves the document untouched.
1015
+ * body from optional markdown, and place it. Transactional: the card is
1016
+ * committed in full before it joins the document, so a rejected field, kind,
1017
+ * body, or position leaves the document untouched.
1162
1018
  * @param {string} kind
1163
1019
  * @param {Record<string, unknown>} [fields]
1164
1020
  * @param {string} [body]
@@ -1169,8 +1025,6 @@ export class DocumentWriter {
1169
1025
  return this.#doc._addCard(this.#quill, kind, fields, body, at);
1170
1026
  }
1171
1027
  /**
1172
- * Remove the composable card at `index`, returning it (or `undefined` if the
1173
- * index is out of range): the writer spelling of `Document.removeCard`.
1174
1028
  * @param {number} index
1175
1029
  * @returns {import('../core/wasm.js').Card | undefined}
1176
1030
  */
@@ -1178,14 +1032,9 @@ export class DocumentWriter {
1178
1032
  return this.#doc.removeCard(index);
1179
1033
  }
1180
1034
  /**
1181
- * A {@link CardWriter} bound to the composable card at `index`. Index
1182
- * validity is checked lazily by the underlying write (it throws
1183
- * `IndexOutOfRange` at commit time), so an out-of-range index does not throw
1184
- * here.
1185
- *
1186
- * The cursor is ephemeral: bind, write, discard. It holds `index`, not the
1187
- * card: a `removeCard`/`addCard` between binding and writing silently
1188
- * retargets it. Re-resolve the index at write time when cards may move.
1035
+ * A {@link CardWriter} bound to the composable card at `index`, checked
1036
+ * lazily at the write. It holds `index`, not the card, so a
1037
+ * `removeCard`/`addCard` between binding and writing silently retargets it.
1189
1038
  * @param {number} index
1190
1039
  * @returns {CardWriter}
1191
1040
  */
@@ -1220,9 +1069,8 @@ export class CardWriter {
1220
1069
  return this.#index;
1221
1070
  }
1222
1071
  /**
1223
- * The bound card's `$kind` (empty string when it carries none), read through
1224
- * the document: mirrors core `CardWriter::kind()`. Ephemeral like the cursor
1225
- * itself: throws `IndexOutOfRange` if the bound index is out of range.
1072
+ * The bound card's `$kind`, empty string when it carries none. Throws
1073
+ * `IndexOutOfRange` for a bad bound index.
1226
1074
  * @returns {string}
1227
1075
  */
1228
1076
  get kind() {
@@ -1250,8 +1098,7 @@ export class CardWriter {
1250
1098
  return this.#doc._commitFields(this.#quill, { card: this.#index }, fields);
1251
1099
  }
1252
1100
  /**
1253
- * Revise this card's body from markdown (edit semantics), returning the text
1254
- * {@link Delta}: the card twin of {@link DocumentWriter.reviseBody}.
1101
+ * The card twin of {@link DocumentWriter.reviseBody}.
1255
1102
  * @param {string} markdown
1256
1103
  * @returns {import('../core/wasm.js').Delta}
1257
1104
  */
@@ -1259,11 +1106,8 @@ export class CardWriter {
1259
1106
  return this.#doc.revise({ card: this.#index }, markdown);
1260
1107
  }
1261
1108
  /**
1262
- * Revise the content field `name` on this card from authored text: typed *and*
1263
- * anchor-preserving; the card twin of {@link DocumentWriter.reviseField},
1264
- * codec included. Throws `UnknownField` for an undeclared name and
1265
- * `IndexOutOfRange` if the bound index is out of range. Returns the text
1266
- * {@link Delta}.
1109
+ * The card twin of {@link DocumentWriter.reviseField}. Throws `UnknownField`
1110
+ * for an undeclared name and `IndexOutOfRange` for a bad bound index.
1267
1111
  * @param {string} name
1268
1112
  * @param {string} text
1269
1113
  * @returns {import('../core/wasm.js').Delta}
@@ -1274,17 +1118,12 @@ export class CardWriter {
1274
1118
  }
1275
1119
 
1276
1120
  // ── `quill.writer(doc)`, the typed front door ──────────────────────────────
1277
- // The schema-bound writer: bind the quill's schema to a document and issue bare
1278
- // typed writes. Mirrors core's `quill.writer(&mut doc)`: the schema grants the
1279
- // typing, so the quill (not the document) is the factory. Patched onto the
1280
- // re-exported `Quill` prototype rather than wrapped: `Quill === CoreQuill`
1281
- // stays true (the identity invariant above); this only adds a method that
1282
- // constructs the pure-JS writer, which owns no WASM handle.
1121
+ // Patched onto the re-exported `Quill` prototype rather than wrapped, so
1122
+ // `Quill === CoreQuill` stays true: this only adds a method constructing the
1123
+ // pure-JS writer, which owns no WASM handle.
1283
1124
  /**
1284
- * A {@link DocumentWriter} binding this quill's schema to `doc` for typed
1285
- * writes: the documented front door. The returned writer holds both handles by
1286
- * reference and owns neither, so there is nothing to `free()`. Ephemeral by
1287
- * convention: bind, write, discard.
1125
+ * A {@link DocumentWriter} binding this quill's schema to `doc`. It holds both
1126
+ * handles by reference and owns neither: bind, write, discard.
1288
1127
  * @this {Quill}
1289
1128
  * @param {Document} doc the document to mutate, held by reference (not owned)
1290
1129
  * @returns {DocumentWriter}
@@ -1293,22 +1132,16 @@ Quill.prototype.writer = function writer(doc) {
1293
1132
  return new DocumentWriter(this, doc);
1294
1133
  };
1295
1134
 
1296
- // ── Typed-reader sugar: the schema-plane read surface ──────────────────────────
1297
- // The read twin of the writer above. The transport `Document.getStored` is schema-free:
1298
- // a `Document` cannot say which fields are richtext, so an unknown field name
1135
+ // ── Typed-reader sugar: the schema-plane read surface ─────────────────────────
1136
+ // The transport `Document.getStored` is schema-free, so an unknown field name
1299
1137
  // reads back `undefined` rather than as the typo it is. Binding the quill's
1300
- // schema (`_readerGet` takes the handle, like the `commit*` verbs) lets one `get`
1301
- // interpret by declared type: a richtext field to markdown, a plaintext field to
1302
- // its literal text, every other type verbatim, and an unknown name throws
1303
- // `UnknownField`. A field's markdown lives here, not on the body-only
1304
- // `bodyMarkdown`. Like the writer classes these hold the caller's handles by
1305
- // reference, own no WASM object, and have nothing to `free()`.
1138
+ // schema lets one `get` interpret by declared type and throw `UnknownField` on a
1139
+ // name the schema does not declare.
1306
1140
 
1307
1141
  /**
1308
- * A {@link Document} bound to its {@link Quill} for typed reads: the JS twin of
1309
- * Rust's `quill.reader(&doc)` and the read counterpart of {@link DocumentWriter}.
1310
- * Reads target the main card; use {@link card} for a composable card. Holds both
1311
- * handles by reference and owns neither, so there is nothing to `free()`.
1142
+ * A {@link Document} bound to its {@link Quill} for typed reads, the read
1143
+ * counterpart of {@link DocumentWriter}. Reads target the main card; use
1144
+ * {@link card} for a composable one. Owns neither handle.
1312
1145
  */
1313
1146
  export class DocumentReader {
1314
1147
  #quill;
@@ -1341,15 +1174,11 @@ export class DocumentReader {
1341
1174
  return this.#doc._readerGet(this.#quill, addr);
1342
1175
  }
1343
1176
  /**
1344
- * Read the content field at `addr` as its canonical `Content`: the
1345
- * `Content` twin of {@link get}, which projects. Decodes through the codec the
1346
- * declared type names (`richtext` as markdown, `plaintext` as literal text),
1347
- * so a field the writer committed as a `Content` and one a markdown parse left
1348
- * as an authored string read back the same; no branching on how the
1349
- * document was built. An absent `addr.field` reads the body `Content`.
1350
- * `undefined` for an absent field; throws `UnknownField`, `FieldNotContent`
1351
- * for a type that is not a content leaf, `FieldDecode` for an undecodable
1352
- * value, and `IndexOutOfRange` for a bad `addr.card`.
1177
+ * Read the content field at `addr` as canonical `Content`: the twin of
1178
+ * {@link get}, which projects. An absent `addr.field` reads the body
1179
+ * `Content`. `undefined` for an absent field; throws `UnknownField`,
1180
+ * `FieldNotContent` for a type that is not a content leaf, `FieldDecode` for
1181
+ * an undecodable value, and `IndexOutOfRange` for a bad `addr.card`.
1353
1182
  * @param {import('../core/wasm.js').Addr | string} addr
1354
1183
  * @returns {import('../core/wasm.js').Content | undefined}
1355
1184
  */
@@ -1357,18 +1186,29 @@ export class DocumentReader {
1357
1186
  return this.#doc._readerGetContent(this.#quill, addr);
1358
1187
  }
1359
1188
  /**
1360
- * The main body's markdown: the quill-free body read (a body's type is a
1361
- * format fact, not a schema fact). Equivalent to `get({})`.
1189
+ * Read the `Content` nested inside the composite field at `addr`, at `path`:
1190
+ * `[0]` an `array<richtext>` element, `["motto"]` an object's content property,
1191
+ * `[1, "notes"]` a leaf under both. The codec is the leaf's declared type's.
1192
+ * `undefined` for an absent field and for a path that names nothing stored;
1193
+ * throws `UnknownField`, `FieldNotContent` when `path` resolves to no content
1194
+ * leaf, `FieldDecode` anchored at the addressed path, and `IndexOutOfRange`.
1195
+ * @param {import('../core/wasm.js').Addr | string} addr
1196
+ * @param {import('../core/wasm.js').PathStep[]} path
1197
+ * @returns {import('../core/wasm.js').Content | undefined}
1198
+ */
1199
+ getContentAt(addr, path) {
1200
+ return this.#doc._readerGetContentAt(this.#quill, addr, path);
1201
+ }
1202
+ /**
1203
+ * The main body's markdown: the quill-free body read. Equals `get({})`.
1362
1204
  * @returns {string}
1363
1205
  */
1364
1206
  bodyMarkdown() {
1365
1207
  return this.#doc._readerGet(this.#quill, {});
1366
1208
  }
1367
1209
  /**
1368
- * A {@link CardReader} bound to the composable card at `index`. Index validity
1369
- * is checked lazily by the underlying read (it throws `IndexOutOfRange` at read
1370
- * time), so an out-of-range index does not throw here. Ephemeral like the
1371
- * writer cursor: it holds `index`, not the card, so a `removeCard`/`addCard`
1210
+ * A {@link CardReader} bound to the composable card at `index`, checked lazily
1211
+ * at the read. It holds `index`, not the card, so a `removeCard`/`addCard`
1372
1212
  * between binding and reading silently retargets it.
1373
1213
  * @param {number} index
1374
1214
  * @returns {CardReader}
@@ -1422,8 +1262,7 @@ export class CardReader {
1422
1262
  return this.#doc._readerGet(this.#quill, { card: this.#index, field: name });
1423
1263
  }
1424
1264
  /**
1425
- * Read the content field `name` on this card as its canonical `Content`
1426
- * `Content`: the card twin of {@link DocumentReader.getContent}.
1265
+ * The card twin of {@link DocumentReader.getContent}.
1427
1266
  * @param {string} name
1428
1267
  * @returns {import('../core/wasm.js').Content | undefined}
1429
1268
  */
@@ -1431,7 +1270,16 @@ export class CardReader {
1431
1270
  return this.#doc._readerGetContent(this.#quill, { card: this.#index, field: name });
1432
1271
  }
1433
1272
  /**
1434
- * This card's body markdown: the card twin of {@link DocumentReader.bodyMarkdown}.
1273
+ * The card twin of {@link DocumentReader.getContentAt}.
1274
+ * @param {string} name
1275
+ * @param {import('../core/wasm.js').PathStep[]} path
1276
+ * @returns {import('../core/wasm.js').Content | undefined}
1277
+ */
1278
+ getContentAt(name, path) {
1279
+ return this.#doc._readerGetContentAt(this.#quill, { card: this.#index, field: name }, path);
1280
+ }
1281
+ /**
1282
+ * The card twin of {@link DocumentReader.bodyMarkdown}.
1435
1283
  * @returns {string}
1436
1284
  */
1437
1285
  bodyMarkdown() {
@@ -1440,14 +1288,10 @@ export class CardReader {
1440
1288
  }
1441
1289
 
1442
1290
  // ── `quill.reader(doc)`: the schema-plane read front door ─────────────────────
1443
- // The read twin of `quill.writer(doc)`, patched onto the same re-exported `Quill`
1444
- // prototype (the `Quill === CoreQuill` identity invariant holds: this only adds
1445
- // a method constructing the pure-JS reader, which owns no WASM handle).
1291
+ // Patched onto the same re-exported `Quill` prototype as `writer`.
1446
1292
  /**
1447
- * A {@link DocumentReader} binding this quill's schema to `doc` for interpreted
1448
- * reads: the read front door, mirroring core's `quill.reader(&doc)`. The returned
1449
- * reader holds both handles by reference and owns neither, so there is nothing to
1450
- * `free()`. Ephemeral by convention: bind, read, discard.
1293
+ * A {@link DocumentReader} binding this quill's schema to `doc`. It holds both
1294
+ * handles by reference and owns neither: bind, read, discard.
1451
1295
  * @this {Quill}
1452
1296
  * @param {Document} doc the document to read, held by reference (not owned)
1453
1297
  * @returns {DocumentReader}