@quillmark/wasm 0.92.1 → 0.94.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.
@@ -52,7 +52,17 @@
52
52
  // (it is) so it tolerates handles from any core instance. The `runtime.test.js`
53
53
  // "re-exports the internal core build classes verbatim" case
54
54
  // (`Quill === CoreQuill`) is the executable guard for this invariant.
55
- export { Quill, Document, init } from '../core/wasm.js';
55
+ //
56
+ // 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
58
+ // re-export keeps the identity: the exported `Quill` IS the core class.
59
+ import { Quill, Document, init } from '../core/wasm.js';
60
+ export { Quill, Document, init };
61
+ // The document-free corpus codec — re-exported verbatim from the core build so
62
+ // the runtime subpath exposes `exportMarkdown(body)` (the on-demand markdown
63
+ // projection that replaces the eager `bodyMarkdown`), `importMarkdown`, and the
64
+ // position-mapping pair (`rebase`, `mapPos`).
65
+ export { importMarkdown, exportMarkdown, rebase, mapPos } from '../core/wasm.js';
56
66
 
57
67
  /**
58
68
  * Narrow an unknown caught value to a `QuillmarkError` — the error every
@@ -79,15 +89,22 @@ export function isQuillmarkError(e) {
79
89
  // and `formats`/`canvas` are the REQUIRED static capability manifest so the
80
90
  // cheap probes (`supportedFormats`/`supportsCanvas`) ALWAYS answer without
81
91
  // loading the binary or cloning the quill. The manifest values are verified
82
- // against the backend's Rust source (`crates/backends/typst/src/lib.rs`
83
- // `SUPPORTED_FORMATS` and `supports_canvas`) and pinned by the `runtime.test.js`
84
- // drift-guard test, which renders once and asserts the loaded backend reports
85
- // the same list.
92
+ // against each backend's Rust source (`crates/backends/<id>/src/lib.rs`
93
+ // `SUPPORTED_FORMATS`) and pinned by the `runtime.test.js` drift-guard test,
94
+ // which renders once and asserts the loaded backend reports the same list.
95
+ // `canvas` mirrors `quillmark_core::formats_support_canvas`: true iff the
96
+ // format list includes a visual-page format (`svg` or `png`).
86
97
  const DEFAULT_BACKENDS = {
87
98
  typst: {
88
99
  load: () => import('../backends/typst/wasm.js'),
89
100
  formats: ['pdf', 'svg', 'png'], // crates/backends/typst/src/lib.rs SUPPORTED_FORMATS
90
- canvas: true // crates/backends/typst/src/lib.rs supports_canvas() == true
101
+ canvas: true // has svg/png formats_support_canvas == true
102
+ },
103
+ pdfform: {
104
+ load: () => import('../backends/pdfform/wasm.js'),
105
+ // crates/backends/pdfform/src/lib.rs SUPPORTED_FORMATS == [Pdf, Svg, Png]
106
+ formats: ['pdf', 'svg', 'png'],
107
+ canvas: true // has svg/png → formats_support_canvas == true
91
108
  }
92
109
  };
93
110
 
@@ -210,16 +227,18 @@ export class Engine {
210
227
 
211
228
  /**
212
229
  * Get (or materialize-and-cache) the backend-memory `Quill` clone for
213
- * `quill` under `backendId`. On a cache miss the clone is built via
214
- * `toTree`→`fromTree` and stored in the per-backend `WeakMap` keyed on the
230
+ * `quill` under `backendId`. On a cache miss the clone is built from `tree`
231
+ * — the caller's pre-await `toTree()` snapshot; the canonical handle may be
232
+ * freed by now — and stored in the per-backend `WeakMap` keyed on the
215
233
  * canonical `Quill` instance, so a later call with the same instance reuses
216
234
  * it (the hot-path fix: no re-serialize / re-copy / re-validate per call).
217
235
  * @param {any} mod the backend build module
218
236
  * @param {string} backendId
219
- * @param {{ toTree(): Map<string, Uint8Array> }} quill
237
+ * @param {object} quill the canonical instance (cache key only)
238
+ * @param {Map<string, Uint8Array> | null} tree pre-await snapshot; `null` on a cache hit
220
239
  * @returns {any} the backend-memory quill clone
221
240
  */
222
- #cachedQuillClone(mod, backendId, quill) {
241
+ #cachedQuillClone(mod, backendId, quill, tree) {
223
242
  let perQuill = this.#quillClones.get(backendId);
224
243
  if (!perQuill) {
225
244
  perQuill = new WeakMap();
@@ -227,7 +246,7 @@ export class Engine {
227
246
  }
228
247
  let backendQuill = perQuill.get(quill);
229
248
  if (!backendQuill) {
230
- backendQuill = mod.Quill.fromTree(quill.toTree());
249
+ backendQuill = mod.Quill.fromTree(tree);
231
250
  perQuill.set(quill, backendQuill);
232
251
  }
233
252
  return backendQuill;
@@ -238,6 +257,13 @@ export class Engine {
238
257
  * memory and run `fn` against the backend engine. Only `render`/`open` call
239
258
  * this, so `doc` is always present.
240
259
  *
260
+ * OWNERSHIP WINDOW: both caller handles are snapshotted (`doc.toJson()`, and
261
+ * `quill.toTree()` on a clone-cache miss) BEFORE the first await. The backend
262
+ * load below is a real suspension point — a multi-MB `import()` on first
263
+ * render — so reading the handles after it would race a caller that
264
+ * `free()`s them as soon as this call returns its promise ("null pointer
265
+ * passed to rust"). The snapshot makes that natural calling pattern correct.
266
+ *
241
267
  * Clone lifetimes differ by design: the `doc` clone is TRANSIENT — freed in
242
268
  * the `finally` of every call. The `quill` clone is CACHED per (engine,
243
269
  * backend, canonical quill instance) and is NOT freed here; a `Quill`
@@ -251,6 +277,8 @@ export class Engine {
251
277
  * @param {(ctx: { mod: any, engine: any, quill: any, doc: any }) => any} fn
252
278
  */
253
279
  async #withClones(backendId, quill, doc, fn) {
280
+ const docJson = doc.toJson();
281
+ const quillTree = this.#quillClones.get(backendId)?.has(quill) ? null : quill.toTree();
254
282
  const { mod, engine } = await this.#resolveBackend(backendId);
255
283
  // The quill clone is cached (see #cachedQuillClone); only the per-call doc
256
284
  // clone is transient. Bring the doc clone + `fn` under one try so the doc
@@ -258,10 +286,10 @@ export class Engine {
258
286
  // rejecting a cross-version DTO). The cached quill clone is intentionally
259
287
  // NOT freed here. `fn` MUST be synchronous — the doc clone is freed as soon
260
288
  // as it returns, so an async `fn` would have it freed mid-flight.
261
- const backendQuill = this.#cachedQuillClone(mod, backendId, quill);
289
+ const backendQuill = this.#cachedQuillClone(mod, backendId, quill, quillTree);
262
290
  let backendDoc = null;
263
291
  try {
264
- backendDoc = mod.Document.fromJson(doc.toJson());
292
+ backendDoc = mod.Document.fromJson(docJson);
265
293
  return fn({ mod, engine, quill: backendQuill, doc: backendDoc });
266
294
  } finally {
267
295
  backendDoc?.free();
@@ -270,6 +298,8 @@ export class Engine {
270
298
 
271
299
  /**
272
300
  * Render `doc` against `quill` in one shot, returning a `RenderResult`.
301
+ * Both handles are read synchronously before the first await, so the caller
302
+ * may `free()` them as soon as this call returns.
273
303
  * @param {Quill} quill
274
304
  * @param {Document} doc
275
305
  * @param {object} [options] render options (`{ format, ppi, pages, producer }`)
@@ -282,23 +312,22 @@ export class Engine {
282
312
  }
283
313
 
284
314
  /**
285
- * Open an iterative render session (for canvas preview / per-page paint).
286
- * The session is a self-contained compiled snapshot, so the transient quill
287
- * and document clones are freed before this returns; the caller owns the
288
- * returned session and must `.free()` it.
289
- * @experimental Ships ahead of its first production consumer (the designed
290
- * canvas live-preview path); the session/paint surface may change in any
291
- * 0.x release. `render()` is the stable path.
315
+ * Open a live render session (canvas preview / per-page paint / `apply`).
316
+ * The session is self-contained (it retains what it needs for `apply`), so
317
+ * the transient quill and document clones are freed before this returns;
318
+ * the caller owns the returned session and must `.free()` it. The `quill`
319
+ * and `doc` handles are read synchronously before the first await, so the
320
+ * caller may `free()` them as soon as this call returns.
292
321
  * @param {Quill} quill
293
322
  * @param {Document} doc
294
- * @returns {Promise<RenderSession>}
323
+ * @returns {Promise<LiveSession>}
295
324
  */
296
325
  async open(quill, doc) {
297
326
  return this.#withClones(
298
327
  quill.backendId,
299
328
  quill,
300
329
  doc,
301
- ({ engine, quill: q, doc: d }) => new RenderSession(engine.open(q, d))
330
+ ({ mod, engine, quill: q, doc: d }) => new LiveSession(engine.open(q, d), mod)
302
331
  );
303
332
  }
304
333
 
@@ -317,9 +346,13 @@ export class Engine {
317
346
  }
318
347
 
319
348
  /**
320
- * Whether `quill`'s backend can paint sessions to a canvas. Same ALWAYS-free
321
- * probe as `supportedFormats`: answered from the descriptor's required
322
- * `canvas` manifest, no load and no clone.
349
+ * Whether `quill`'s BACKEND can paint sessions to a canvas a pre-session
350
+ * ESTIMATE, not a fact about any particular compile. Same ALWAYS-free probe
351
+ * as `supportedFormats`: answered from the descriptor's required `canvas`
352
+ * manifest, no load and no clone. A specific compile can still refuse to
353
+ * paint (e.g. a 0-page document), so this can answer `true` while the
354
+ * resulting `LiveSession.supportsCanvas` answers `false` — gate mounting a
355
+ * canvas UI on this, gate the actual `paint` call on the session's getter.
323
356
  * @param {Quill} quill
324
357
  * @returns {Promise<boolean>}
325
358
  */
@@ -330,15 +363,49 @@ export class Engine {
330
363
  }
331
364
 
332
365
  /**
333
- * Thin wrapper over a backend's iterative render session. Holds the compiled
334
- * snapshot; the quill/document it was opened from have already been freed.
366
+ * Thin wrapper over a backend's live render session. Reads serve the current
367
+ * compile; `apply(doc)` recompiles in place (transactional: on throw, reads
368
+ * keep serving the last-good compile). The quill/document clones it was
369
+ * opened from have already been freed — the session retains what `apply`
370
+ * needs.
371
+ *
372
+ * Geometry reads (`regions`, `positionAt`, `locate`) resolve against the
373
+ * current compile; anchoring a caret or selection across edits is the editor's
374
+ * job (its own transaction mapping) — re-read geometry after each committed
375
+ * `apply`.
376
+ *
377
+ * `paint` writes a COMPLETE page raster — all content visible, no caller-side
378
+ * compositing — for every backend that supports canvas (Typst rasterizes
379
+ * natively; pdfform rasterizes its pre-flattened page). See `runtime.d.ts`.
335
380
  */
336
- export class RenderSession {
337
- /** @param {import('../backends/typst/wasm').RenderSession} inner */
338
- constructor(inner) {
381
+ export class LiveSession {
382
+ /**
383
+ * @param {{ pageCount: number, backendId: string, supportsCanvas: boolean, warnings: any[], apply: Function, render: Function, regions: Function, pageSize: Function, paint: Function, free: Function }} inner backend-build LiveSession (typst or pdfform)
384
+ * @param {{ Document: { fromJson(json: string): any } }} mod the session's backend build, used to materialize `apply` documents in its linear memory
385
+ */
386
+ constructor(inner, mod) {
339
387
  this.#inner = inner;
388
+ this.#mod = mod;
340
389
  }
341
390
  #inner;
391
+ #mod;
392
+
393
+ /**
394
+ * Recompile the session against `doc` — the edit verb of a live preview.
395
+ * Transactional: on throw every read keeps serving the last-good compile.
396
+ * On success reads serve the new compile; repaint `dirtyPages ∩ visible`.
397
+ * @param {Document} doc
398
+ * @returns {import('./runtime.d.ts').ChangeSet}
399
+ */
400
+ apply(doc) {
401
+ let backendDoc = null;
402
+ try {
403
+ backendDoc = this.#mod.Document.fromJson(doc.toJson());
404
+ return this.#inner.apply(backendDoc);
405
+ } finally {
406
+ backendDoc?.free();
407
+ }
408
+ }
342
409
 
343
410
  get pageCount() {
344
411
  return this.#inner.pageCount;
@@ -346,6 +413,16 @@ export class RenderSession {
346
413
  get backendId() {
347
414
  return this.#inner.backendId;
348
415
  }
416
+ /**
417
+ * `true` iff `paint`/`pageSize` will succeed for THIS compile — the
418
+ * authoritative answer, derived from the session's canvas seam, so it can
419
+ * never disagree with what `paint` actually does. This can be `false` even
420
+ * when `Engine.supportsCanvas` answered `true` for the same `quill` (that
421
+ * probe is a pre-session backend estimate; e.g. a canvas-capable backend
422
+ * compiled to a 0-page document has nothing to paint). Re-check this getter
423
+ * after `open()` rather than relying on the engine hint alone.
424
+ * @returns {boolean}
425
+ */
349
426
  get supportsCanvas() {
350
427
  return this.#inner.supportsCanvas;
351
428
  }
@@ -358,12 +435,75 @@ export class RenderSession {
358
435
  return this.#inner.render(options ?? undefined);
359
436
  }
360
437
 
438
+ /**
439
+ * Schema-field geometry for this compiled session — one region per
440
+ * schema-bound field, keyed on its quill schema field path. A session-level
441
+ * query (no render); read it to place field overlays / cross-navigation over
442
+ * a `paint`-ed canvas.
443
+ * @returns {import('./runtime.d.ts').FieldRegion[]}
444
+ */
445
+ regions() {
446
+ return this.#inner.regions();
447
+ }
448
+
449
+ /**
450
+ * The whole-field highlight boxes for `field` — one union rect per page,
451
+ * over the field's `span`-bearing content segments. Owns the union
452
+ * `regions()` leaves derived (span-filter + per-page union), so a "highlight
453
+ * the focused field" consumer stops reimplementing it. Content only: a field
454
+ * placed solely as a scalar reference or a bound widget returns `[]` — its
455
+ * box is a single `regions()` rect.
456
+ * @param {string} field
457
+ * @returns {import('./runtime.d.ts').FieldRegion[]}
458
+ */
459
+ fieldBoxes(field) {
460
+ return this.#inner.fieldBoxes(field);
461
+ }
462
+
463
+ /**
464
+ * The schema field whose content is under a point on `page` — the forward
465
+ * (click → field) direction, resolving *every* placement, not just the first
466
+ * that `regions` enumerates. `x`/`y` are PDF points with a bottom-left origin
467
+ * (the `FieldRegion.rect` space). See `runtime.d.ts` for the click-to-point
468
+ * inverse transform.
469
+ * @param {number} page
470
+ * @param {number} x
471
+ * @param {number} y
472
+ * @returns {string | undefined}
473
+ */
474
+ fieldAt(page, x, y) {
475
+ return this.#inner.fieldAt(page, x, y);
476
+ }
477
+
478
+ /**
479
+ * @param {number} page
480
+ * @param {number} x
481
+ * @param {number} y
482
+ * @returns {import('./runtime.d.ts').CorpusHit | undefined}
483
+ */
484
+ positionAt(page, x, y) {
485
+ return this.#inner.positionAt(page, x, y);
486
+ }
487
+
488
+ /**
489
+ * @param {string} field
490
+ * @param {number} pos
491
+ * @returns {import('./runtime.d.ts').FieldRegion | undefined}
492
+ */
493
+ locate(field, pos) {
494
+ return this.#inner.locate(field, pos);
495
+ }
496
+
361
497
  /** @param {number} page */
362
498
  pageSize(page) {
363
499
  return this.#inner.pageSize(page);
364
500
  }
365
501
 
366
502
  /**
503
+ * Paint `page` into a 2D canvas context. The painted raster is COMPLETE —
504
+ * all page content visible, no caller-side compositing — for both the Typst
505
+ * and pdfform backends. See `runtime.d.ts` for the DPR/clamp math and the
506
+ * region-overlay coordinate transform.
367
507
  * @param {CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D} ctx
368
508
  * @param {number} page
369
509
  * @param {object} [options]
@@ -376,3 +516,188 @@ export class RenderSession {
376
516
  this.#inner.free();
377
517
  }
378
518
  }
519
+
520
+ // ── Typed-writer sugar: bind the quill once ─────────────────────────────────
521
+ // Rust exposes `quill.writer(&mut doc)` so a caller issues bare `set` / `set_all`
522
+ // without threading the schema per write. The WASM `commit*` verbs can't borrow
523
+ // like that — a `Document` carries only a `$quill` REFERENCE, not the resolved
524
+ // schema, so each `commit*` method takes the `quill` handle as its first
525
+ // argument. These pure-JS classes restore the Rust ergonomics: bind `quill` +
526
+ // `doc` once, then issue `set` / `setAll` / `card(i).set`.
527
+ //
528
+ // They hold JS references to the caller's EXISTING handles — no WASM object of
529
+ // their own, no `free()` burden, no second owner of either handle — and every
530
+ // write delegates straight to the underlying `commit*` verb: a schema field is
531
+ // typed-committed (coerced to canonical form, mismatch throws now), and a name
532
+ // the schema does not declare throws `UnknownField` rather than falling to the
533
+ // opaque store — on the typed path an undeclared name is a typo. Opaque storage
534
+ // stays available through the raw `Document.setField` / `setCardField` verbs.
535
+
536
+ /**
537
+ * A {@link Document} bound to its {@link Quill} for typed writes — the JS twin
538
+ * of Rust's `quill.writer(&mut doc)`. Writes target the main card; use
539
+ * {@link card} for a composable card. Holds both handles by reference and owns
540
+ * neither, so there is nothing to `free()`.
541
+ */
542
+ export class DocumentWriter {
543
+ #quill;
544
+ #doc;
545
+ /**
546
+ * @param {Quill} quill the schema source for typed commits
547
+ * @param {Document} doc the document to mutate, held by reference (not owned)
548
+ */
549
+ constructor(quill, doc) {
550
+ this.#quill = quill;
551
+ this.#doc = doc;
552
+ }
553
+ /** The bound document — the same instance passed in, mutated in place. */
554
+ get document() {
555
+ return this.#doc;
556
+ }
557
+ /**
558
+ * Typed-commit one main-card field (strict coerce, mismatch throws now).
559
+ * Throws `UnknownField` for a name the schema does not declare. See
560
+ * `Document.commitField`.
561
+ * @param {string} name
562
+ * @param {unknown} value
563
+ * @returns {void}
564
+ */
565
+ set(name, value) {
566
+ return this.#doc.commitField(this.#quill, name, value);
567
+ }
568
+ /**
569
+ * Typed-commit several main-card fields atomically — nothing is applied on
570
+ * error (throws a per-field diagnostic bundle, including an `UnknownField`
571
+ * for each undeclared name). See `Document.commitFields`.
572
+ * @param {Record<string, unknown>} fields
573
+ * @returns {void}
574
+ */
575
+ setAll(fields) {
576
+ return this.#doc.commitFields(this.#quill, fields);
577
+ }
578
+ /**
579
+ * Set the main body from markdown (edit semantics: surviving anchors rebase),
580
+ * discarding the text delta — the receipt-free body write. Call
581
+ * `doc.revise({}, md)` for the {@link Delta} receipt (the corpus-lane
582
+ * spelling). Markdown in, no corpus or receipt in sight.
583
+ * @param {string} markdown
584
+ * @returns {void}
585
+ */
586
+ setBody(markdown) {
587
+ this.#doc.revise({}, markdown);
588
+ }
589
+ /**
590
+ * Build a composable card of `kind`, typed-commit `fields` onto it, set its
591
+ * body from optional markdown, and append it — the fused `makeCard` + typed
592
+ * commit + `pushCard`. Transactional: the card is committed in full before it
593
+ * joins the document, so a rejected field (throws a per-field diagnostic
594
+ * bundle, `UnknownField` per undeclared name) or an invalid kind/body leaves
595
+ * the document untouched. See `Document.addCard`.
596
+ * @param {string} kind
597
+ * @param {Record<string, unknown>} [fields]
598
+ * @param {string} [body]
599
+ * @returns {void}
600
+ */
601
+ addCard(kind, fields, body) {
602
+ return this.#doc.addCard(this.#quill, kind, fields, body);
603
+ }
604
+ /**
605
+ * Remove the composable card at `index`, returning it (or `undefined` if the
606
+ * index is out of range) — the tier-1 spelling of `Document.removeCard`.
607
+ * @param {number} index
608
+ * @returns {import('../core/wasm.js').Card | undefined}
609
+ */
610
+ removeCard(index) {
611
+ return this.#doc.removeCard(index);
612
+ }
613
+ /**
614
+ * A {@link CardWriter} bound to the composable card at `index`. Index
615
+ * validity is checked lazily by the underlying write (it throws
616
+ * `IndexOutOfRange` at commit time), so constructing one never throws.
617
+ *
618
+ * The cursor is ephemeral — bind, write, discard. It holds `index`, not the
619
+ * card: a `removeCard`/`addCard` between binding and writing silently
620
+ * retargets it. For durable addressing stamp `$id` and re-resolve the index
621
+ * at write time.
622
+ * @param {number} index
623
+ * @returns {CardWriter}
624
+ */
625
+ card(index) {
626
+ return new CardWriter(this.#quill, this.#doc, index);
627
+ }
628
+ }
629
+
630
+ /**
631
+ * A single composable card bound to its {@link Quill} for typed writes, from
632
+ * {@link DocumentWriter.card}. Same `set` / `setAll` verbs as
633
+ * {@link DocumentWriter}, targeting the card at its bound index.
634
+ */
635
+ export class CardWriter {
636
+ #quill;
637
+ #doc;
638
+ #index;
639
+ /**
640
+ * @param {Quill} quill the schema source
641
+ * @param {Document} doc the document to mutate, held by reference (not owned)
642
+ * @param {number} index the composable card's index
643
+ */
644
+ constructor(quill, doc, index) {
645
+ this.#quill = quill;
646
+ this.#doc = doc;
647
+ this.#index = index;
648
+ }
649
+ /** The bound card index. */
650
+ get index() {
651
+ return this.#index;
652
+ }
653
+ /**
654
+ * Typed-commit one field on this card, per `Document.commitCardField`.
655
+ * Throws `UnknownField` for an undeclared name and `IndexOutOfRange` if the
656
+ * bound index is out of range.
657
+ * @param {string} name
658
+ * @param {unknown} value
659
+ * @returns {void}
660
+ */
661
+ set(name, value) {
662
+ return this.#doc.commitCardField(this.#quill, this.#index, name, value);
663
+ }
664
+ /**
665
+ * Typed-commit several fields on this card atomically, per
666
+ * `Document.commitCardFields`. Throws a per-field diagnostic bundle on error
667
+ * and `IndexOutOfRange` if the bound index is out of range.
668
+ * @param {Record<string, unknown>} fields
669
+ * @returns {void}
670
+ */
671
+ setAll(fields) {
672
+ return this.#doc.commitCardFields(this.#quill, this.#index, fields);
673
+ }
674
+ /**
675
+ * Set this card's body from markdown (edit semantics), discarding the delta —
676
+ * the card twin of {@link DocumentWriter.setBody}.
677
+ * @param {string} markdown
678
+ * @returns {void}
679
+ */
680
+ setBody(markdown) {
681
+ this.#doc.revise({ card: this.#index }, markdown);
682
+ }
683
+ }
684
+
685
+ // ── `quill.writer(doc)` — the typed front door ──────────────────────────────
686
+ // The tier-1 default: bind the quill's schema to a document and issue bare
687
+ // typed writes. Mirrors core's `quill.writer(&mut doc)` — the schema grants the
688
+ // typing, so the quill (not the document) is the factory. Patched onto the
689
+ // re-exported `Quill` prototype rather than wrapped: `Quill === CoreQuill`
690
+ // stays true (the identity invariant above); this only adds a method that
691
+ // constructs the pure-JS writer, which owns no WASM handle.
692
+ /**
693
+ * A {@link DocumentWriter} binding this quill's schema to `doc` for typed
694
+ * writes — the documented front door. The returned writer holds both handles by
695
+ * reference and owns neither, so there is nothing to `free()`. Ephemeral by
696
+ * convention: bind, write, discard.
697
+ * @this {Quill}
698
+ * @param {Document} doc the document to mutate, held by reference (not owned)
699
+ * @returns {DocumentWriter}
700
+ */
701
+ Quill.prototype.writer = function writer(doc) {
702
+ return new DocumentWriter(this, doc);
703
+ };