@quillmark/wasm 0.97.0 → 0.99.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,10 +1,10 @@
1
1
  /* @ts-self-types="./runtime.d.ts" */
2
2
  //
3
- // @quillmark/wasm/runtime the canonical consumer API.
3
+ // @quillmark/wasm/runtime: the canonical consumer API.
4
4
  //
5
5
  // Consumers import `Quill`, `Document`, and `Engine` from here and never touch
6
6
  // the build-specific subpaths. The package ships multiple WASM binaries with
7
- // SEPARATE linear memories a Typst-less `core` build (small, eager) that is
7
+ // SEPARATE linear memories: a Typst-less `core` build (small, eager) that is
8
8
  // the canonical home of `Quill`/`Document`, and one private backend binary per
9
9
  // backend (`backends/typst/` today; more later) that carries an engine. A
10
10
  // handle from one memory cannot be used by another. This module hides that seam
@@ -22,16 +22,16 @@
22
22
  // renders, and the backend handles never escape.
23
23
  //
24
24
  // CLONE LIFETIMES (not all transient): the per-call `Document` clone IS
25
- // transient built and freed inside each call, because documents are small
25
+ // transient, built and freed inside each call, because documents are small
26
26
  // and mutate freely. The `Quill` clone is CACHED instead: re-cloning a quill
27
27
  // per call means re-serializing its whole file tree Rust→JS, copying it into
28
- // backend memory, and re-parsing + re-validating the bundle every time and
28
+ // backend memory, and re-parsing + re-validating the bundle every time, and
29
29
  // quills are validated, effectively-immutable bundles. So each `Engine`
30
30
  // memoizes the backend-memory quill per (engine, backendId, canonical quill
31
31
  // instance) in a `WeakMap` keyed on the canonical `Quill`: when the consumer
32
32
  // drops the core quill the cache entry becomes collectable and wasm-bindgen
33
33
  // weak-refs (`--weak-refs`) free the backend handle. The CONTRACT this buys:
34
- // a `Quill` instance's contents never change after construction mutate by
34
+ // a `Quill` instance's contents never change after construction, mutate by
35
35
  // replacing the instance (the clone is dropped with it via WeakMap +
36
36
  // weak-refs).
37
37
  //
@@ -39,26 +39,30 @@
39
39
  // `Quill`/`Document` to `engine.render(...)` and gets a `RenderResult` back.
40
40
 
41
41
  // ── CANONICAL INVARIANT: re-export the core build, never wrap ───────────────
42
- // The root re-exports the core build's `Quill`/`Document` classes verbatim
42
+ // The root re-exports the core build's `Quill`/`Document` classes verbatim,
43
43
  // NOT subclasses or wrappers. There is exactly ONE public entry point (this
44
44
  // module), so this identity is a structural fact: `Quill`/`Document` ARE the
45
45
  // core classes, and the only boundary that needs crossing is core→backend (a
46
46
  // separate WASM memory), which `Engine` does internally as data
47
47
  // (`toTree`/`toJson`).
48
48
  //
49
- // Do NOT replace this with a wrapper class that breaks the identity and turns
49
+ // Do NOT replace this with a wrapper class: that breaks the identity and turns
50
50
  // a structural fact into a converted type (a breaking design change, not a
51
- // refactor). Keep `Engine` duck-typed on `.toTree()`/`.backendId`/`.toJson()`
52
- // (it is) so it tolerates handles from any core instance. The `runtime.test.js`
53
- // "re-exports the internal core build classes verbatim" case
54
- // (`Quill === CoreQuill`) is the executable guard for this invariant.
51
+ // refactor). The `runtime.test.js` "re-exports the internal core build classes
52
+ // verbatim" case (`Quill === CoreQuill`) is the executable guard for this
53
+ // 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.
55
59
  //
56
60
  // Imported (not bare re-exported) so `Quill` is a local binding this module can
57
- // augment `quill.writer(doc)` is patched onto its prototype below. The
61
+ // augment: `quill.writer(doc)` is patched onto its prototype below. The
58
62
  // re-export keeps the identity: the exported `Quill` IS the core class.
59
63
  import { Quill, Document, init } from '../core/wasm.js';
60
64
  export { Quill, Document, init };
61
- // The document-free content codec re-exported verbatim from the core build so
65
+ // The document-free content codec: re-exported verbatim from the core build so
62
66
  // the runtime subpath exposes `exportMarkdown(body)` (the on-demand markdown
63
67
  // projection), `importMarkdown`, and the position-mapping pair (`rebase`,
64
68
  // `mapPos`).
@@ -70,10 +74,10 @@ export { parseDocPath, formatDocPath } from '../core/wasm.js';
70
74
 
71
75
  // ── The main-card address ───────────────────────────────────────────────────
72
76
  /**
73
- * The main card's address the default target of the card-scoped verbs
77
+ * The main card's address: the default target of the card-scoped verbs
74
78
  * (`storeFields` / `storeExt` / `commitFields` / …). A named, `CardAddr`-typed
75
79
  * alias for the empty address `{}`, so a main-card write names its target:
76
- * `doc.storeFields(MAIN_CARD_ADDR, fields)`. It IS `{}` (frozen), a pure alias
80
+ * `doc.storeFields(MAIN_CARD_ADDR, fields)`. It IS `{}` (frozen), a pure alias:
77
81
  * `{}` and `undefined` stay equally valid. Card axis only: a card selector,
78
82
  * never a field address.
79
83
  * @type {import('../core/wasm.js').CardAddr}
@@ -81,15 +85,15 @@ export { parseDocPath, formatDocPath } from '../core/wasm.js';
81
85
  export const MAIN_CARD_ADDR = Object.freeze({});
82
86
 
83
87
  /**
84
- * Narrow an unknown caught value to a `QuillmarkError` the error every
88
+ * Narrow an unknown caught value to a `QuillmarkError`, the error every
85
89
  * fallible method in this package throws: a real `Error` with a non-empty
86
90
  * `diagnostics` array attached (same entry shape as `RenderResult.warnings`).
87
91
  *
88
92
  * Structural by necessity AND by design: the WASM layer constructs a plain
89
93
  * `Error` and attaches the property (there is no error class to `instanceof`),
90
- * and a structural check works on errors from any build or WASM instance in
91
- * the page consistent with the duck-typed handling of handles elsewhere in
92
- * this layer.
94
+ * and a structural check narrows errors from any build or WASM instance in the
95
+ * page. The deliberate exception to § "Handles from another copy": an error is
96
+ * data, not a handle, so nothing is gained by refusing one that crossed.
93
97
  *
94
98
  * @param {unknown} e
95
99
  * @returns {e is Error & { diagnostics: import('../core/wasm.js').Diagnostic[] }}
@@ -98,16 +102,179 @@ export function isQuillmarkError(e) {
98
102
  return e instanceof Error && Array.isArray(/** @type {any} */ (e).diagnostics);
99
103
  }
100
104
 
105
+ /**
106
+ * Build a `QuillmarkError` JS-side: a real `Error` carrying `diagnostics`, the
107
+ * shape `isQuillmarkError` narrows and the shape Rust's `WasmError::to_js_value`
108
+ * produces. Errors raised by this hand-written layer belong to the same contract
109
+ * as the ones raised across the WASM boundary.
110
+ * @param {string} code
111
+ * @param {string} message
112
+ * @param {string} hint
113
+ * @returns {Error & { diagnostics: import('../core/wasm.js').Diagnostic[] }}
114
+ */
115
+ function quillmarkError(code, message, hint) {
116
+ const err = /** @type {any} */ (new Error(message));
117
+ err.diagnostics = [{ severity: 'error', code, message, hint }];
118
+ return err;
119
+ }
120
+
121
+ // ── Handles from another copy: always a bug ─────────────────────────────────
122
+ // A duplicate install (two copies of this package in one `node_modules` tree)
123
+ // is two `core` builds: two linear memories and two distinct `Quill`/`Document`
124
+ // classes. No topology legitimately loads a multi-megabyte WASM package twice
125
+ // AND needs handles to cross between the copies, so a crossing is a consumer
126
+ // bug. Every seam taking a core handle says so, uniformly, at the crossing.
127
+ //
128
+ // Crossing read-only handles as data is mechanically possible (`toJson` and
129
+ // `toTree` serialize either way) and is not done. It leaves a package where
130
+ // some verbs work and some throw, and it hides a cliff: a crossed read is a
131
+ // whole-document `toJson` + `fromJson`, so a form reading fifty fields pays
132
+ // fifty round trips and the symptom is "the editor got slow". A duplicate
133
+ // install that limps is a duplicate install nobody removes.
134
+ //
135
+ // What the checks deliver is the ERROR, not the rejection. wasm-bindgen's
136
+ // generated glue already rejects a foreign class on every method declaring a
137
+ // reference parameter (`Document.equals`, `Quill.validate`, `Quill.resolve`,
138
+ // the `&Quill`-taking `Document._commitField` and friends): its `_assertClass`
139
+ // is emitted unconditionally and runs before any of our code. It throws a bare
140
+ // `Error` reading `expected instance of Document` at a value that IS a
141
+ // `Document`, so `isQuillmarkError` returns false and the failure leaves this
142
+ // package's error contract, naming neither the cause nor the cure. The checks
143
+ // front-run it with a `QuillmarkError` that names both.
144
+ //
145
+ // They also cover the seams with NO `_assertClass` to front-run: `Engine` and
146
+ // `LiveSession.apply` cross into backend memory as data (`toTree`/`toJson`), so
147
+ // a foreign handle there would silently work, at the price of the round-trip
148
+ // above and a quill clone cache split per copy.
149
+ //
150
+ // Not an API widening in either direction: the declared parameter types stay
151
+ // `Quill`/`Document`, and the accepted set is exactly this copy's instances.
152
+
153
+ /** Per class: the code and hint for "not one at all", and the probe for "from another copy". */
154
+ const HANDLE_KINDS = {
155
+ Quill: {
156
+ code: 'runtime::not_a_quill',
157
+ probe: 'toTree',
158
+ hint: 'Pass a Quill built by Quill.fromTree.'
159
+ },
160
+ Document: {
161
+ code: 'runtime::not_a_document',
162
+ probe: 'toJson',
163
+ hint: 'Pass a Document built by Document.fromMarkdown / fromJson or quill.seedDocument.'
164
+ }
165
+ };
166
+
167
+ /**
168
+ * The rejection for a value that is not one of this copy's handles. Two cures,
169
+ * so two diagnostics: a value carrying the class's serializer is that class
170
+ * from ANOTHER copy (dedupe the install), anything else is the wrong argument
171
+ * (fix the call).
172
+ * @param {unknown} value
173
+ * @param {string} method
174
+ * @param {'Quill' | 'Document'} className
175
+ * @returns {Error & { diagnostics: import('../core/wasm.js').Diagnostic[] }}
176
+ */
177
+ function notLocal(value, method, className) {
178
+ const { code, probe, hint } = HANDLE_KINDS[className];
179
+ if (value && typeof (/** @type {any} */ (value)[probe]) === 'function') {
180
+ return quillmarkError(
181
+ 'runtime::foreign_handle',
182
+ `${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.`,
183
+ 'Two copies of @quillmark/wasm are installed. Run `npm ls @quillmark/wasm` and dedupe to one.'
184
+ );
185
+ }
186
+ return quillmarkError(
187
+ code,
188
+ `${method}: expected a ${className}, got ${value === null ? 'null' : typeof value}.`,
189
+ hint
190
+ );
191
+ }
192
+
193
+ /**
194
+ * Throw unless `doc` is THIS copy's `Document`.
195
+ * @param {unknown} doc
196
+ * @param {string} method
197
+ * @returns {void}
198
+ */
199
+ function requireLocalDoc(doc, method) {
200
+ if (doc instanceof Document) return;
201
+ throw notLocal(doc, method, 'Document');
202
+ }
203
+
204
+ /**
205
+ * Throw unless `quill` is THIS copy's `Quill`.
206
+ * @param {unknown} quill
207
+ * @param {string} method
208
+ * @returns {void}
209
+ */
210
+ function requireLocalQuill(quill, method) {
211
+ if (quill instanceof Quill) return;
212
+ throw notLocal(quill, method, 'Quill');
213
+ }
214
+
215
+ // Marker for the patches below. `Symbol.for`, not a module-local `Symbol()`:
216
+ // re-evaluating THIS module (Vite HMR, a Vitest worker sharing a module graph)
217
+ // against an already-patched (because cached) core build must see the existing
218
+ // marker, or each pass wraps the previous wrapper.
219
+ const HANDLE_CHECKED = Symbol.for('@quillmark/wasm:handle-checked');
220
+
221
+ /**
222
+ * Replace `proto[name]` with `wrap(original)`, once.
223
+ * @param {object} proto
224
+ * @param {string} name
225
+ * @param {(original: Function) => Function} wrap
226
+ */
227
+ function patchHandleChecked(proto, name, wrap) {
228
+ const original = /** @type {any} */ (proto)[name];
229
+ if (typeof original !== 'function' || original[HANDLE_CHECKED]) return;
230
+ const patched = wrap(original);
231
+ /** @type {any} */ (patched)[HANDLE_CHECKED] = true;
232
+ // Keep the method's name so a stack trace still reads `Quill.validate`.
233
+ Object.defineProperty(patched, 'name', { value: name, configurable: true });
234
+ /** @type {any} */ (proto)[name] = patched;
235
+ }
236
+
237
+ // The three core methods declaring a `&Document` parameter. Each already refuses
238
+ // a foreign handle inside `_assertClass`; the patch is what makes the refusal
239
+ // legible. Only the argument can be foreign, the receiver being whichever copy
240
+ // the caller reached for.
241
+ patchHandleChecked(Document.prototype, 'equals', (original) =>
242
+ function equals(/** @type {any} */ other) {
243
+ requireLocalDoc(other, 'Document.equals');
244
+ return original.call(this, other);
245
+ }
246
+ );
247
+ for (const name of /** @type {const} */ (['validate', 'resolve'])) {
248
+ // Named once per patch, not per call: `validate` runs per keystroke.
249
+ const method = `Quill.${name}`;
250
+ patchHandleChecked(Quill.prototype, name, (original) =>
251
+ function (/** @type {any} */ doc) {
252
+ requireLocalDoc(doc, method);
253
+ return original.call(this, doc);
254
+ }
255
+ );
256
+ }
257
+
258
+ // The typed writer/reader primitives (`Document._commitField` and friends) take
259
+ // the QUILL by reference, so they hit the same `_assertClass` from the other
260
+ // direction. Checked at the four writer/reader classes below, not patched onto
261
+ // `Document`: a foreign document carries its OWN prototype, so patching this
262
+ // copy's would never run.
263
+
101
264
  // ── Open-set discriminant guards ────────────────────────────────────────────
102
- // `ContentIsland.type` and `ContentMark.type` are OPEN sets: each union carries
103
- // a residual `{ type: string; }` arm, so a bare `x.type === 'table'` check
104
- // never narrows the payload TS keeps the residual arm live (a `string` can be
105
- // `'table'`), leaving `props` / the mark payload opaque at every consumer. These
265
+ // `ContentIsland.type`, `ContentMark.type`, `ContentLine.kind`, and
266
+ // `ContentContainer.container` are OPEN sets: each union carries a residual
267
+ // `{ …: string; }` arm, so a bare `x.type === 'table'` check never narrows the
268
+ // payload, TS keeps the residual arm live (a `string` can be `'table'`),
269
+ // leaving `props` / the mark payload / `level` opaque at every consumer. These
106
270
  // are the checked narrowing path: on the true branch the payload's pinned shape
107
- // is asserted. Only the payload-carrying arms get a guard an island always
108
- // carries `props`, a `link` mark carries `url`, an `anchor` mark carries `id`;
109
- // the bare marks (`strong`/`emph`/`underline`/`strike`/`code`) narrow to
110
- // nothing. An unrecognized `type` fails every guard and keeps its opaque payload.
271
+ // is asserted. Only the payload-carrying arms get a guard: an island always
272
+ // carries `props`, a `link` mark carries `url`, an `anchor` mark carries `id`, a
273
+ // `heading` line carries `level` and a `code` line `lang`, a `list_item`
274
+ // container its shape; the payload-free arms (`strong`/`emph`/`underline`/
275
+ // `strike`/`code` marks, `para`/`island`/`rule` lines, `quote`) narrow to
276
+ // nothing. An unrecognized discriminant fails every guard and keeps its opaque
277
+ // `attrs`/`props`.
111
278
 
112
279
  /**
113
280
  * @param {import('../core/wasm.js').ContentIsland} island
@@ -141,7 +308,90 @@ export function isAnchorMark(mark) {
141
308
  return mark.type === 'anchor';
142
309
  }
143
310
 
144
- // Backend builds are NEVER statically imported here — that would pull a
311
+ /**
312
+ * @param {import('../core/wasm.js').ContentLine} line
313
+ * @returns {line is import('../core/wasm.js').ContentLine & { kind: 'heading'; level: number }}
314
+ */
315
+ export function isHeadingLine(line) {
316
+ return line.kind === 'heading';
317
+ }
318
+
319
+ /**
320
+ * @param {import('../core/wasm.js').ContentLine} line
321
+ * @returns {line is import('../core/wasm.js').ContentLine & { kind: 'code'; lang?: string }}
322
+ */
323
+ export function isCodeLine(line) {
324
+ return line.kind === 'code';
325
+ }
326
+
327
+ /**
328
+ * @param {import('../core/wasm.js').ContentContainer} container
329
+ * @returns {container is import('../core/wasm.js').ContentContainer & { container: 'list_item'; ordered: boolean; start: number; ordinal: number }}
330
+ */
331
+ export function isListItemContainer(container) {
332
+ return container.container === 'list_item';
333
+ }
334
+
335
+ // ── Open-set membership guards ──────────────────────────────────────────────
336
+ // The guards above each answer "is this arm X": one pinned arm at a time. These
337
+ // four answer the other question: "is this a value this build knows?" A consumer
338
+ // that must branch known-vs-unknown, any read-modify-write consumer, since
339
+ // lowering an edit restates every line's kind and containers, otherwise
340
+ // enumerates the built-in names in its own source, recreating the closed-set
341
+ // coupling the open set exists to remove. That list is correct until the release
342
+ // that adds a built-in, at which point the new construct is misclassified as
343
+ // unknown and round-trips through the consumer's unknown carrier, losing any
344
+ // sibling-key payload.
345
+ //
346
+ // A predicate rather than an exported name list, because the known tables below
347
+ // are upstream's business. They are pinned against the Rust source
348
+ // (`Content::RESERVED_*` and `KnownIslandType`) by the
349
+ // `known_open_set_names_are_pinned` drift-guard test in
350
+ // `crates/content/src/model.rs`: adding a built-in means editing there, here, and
351
+ // the TS unions in `crates/bindings/wasm/src/engine.rs` in one commit.
352
+ //
353
+ // These classify unknown *tags*, not unknown *payloads on known tags*. A future
354
+ // `kind: "footnote"` with a sibling `ref` loses `ref` at a consumer that predates
355
+ // it either way.
356
+
357
+ const KNOWN_LINE_KINDS = new Set(['para', 'heading', 'code', 'island', 'rule']);
358
+ const KNOWN_CONTAINERS = new Set(['list_item', 'quote']);
359
+ const KNOWN_MARK_TYPES = new Set(['strong', 'emph', 'underline', 'strike', 'code', 'link', 'anchor']);
360
+ const KNOWN_ISLAND_TYPES = new Set(['table', 'image']);
361
+
362
+ /**
363
+ * @param {import('../core/wasm.js').ContentLine} line
364
+ * @returns {line is import('../core/wasm.js').ContentLine & { kind: string; attrs: unknown }}
365
+ */
366
+ export function isUnknownLine(line) {
367
+ return typeof line?.kind === 'string' && !KNOWN_LINE_KINDS.has(line.kind);
368
+ }
369
+
370
+ /**
371
+ * @param {import('../core/wasm.js').ContentContainer} container
372
+ * @returns {container is import('../core/wasm.js').ContentContainer & { container: string; attrs: unknown }}
373
+ */
374
+ export function isUnknownContainer(container) {
375
+ return typeof container?.container === 'string' && !KNOWN_CONTAINERS.has(container.container);
376
+ }
377
+
378
+ /**
379
+ * @param {import('../core/wasm.js').ContentMark} mark
380
+ * @returns {mark is import('../core/wasm.js').ContentMark & { type: string; attrs: unknown }}
381
+ */
382
+ export function isUnknownMark(mark) {
383
+ return typeof mark?.type === 'string' && !KNOWN_MARK_TYPES.has(mark.type);
384
+ }
385
+
386
+ /**
387
+ * @param {import('../core/wasm.js').ContentIsland} island
388
+ * @returns {island is import('../core/wasm.js').ContentIsland & { type: string; props: unknown }}
389
+ */
390
+ export function isUnknownIsland(island) {
391
+ return typeof island?.type === 'string' && !KNOWN_ISLAND_TYPES.has(island.type);
392
+ }
393
+
394
+ // Backend builds are NEVER statically imported here: that would pull a
145
395
  // multi-MB binary into the eager graph and defeat lazy loading. Each entry is a
146
396
  // DESCRIPTOR: `load` is a thunk returning a dynamic `import()` (a backend's
147
397
  // chunk is fetched only when something actually renders against that backend),
@@ -172,7 +422,7 @@ const DEFAULT_BACKENDS = {
172
422
  * backend id on any malformed entry. Descriptors are the ONLY accepted form:
173
423
  * `{ load, formats, canvas }` with a callable `load`, a `formats` array, and a
174
424
  * boolean `canvas`. Failing at construction (not deep inside a render) keeps the
175
- * capability probes free they can answer from the manifest unconditionally.
425
+ * capability probes free; they can answer from the manifest unconditionally.
176
426
  * @param {string} id
177
427
  * @param {unknown} entry
178
428
  * @returns {{ load: () => Promise<unknown>, formats: string[], canvas: boolean }}
@@ -221,7 +471,7 @@ export class Engine {
221
471
  * @param {{ backends?: Record<string, { load: () => Promise<unknown>, formats: string[], canvas: boolean }> }} [options]
222
472
  * Extra or overriding backend descriptors, merged over the built-ins. Each
223
473
  * entry is a descriptor (`{ load, formats, canvas }`) with `formats` and
224
- * `canvas` REQUIRED that static manifest is what makes
474
+ * `canvas` REQUIRED: that static manifest is what makes
225
475
  * `supportedFormats`/`supportsCanvas` always free (no binary load, no quill
226
476
  * clone). Malformed entries throw here, at construction. The default
227
477
  * registry maps `"typst"` to the bundled Typst build.
@@ -238,7 +488,7 @@ export class Engine {
238
488
 
239
489
  /**
240
490
  * Look up the registered descriptor for `backendId`, throwing the canonical
241
- * "no backend registered" error if none. Pure touches no binary.
491
+ * "no backend registered" error if none. Pure, touches no binary.
242
492
  * @param {string} backendId
243
493
  * @returns {{ load: () => Promise<unknown>, formats: string[], canvas: boolean }}
244
494
  */
@@ -253,6 +503,20 @@ export class Engine {
253
503
  return descriptor;
254
504
  }
255
505
 
506
+ /**
507
+ * `quill`'s backend id, after checking the handle. The ONE way an `Engine`
508
+ * verb reaches `backendId`, so "no verb touches a foreign quill" is
509
+ * structural rather than four remembered calls. See § "Handles from another
510
+ * copy".
511
+ * @param {Quill} quill
512
+ * @param {string} method the caller's name, for the rejection message
513
+ * @returns {string}
514
+ */
515
+ #backendOf(quill, method) {
516
+ requireLocalQuill(quill, method);
517
+ return quill.backendId;
518
+ }
519
+
256
520
  /**
257
521
  * Resolve (and lazily load) the backend module + its engine for `backendId`.
258
522
  * @param {string} backendId
@@ -286,9 +550,9 @@ export class Engine {
286
550
 
287
551
  /**
288
552
  * Get (or materialize-and-cache) the backend-memory `Quill` clone for
289
- * `quill` under `backendId`. On a cache miss the clone is built from `tree`
290
- * the caller's pre-await `toTree()` snapshot; the canonical handle may be
291
- * freed by now and stored in the per-backend `WeakMap` keyed on the
553
+ * `quill` under `backendId`. On a cache miss the clone is built from `tree`,
554
+ * the caller's pre-await `toTree()` snapshot; the canonical handle may be
555
+ * freed by now, and stored in the per-backend `WeakMap` keyed on the
292
556
  * canonical `Quill` instance, so a later call with the same instance reuses
293
557
  * it (the hot-path fix: no re-serialize / re-copy / re-validate per call).
294
558
  * @param {any} mod the backend build module
@@ -318,33 +582,39 @@ export class Engine {
318
582
  *
319
583
  * OWNERSHIP WINDOW: both caller handles are snapshotted (`doc.toJson()`, and
320
584
  * `quill.toTree()` on a clone-cache miss) BEFORE the first await. The backend
321
- * load below is a real suspension point a multi-MB `import()` on first
322
- * render so reading the handles after it would race a caller that
585
+ * load below is a real suspension point (a multi-MB `import()` on first
586
+ * render) so reading the handles after it would race a caller that
323
587
  * `free()`s them as soon as this call returns its promise ("null pointer
324
588
  * passed to rust"). The snapshot makes that natural calling pattern correct.
325
589
  *
326
- * Clone lifetimes differ by design: the `doc` clone is TRANSIENT freed in
590
+ * Clone lifetimes differ by design: the `doc` clone is TRANSIENT, freed in
327
591
  * the `finally` of every call. The `quill` clone is CACHED per (engine,
328
592
  * backend, canonical quill instance) and is NOT freed here; a `Quill`
329
593
  * instance's contents never change after construction, so it is dropped with
330
594
  * the canonical quill (WeakMap collection → wasm-bindgen weak-ref free) when
331
595
  * the consumer replaces the instance. A cache miss materializes it once;
332
596
  * subsequent calls reuse it.
333
- * @param {string} backendId
334
- * @param {{ toTree(): Map<string, Uint8Array> }} quill
335
- * @param {{ toJson(): string }} doc
597
+ *
598
+ * Both handles are checked here, so the crossing is core→backend (always two
599
+ * memories, always as data); core→core is a duplicate install and never gets
600
+ * past the first line. See § "Handles from another copy".
601
+ * @param {string} method the caller's name, for the rejection message
602
+ * @param {Quill} quill
603
+ * @param {Document} doc
336
604
  * @param {(ctx: { mod: any, engine: any, quill: any, doc: any }) => any} fn
337
605
  */
338
- async #withClones(backendId, quill, doc, fn) {
606
+ async #withClones(method, quill, doc, fn) {
607
+ const backendId = this.#backendOf(quill, method);
608
+ requireLocalDoc(doc, method);
339
609
  const docJson = doc.toJson();
340
610
  const quillTree = this.#quillClones.get(backendId)?.has(quill) ? null : quill.toTree();
341
611
  const { mod, engine } = await this.#resolveBackend(backendId);
342
612
  // The quill clone is cached (see #cachedQuillClone); only the per-call doc
343
613
  // clone is transient. Bring the doc clone + `fn` under one try so the doc
344
- // clone is freed even if a later step throws (e.g. `Document.fromJson`
345
- // rejecting a cross-version DTO). The cached quill clone is intentionally
346
- // NOT freed here. `fn` MUST be synchronous the doc clone is freed as soon
347
- // as it returns, so an async `fn` would have it freed mid-flight.
614
+ // clone is freed even if a later step throws. The cached quill clone is
615
+ // intentionally NOT freed here. `fn` MUST be synchronous: the doc clone is
616
+ // freed as soon as it returns, so an async `fn` would have it freed
617
+ // mid-flight.
348
618
  const backendQuill = this.#cachedQuillClone(mod, backendId, quill, quillTree);
349
619
  let backendDoc = null;
350
620
  try {
@@ -365,7 +635,7 @@ export class Engine {
365
635
  * @returns {Promise<import('./runtime.js').RenderResult>}
366
636
  */
367
637
  async render(quill, doc, options) {
368
- return this.#withClones(quill.backendId, quill, doc, ({ engine, quill: q, doc: d }) =>
638
+ return this.#withClones('engine.render(quill, doc)', quill, doc, ({ engine, quill: q, doc: d }) =>
369
639
  engine.render(q, d, options ?? undefined)
370
640
  );
371
641
  }
@@ -383,7 +653,7 @@ export class Engine {
383
653
  */
384
654
  async open(quill, doc) {
385
655
  return this.#withClones(
386
- quill.backendId,
656
+ 'engine.open(quill, doc)',
387
657
  quill,
388
658
  doc,
389
659
  ({ mod, engine, quill: q, doc: d }) => new LiveSession(engine.open(q, d), mod)
@@ -393,30 +663,30 @@ export class Engine {
393
663
  /**
394
664
  * The output formats `quill`'s backend can emit. A cheap, non-failing,
395
665
  * ALWAYS-free pre-render probe: it answers from the descriptor's required
396
- * `formats` manifest NO binary load and NO quill clone depending only on
666
+ * `formats` manifest (NO binary load and NO quill clone) depending only on
397
667
  * `quill.backendId`. Stays `async` for API stability (it never awaits a load).
398
668
  * @param {Quill} quill
399
669
  * @returns {Promise<import('./runtime.js').OutputFormat[]>}
400
670
  */
401
671
  async supportedFormats(quill) {
402
- const descriptor = this.#descriptorFor(quill.backendId);
672
+ const descriptor = this.#descriptorFor(this.#backendOf(quill, 'engine.supportedFormats(quill)'));
403
673
  // Defensive copy so callers can't mutate the shared manifest.
404
674
  return descriptor.formats.slice();
405
675
  }
406
676
 
407
677
  /**
408
- * Whether `quill`'s BACKEND can paint sessions to a canvas a pre-session
678
+ * Whether `quill`'s BACKEND can paint sessions to a canvas: a pre-session
409
679
  * ESTIMATE, not a fact about any particular compile. Same ALWAYS-free probe
410
680
  * as `supportedFormats`: answered from the descriptor's required `canvas`
411
681
  * manifest, no load and no clone. A specific compile can still refuse to
412
682
  * paint (e.g. a 0-page document), so this can answer `true` while the
413
- * resulting `LiveSession.supportsCanvas` answers `false` gate mounting a
683
+ * resulting `LiveSession.supportsCanvas` answers `false`: gate mounting a
414
684
  * canvas UI on this, gate the actual `paint` call on the session's getter.
415
685
  * @param {Quill} quill
416
686
  * @returns {Promise<boolean>}
417
687
  */
418
688
  async supportsCanvas(quill) {
419
- const descriptor = this.#descriptorFor(quill.backendId);
689
+ const descriptor = this.#descriptorFor(this.#backendOf(quill, 'engine.supportsCanvas(quill)'));
420
690
  return descriptor.canvas;
421
691
  }
422
692
  }
@@ -425,16 +695,16 @@ export class Engine {
425
695
  * Thin wrapper over a backend's live render session. Reads serve the current
426
696
  * compile; `apply(doc)` recompiles in place (transactional: on throw, reads
427
697
  * keep serving the last-good compile). The quill/document clones it was
428
- * opened from have already been freed the session retains what `apply`
698
+ * opened from have already been freed: the session retains what `apply`
429
699
  * needs.
430
700
  *
431
701
  * Geometry reads (`regions`, `positionAt`, `locate`) resolve against the
432
702
  * current compile; anchoring a caret or selection across edits is the editor's
433
- * job (its own transaction mapping) re-read geometry after each committed
703
+ * job (its own transaction mapping): re-read geometry after each committed
434
704
  * `apply`.
435
705
  *
436
- * `paint` writes a COMPLETE page raster all content visible, no caller-side
437
- * compositing for every backend that supports canvas (Typst rasterizes
706
+ * `paint` writes a COMPLETE page raster (all content visible, no caller-side
707
+ * compositing) for every backend that supports canvas (Typst rasterizes
438
708
  * natively; pdfform rasterizes its pre-flattened page). See `runtime.d.ts`.
439
709
  */
440
710
  export class LiveSession {
@@ -450,13 +720,14 @@ export class LiveSession {
450
720
  #mod;
451
721
 
452
722
  /**
453
- * Recompile the session against `doc` the edit verb of a live preview.
723
+ * Recompile the session against `doc`: the edit verb of a live preview.
454
724
  * Transactional: on throw every read keeps serving the last-good compile.
455
725
  * On success reads serve the new compile; repaint `dirtyPages ∩ visible`.
456
726
  * @param {Document} doc
457
727
  * @returns {import('./runtime.d.ts').ChangeSet}
458
728
  */
459
729
  apply(doc) {
730
+ requireLocalDoc(doc, 'session.apply(doc)');
460
731
  let backendDoc = null;
461
732
  try {
462
733
  backendDoc = this.#mod.Document.fromJson(doc.toJson());
@@ -473,7 +744,7 @@ export class LiveSession {
473
744
  return this.#inner.backendId;
474
745
  }
475
746
  /**
476
- * `true` iff `paint`/`pageSize` will succeed for THIS compile the
747
+ * `true` iff `paint`/`pageSize` will succeed for THIS compile: the
477
748
  * authoritative answer, derived from the session's canvas seam, so it can
478
749
  * never disagree with what `paint` actually does. This can be `false` even
479
750
  * when `Engine.supportsCanvas` answered `true` for the same `quill` (that
@@ -495,7 +766,7 @@ export class LiveSession {
495
766
  }
496
767
 
497
768
  /**
498
- * Schema-field geometry for this compiled session one region per
769
+ * Schema-field geometry for this compiled session: one region per
499
770
  * schema-bound field, keyed on its quill schema field path. A session-level
500
771
  * query (no render); read it to place field overlays / cross-navigation over
501
772
  * a `paint`-ed canvas.
@@ -506,11 +777,11 @@ export class LiveSession {
506
777
  }
507
778
 
508
779
  /**
509
- * The whole-field highlight boxes for `field` one union rect per page,
780
+ * The whole-field highlight boxes for `field`: one union rect per page,
510
781
  * over the field's `span`-bearing content segments. Owns the union
511
782
  * `regions()` leaves derived (span-filter + per-page union), so a "highlight
512
783
  * the focused field" consumer stops reimplementing it. Content only: a field
513
- * placed solely as a scalar reference or a bound widget returns `[]` its
784
+ * placed solely as a scalar reference or a bound widget returns `[]`, its
514
785
  * box is a single `regions()` rect.
515
786
  * @param {string} field
516
787
  * @returns {import('./runtime.d.ts').FieldRegion[]}
@@ -520,7 +791,7 @@ export class LiveSession {
520
791
  }
521
792
 
522
793
  /**
523
- * The schema field whose content is under a point on `page` the forward
794
+ * The schema field whose content is under a point on `page`: the forward
524
795
  * (click → field) direction, resolving *every* placement, not just the first
525
796
  * that `regions` enumerates. `x`/`y` are PDF points with a bottom-left origin
526
797
  * (the `FieldRegion.rect` space). See `runtime.d.ts` for the click-to-point
@@ -559,8 +830,8 @@ export class LiveSession {
559
830
  }
560
831
 
561
832
  /**
562
- * Paint `page` into a 2D canvas context. The painted raster is COMPLETE
563
- * all page content visible, no caller-side compositing for both the Typst
833
+ * Paint `page` into a 2D canvas context. The painted raster is COMPLETE
834
+ * (all page content visible, no caller-side compositing) for both the Typst
564
835
  * and pdfform backends. See `runtime.d.ts` for the DPR/clamp math and the
565
836
  * region-overlay coordinate transform.
566
837
  * @param {CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D} ctx
@@ -579,21 +850,21 @@ export class LiveSession {
579
850
  // ── Typed-writer sugar: bind the quill once ─────────────────────────────────
580
851
  // Rust exposes `quill.writer(&mut doc)` so a caller issues bare `set` / `set_all`
581
852
  // without threading the schema per write. The WASM `commit*` verbs can't borrow
582
- // like that a `Document` carries only a `$quill` REFERENCE, not the resolved
853
+ // like that: a `Document` carries only a `$quill` REFERENCE, not the resolved
583
854
  // schema, so each `commit*` method takes the `quill` handle as its first
584
855
  // argument. These pure-JS classes restore the Rust ergonomics: bind `quill` +
585
856
  // `doc` once, then issue `set` / `setAll` / `card(i).set`.
586
857
  //
587
- // They hold JS references to the caller's EXISTING handles no WASM object of
588
- // their own, no `free()` burden, no second owner of either handle and every
858
+ // They hold JS references to the caller's EXISTING handles (no WASM object of
859
+ // their own, no `free()` burden, no second owner of either handle) and every
589
860
  // write delegates straight to the underlying `commit*` verb: a schema field is
590
861
  // typed-committed (coerced to canonical form, mismatch throws now), and a name
591
862
  // the schema does not declare throws `UnknownField` rather than falling to the
592
- // opaque store on the typed path an undeclared name is a typo. Opaque storage
863
+ // opaque store, on the typed path an undeclared name is a typo. Opaque storage
593
864
  // stays available through the raw addressed `Document.storeField` verb.
594
865
 
595
866
  /**
596
- * A {@link Document} bound to its {@link Quill} for typed writes the JS twin
867
+ * A {@link Document} bound to its {@link Quill} for typed writes: the JS twin
597
868
  * of Rust's `quill.writer(&mut doc)`. Writes target the main card; use
598
869
  * {@link card} for a composable card. Holds both handles by reference and owns
599
870
  * neither, so there is nothing to `free()`.
@@ -606,10 +877,12 @@ export class DocumentWriter {
606
877
  * @param {Document} doc the document to mutate, held by reference (not owned)
607
878
  */
608
879
  constructor(quill, doc) {
880
+ requireLocalQuill(quill, 'quill.writer(doc)');
881
+ requireLocalDoc(doc, 'quill.writer(doc)');
609
882
  this.#quill = quill;
610
883
  this.#doc = doc;
611
884
  }
612
- /** The bound document the same instance passed in, mutated in place. */
885
+ /** The bound document: the same instance passed in, mutated in place. */
613
886
  get document() {
614
887
  return this.#doc;
615
888
  }
@@ -624,7 +897,7 @@ export class DocumentWriter {
624
897
  return this.#doc._commitField(this.#quill, name, value);
625
898
  }
626
899
  /**
627
- * Typed-commit several main-card fields atomically nothing is applied on
900
+ * Typed-commit several main-card fields atomically: nothing is applied on
628
901
  * error (throws a per-field diagnostic bundle, including an `UnknownField`
629
902
  * for each undeclared name).
630
903
  * @param {Record<string, unknown>} fields
@@ -635,7 +908,7 @@ export class DocumentWriter {
635
908
  }
636
909
  /**
637
910
  * Set the main body from markdown (edit semantics: surviving anchors rebase),
638
- * discarding the text delta the receipt-free body write. Call
911
+ * discarding the text delta, the receipt-free body write. Call
639
912
  * `doc.revise({}, md)` for the {@link Delta} receipt.
640
913
  * @param {string} markdown
641
914
  * @returns {void}
@@ -644,7 +917,7 @@ export class DocumentWriter {
644
917
  this.#doc.revise({}, markdown);
645
918
  }
646
919
  /**
647
- * Revise the richtext main-card field `name` from markdown typed *and*
920
+ * Revise the richtext main-card field `name` from markdown: typed *and*
648
921
  * anchor-preserving. Surviving anchors rebase, then the diffed result is
649
922
  * schema-conformed (`richtext(inline)` rejects a multi-block result). Throws
650
923
  * `UnknownField` for a name the schema does not declare. Returns the text
@@ -658,7 +931,7 @@ export class DocumentWriter {
658
931
  }
659
932
  /**
660
933
  * Build a composable card of `kind`, typed-commit `fields` onto it, set its
661
- * body from optional markdown, and place it the fused `makeCard` + typed
934
+ * body from optional markdown, and place it: the fused `makeCard` + typed
662
935
  * commit + insertion. `at` picks the position: omitted appends, a number
663
936
  * inserts at that index (`0..=cardCount`), so a positioned typed insert is one
664
937
  * atomic call rather than `addCard` + `moveCard`. Transactional: the card is
@@ -676,7 +949,7 @@ export class DocumentWriter {
676
949
  }
677
950
  /**
678
951
  * Remove the composable card at `index`, returning it (or `undefined` if the
679
- * index is out of range) the writer spelling of `Document.removeCard`.
952
+ * index is out of range): the writer spelling of `Document.removeCard`.
680
953
  * @param {number} index
681
954
  * @returns {import('../core/wasm.js').Card | undefined}
682
955
  */
@@ -686,9 +959,10 @@ export class DocumentWriter {
686
959
  /**
687
960
  * A {@link CardWriter} bound to the composable card at `index`. Index
688
961
  * validity is checked lazily by the underlying write (it throws
689
- * `IndexOutOfRange` at commit time), so constructing one never throws.
962
+ * `IndexOutOfRange` at commit time), so an out-of-range index does not throw
963
+ * here.
690
964
  *
691
- * The cursor is ephemeral bind, write, discard. It holds `index`, not the
965
+ * The cursor is ephemeral: bind, write, discard. It holds `index`, not the
692
966
  * card: a `removeCard`/`addCard` between binding and writing silently
693
967
  * retargets it. For durable addressing stamp `$id` and re-resolve the index
694
968
  * at write time.
@@ -715,6 +989,8 @@ export class CardWriter {
715
989
  * @param {number} index the composable card's index
716
990
  */
717
991
  constructor(quill, doc, index) {
992
+ requireLocalQuill(quill, 'writer.card(index)');
993
+ requireLocalDoc(doc, 'writer.card(index)');
718
994
  this.#quill = quill;
719
995
  this.#doc = doc;
720
996
  this.#index = index;
@@ -725,7 +1001,7 @@ export class CardWriter {
725
1001
  }
726
1002
  /**
727
1003
  * The bound card's `$kind` (empty string when it carries none), read through
728
- * the document mirrors core `CardWriter::kind()`. Ephemeral like the cursor
1004
+ * the document: mirrors core `CardWriter::kind()`. Ephemeral like the cursor
729
1005
  * itself: throws `IndexOutOfRange` if the bound index is out of range.
730
1006
  * @returns {string}
731
1007
  */
@@ -754,7 +1030,7 @@ export class CardWriter {
754
1030
  return this.#doc._commitFields(this.#quill, { card: this.#index }, fields);
755
1031
  }
756
1032
  /**
757
- * Set this card's body from markdown (edit semantics), discarding the delta
1033
+ * Set this card's body from markdown (edit semantics), discarding the delta:
758
1034
  * the card twin of {@link DocumentWriter.setBody}.
759
1035
  * @param {string} markdown
760
1036
  * @returns {void}
@@ -763,7 +1039,7 @@ export class CardWriter {
763
1039
  this.#doc.revise({ card: this.#index }, markdown);
764
1040
  }
765
1041
  /**
766
- * Revise the richtext field `name` on this card from markdown typed *and*
1042
+ * Revise the richtext field `name` on this card from markdown: typed *and*
767
1043
  * anchor-preserving; the card twin of {@link DocumentWriter.reviseField}.
768
1044
  * Throws `UnknownField` for an undeclared name and `IndexOutOfRange` if the
769
1045
  * bound index is out of range. Returns the text {@link Delta}.
@@ -776,16 +1052,16 @@ export class CardWriter {
776
1052
  }
777
1053
  }
778
1054
 
779
- // ── `quill.writer(doc)` the typed front door ──────────────────────────────
1055
+ // ── `quill.writer(doc)`, the typed front door ──────────────────────────────
780
1056
  // The schema-bound writer: bind the quill's schema to a document and issue bare
781
- // typed writes. Mirrors core's `quill.writer(&mut doc)` the schema grants the
1057
+ // typed writes. Mirrors core's `quill.writer(&mut doc)`: the schema grants the
782
1058
  // typing, so the quill (not the document) is the factory. Patched onto the
783
1059
  // re-exported `Quill` prototype rather than wrapped: `Quill === CoreQuill`
784
1060
  // stays true (the identity invariant above); this only adds a method that
785
1061
  // constructs the pure-JS writer, which owns no WASM handle.
786
1062
  /**
787
1063
  * A {@link DocumentWriter} binding this quill's schema to `doc` for typed
788
- * writes the documented front door. The returned writer holds both handles by
1064
+ * writes: the documented front door. The returned writer holds both handles by
789
1065
  * reference and owns neither, so there is nothing to `free()`. Ephemeral by
790
1066
  * convention: bind, write, discard.
791
1067
  * @this {Quill}
@@ -797,8 +1073,8 @@ Quill.prototype.writer = function writer(doc) {
797
1073
  };
798
1074
 
799
1075
  // ── Typed-reader sugar: the schema-plane read surface ──────────────────────────
800
- // The read twin of the writer above. The transport `Document.getStored` is schema-free
801
- // a `Document` cannot say which fields are richtext, so an unknown field name
1076
+ // The read twin of the writer above. The transport `Document.getStored` is schema-free:
1077
+ // a `Document` cannot say which fields are richtext, so an unknown field name
802
1078
  // reads back `undefined` rather than as the typo it is. Binding the quill's
803
1079
  // schema (`_readerGet` takes the handle, like the `commit*` verbs) lets one `get`
804
1080
  // interpret by declared type: a richtext field to markdown, a plaintext field to
@@ -808,7 +1084,7 @@ Quill.prototype.writer = function writer(doc) {
808
1084
  // reference, own no WASM object, and have nothing to `free()`.
809
1085
 
810
1086
  /**
811
- * A {@link Document} bound to its {@link Quill} for typed reads the JS twin of
1087
+ * A {@link Document} bound to its {@link Quill} for typed reads: the JS twin of
812
1088
  * Rust's `quill.reader(&doc)` and the read counterpart of {@link DocumentWriter}.
813
1089
  * Reads target the main card; use {@link card} for a composable card. Holds both
814
1090
  * handles by reference and owns neither, so there is nothing to `free()`.
@@ -821,10 +1097,12 @@ export class DocumentReader {
821
1097
  * @param {Document} doc the document to read, held by reference (not owned)
822
1098
  */
823
1099
  constructor(quill, doc) {
1100
+ requireLocalQuill(quill, 'quill.reader(doc)');
1101
+ requireLocalDoc(doc, 'quill.reader(doc)');
824
1102
  this.#quill = quill;
825
1103
  this.#doc = doc;
826
1104
  }
827
- /** The bound document the same instance passed in. */
1105
+ /** The bound document: the same instance passed in. */
828
1106
  get document() {
829
1107
  return this.#doc;
830
1108
  }
@@ -842,7 +1120,7 @@ export class DocumentReader {
842
1120
  return this.#doc._readerGet(this.#quill, addr);
843
1121
  }
844
1122
  /**
845
- * The main body's markdown the quill-free body read (a body's type is a
1123
+ * The main body's markdown: the quill-free body read (a body's type is a
846
1124
  * format fact, not a schema fact). Equivalent to `get({})`.
847
1125
  * @returns {string}
848
1126
  */
@@ -852,9 +1130,9 @@ export class DocumentReader {
852
1130
  /**
853
1131
  * A {@link CardReader} bound to the composable card at `index`. Index validity
854
1132
  * is checked lazily by the underlying read (it throws `IndexOutOfRange` at read
855
- * time), so constructing one never throws. Ephemeral like the writer cursor —
856
- * it holds `index`, not the card, so a `removeCard`/`addCard` between binding
857
- * and reading silently retargets it.
1133
+ * time), so an out-of-range index does not throw here. Ephemeral like the
1134
+ * writer cursor: it holds `index`, not the card, so a `removeCard`/`addCard`
1135
+ * between binding and reading silently retargets it.
858
1136
  * @param {number} index
859
1137
  * @returns {CardReader}
860
1138
  */
@@ -878,6 +1156,8 @@ export class CardReader {
878
1156
  * @param {number} index the composable card's index
879
1157
  */
880
1158
  constructor(quill, doc, index) {
1159
+ requireLocalQuill(quill, 'reader.card(index)');
1160
+ requireLocalDoc(doc, 'reader.card(index)');
881
1161
  this.#quill = quill;
882
1162
  this.#doc = doc;
883
1163
  this.#index = index;
@@ -905,7 +1185,7 @@ export class CardReader {
905
1185
  return this.#doc._readerGet(this.#quill, { card: this.#index, field: name });
906
1186
  }
907
1187
  /**
908
- * This card's body markdown the card twin of {@link DocumentReader.getBody}.
1188
+ * This card's body markdown: the card twin of {@link DocumentReader.getBody}.
909
1189
  * @returns {string}
910
1190
  */
911
1191
  getBody() {
@@ -913,13 +1193,13 @@ export class CardReader {
913
1193
  }
914
1194
  }
915
1195
 
916
- // ── `quill.reader(doc)` the schema-plane read front door ─────────────────────
1196
+ // ── `quill.reader(doc)`: the schema-plane read front door ─────────────────────
917
1197
  // The read twin of `quill.writer(doc)`, patched onto the same re-exported `Quill`
918
- // prototype (the `Quill === CoreQuill` identity invariant holds this only adds
1198
+ // prototype (the `Quill === CoreQuill` identity invariant holds: this only adds
919
1199
  // a method constructing the pure-JS reader, which owns no WASM handle).
920
1200
  /**
921
1201
  * A {@link DocumentReader} binding this quill's schema to `doc` for interpreted
922
- * reads the read front door, mirroring core's `quill.reader(&doc)`. The returned
1202
+ * reads: the read front door, mirroring core's `quill.reader(&doc)`. The returned
923
1203
  * reader holds both handles by reference and owns neither, so there is nothing to
924
1204
  * `free()`. Ephemeral by convention: bind, read, discard.
925
1205
  * @this {Quill}