@quillmark/wasm 0.101.0 → 0.103.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,15 +2,17 @@
2
2
  //
3
3
  // @quillmark/wasm/runtime: the canonical consumer API.
4
4
  //
5
- // Consumers import `Quill`, `Document`, and `Engine` from here and never touch
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
8
- // the canonical home of `Quill`/`Document`, and one private backend binary per
9
- // backend (`backends/typst/` today; more later) that carries an engine. A
10
- // handle from one memory cannot be used by another. This module hides that seam
11
- // and is exposed at the package root (`@quillmark/wasm`):
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`):
12
13
  //
13
- // - `Quill` and `Document` ARE the core build's classes, re-exported. They
14
+ // - `Quill` and `Document` ARE the core build's classes, handed out by the
15
+ // gate (§ "Initialization"). They
14
16
  // hold the canonical data and the full sync surface (schema / validate /
15
17
  // seed / mutate / toJson / toTree). No backend is loaded to use them, so
16
18
  // the editor/validation path never pays for a multi-MB backend binary.
@@ -38,39 +40,201 @@
38
40
  // The cross-memory crossing is therefore invisible: a consumer hands canonical
39
41
  // `Quill`/`Document` to `engine.render(...)` and gets a `RenderResult` back.
40
42
 
41
- // ── CANONICAL INVARIANT: re-export the core build, never wrap ───────────────
42
- // The root re-exports the core build's `Quill`/`Document` classes verbatim,
43
- // NOT subclasses or wrappers. There is exactly ONE public entry point (this
44
- // module), so this identity is a structural fact: `Quill`/`Document` ARE the
45
- // core classes, and the only boundary that needs crossing is core→backend (a
46
- // separate WASM memory), which `Engine` does internally as data
47
- // (`toTree`/`toJson`).
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`).
48
49
  //
49
- // Do NOT replace this with a wrapper class: that breaks the identity and turns
50
- // a structural fact into a converted type (a breaking design change, not a
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.
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
54
  //
55
55
  // The identity is what makes `instanceof` the whole membership test: a handle
56
56
  // either belongs to this copy's classes or it belongs to another copy, and the
57
57
  // second is always a consumer bug. `Engine` is NOT duck-typed on its inputs; it
58
58
  // checks them. See § "Handles from another copy" below.
59
59
  //
60
- // Imported (not bare re-exported) so `Quill` is a local binding this module can
61
- // augment: `quill.writer(doc)` is patched onto its prototype below. The
62
- // re-export keeps the identity: the exported `Quill` IS the core class.
63
- import { Quill, Document, init } from '../core/wasm.js';
64
- export { Quill, Document, init };
65
- // The document-free content codec: re-exported verbatim from the core build so
66
- // the runtime subpath exposes `exportMarkdown(body)` (the on-demand markdown
67
- // projection), `importMarkdown`, and the position-mapping pair (`rebase`,
68
- // `mapPos`).
69
- export { importMarkdown, exportMarkdown, rebase, mapPos } from '../core/wasm.js';
60
+ // 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.
65
+ import initCore, { Quill, Document } from '../core/wasm.js';
66
+ // The wasm byte source, resolved per environment by package.json's `imports`
67
+ // map: a pass-through in a browser (the glue fetches and streams the URL
68
+ // itself), a `node:fs` read under Node, whose `fetch` rejects `file:` URLs.
69
+ // Resolution-time, so `node:fs` never enters a browser graph.
70
+ 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
+ import { importMarkdown, exportMarkdown, rebase, mapPos } from '../core/wasm.js';
70
75
  // The document-model path parser/serializer: `parseDocPath(str) => DocPathSeg[]`
71
76
  // and its inverse `formatDocPath`, so a consumer routes on `Diagnostic.path`
72
77
  // segments instead of reverse-engineering the grammar.
73
- export { parseDocPath, formatDocPath } from '../core/wasm.js';
78
+ import { parseDocPath, formatDocPath } from '../core/wasm.js';
79
+
80
+ // ── Initialization ──────────────────────────────────────────────────────────
81
+ // The builds are `--target web`: they export their classes synchronously but
82
+ // carry no wasm instance until something instantiates them. This module owns
83
+ // 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.
95
+ //
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.
99
+ //
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.
106
+ //
107
+ // 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.
114
+
115
+ /**
116
+ * The gated surface: the core build's values, which are exactly the ones its
117
+ * instance stands behind. Frozen and built at module scope, because the classes
118
+ * and functions themselves resolve synchronously (only the instance behind them
119
+ * is late), so there is nothing to defer and no per-call allocation.
120
+ *
121
+ * Membership is derived, not chosen: it is the core build's exports minus its
122
+ * instantiation machinery (`default`, `initSync`, and the start-section
123
+ * `start`), which `init` owns and no consumer calls. `init.test.js` § "the
124
+ * gated surface" computes that set and pins it, so a new core export that never
125
+ * reaches here fails there rather than going missing.
126
+ * @type {import('./runtime.js').CoreSurface}
127
+ */
128
+ const CORE_SURFACE = Object.freeze({
129
+ Quill,
130
+ Document,
131
+ importMarkdown,
132
+ exportMarkdown,
133
+ rebase,
134
+ mapPos,
135
+ parseDocPath,
136
+ formatDocPath
137
+ });
138
+
139
+ /** The in-flight or settled core instantiation, resolving to `CORE_SURFACE`.
140
+ * The memo is the PROMISE, not a boolean, so concurrent callers share one
141
+ * instantiation instead of racing.
142
+ * @type {Promise<import('./runtime.js').CoreSurface> | undefined} */
143
+ let coreInit;
144
+ /** The source `init` was first called with; the conflict check reads it. */
145
+ let coreInitSource;
146
+
147
+ /**
148
+ * Instantiate the core WASM build and resolve to its surface.
149
+ *
150
+ * ```js
151
+ * import { init } from '@quillmark/wasm';
152
+ * const { Quill, Document } = await init();
153
+ * ```
154
+ *
155
+ * The classes and the free functions come from here and nowhere else, so the
156
+ * pre-init mistake is not expressible. Destructure at each entry point (route
157
+ * loader, hydration path, worker) rather than threading one result around: the
158
+ * gate is memoized, so every await after the first is free.
159
+ *
160
+ * Identical in every environment: in a browser the binary is fetched and
161
+ * streamed, under Node it is read off disk, and the call site is the same line.
162
+ *
163
+ * Idempotent and concurrency-safe: every non-conflicting call returns the same
164
+ * promise, so several entry points cost one instantiation. A failed init clears
165
+ * the memo, so a retry is possible.
166
+ *
167
+ * Both failures reject (§ "Initialization", FAILURE DELIVERY): one `catch`
168
+ * around `await init(...)` covers `runtime::init_conflict` and
169
+ * `runtime::init_failed` alike.
170
+ *
171
+ * @param {import('../core/wasm.js').InitInput} [source] override the binary's
172
+ * source (bytes, a `Response`, a `WebAssembly.Module`, a URL) for hosts that
173
+ * route assets themselves or embed the binary. Pass it on the FIRST call; a
174
+ * later call passing a *different* source rejects with
175
+ * `runtime::init_conflict` rather than silently ignoring it. Passing the same
176
+ * value again is fine, so several entry points may each `await init(BYTES)`
177
+ * against one constant.
178
+ * @returns {Promise<import('./runtime.js').CoreSurface>} the core surface, once
179
+ * its instance is live
180
+ */
181
+ export function init(source) {
182
+ if (coreInit) {
183
+ if (source !== undefined && source !== coreInitSource) {
184
+ // A rejection, not a throw: this is the one failure on the
185
+ // promise-returning surface that could land on the caller's stack, where
186
+ // `init(BYTES).catch(…)` would not see it.
187
+ return Promise.reject(
188
+ quillmarkError(
189
+ 'runtime::init_conflict',
190
+ 'init(source): core is already initializing or initialized from a different source.',
191
+ 'Pass a source on the first call only, or pass the same value every time.'
192
+ )
193
+ );
194
+ }
195
+ return coreInit;
196
+ }
197
+ coreInitSource = source;
198
+ // Assign before the first await so a synchronous second call sees the memo.
199
+ coreInit = instantiateCore(source).then(
200
+ () => CORE_SURFACE,
201
+ (err) => {
202
+ // Self-heal, as `#resolveBackend` does: one transient failure (a 404, an
203
+ // offline fetch) must not poison every later attempt.
204
+ coreInit = undefined;
205
+ coreInitSource = undefined;
206
+ throw err;
207
+ }
208
+ );
209
+ return coreInit;
210
+ }
211
+
212
+ /**
213
+ * @param {import('../core/wasm.js').InitInput | undefined} source
214
+ * @returns {Promise<void>}
215
+ */
216
+ async function instantiateCore(source) {
217
+ // The literal `new URL(..., import.meta.url)` form: every bundler rewrites it
218
+ // into an emitted asset, and unbundled browsers and Node resolve it against
219
+ // the shipped package layout.
220
+ const resolved = await toModuleSource(
221
+ source ?? new URL('../core/wasm_bg.wasm', import.meta.url)
222
+ );
223
+ try {
224
+ await initCore({ module_or_path: resolved });
225
+ } catch (cause) {
226
+ throw Object.assign(
227
+ quillmarkError(
228
+ 'runtime::init_failed',
229
+ `init(): could not load or instantiate the core WASM binary: ${
230
+ /** @type {any} */ (cause)?.message ?? cause
231
+ }`,
232
+ "The binary ships beside the package files; check the network tab for a 404 or an HTML response. Under Vite's dev server, dependency pre-bundling moves the package away from it: add optimizeDeps: { exclude: ['@quillmark/wasm'] }."
233
+ ),
234
+ { cause }
235
+ );
236
+ }
237
+ }
74
238
 
75
239
  // ── The main-card address ───────────────────────────────────────────────────
76
240
  /**
@@ -143,7 +307,7 @@ function quillmarkError(code, message, hint) {
143
307
  // front-run it with a `QuillmarkError` that names both.
144
308
  //
145
309
  // 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
310
+ // `LiveSession.update` cross into backend memory as data (`toTree`/`toJson`), so
147
311
  // a foreign handle there would silently work, at the price of the round-trip
148
312
  // above and a quill clone cache split per copy.
149
313
  //
@@ -391,11 +555,51 @@ export function isUnknownIsland(island) {
391
555
  return typeof island?.type === 'string' && !KNOWN_ISLAND_TYPES.has(island.type);
392
556
  }
393
557
 
558
+ /**
559
+ * Build a `load` thunk: dynamic-import a backend build, then instantiate it.
560
+ *
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.
567
+ *
568
+ * @param {string} id backend id, for the failure message
569
+ * @param {() => Promise<any>} importThunk the dynamic `import()`
570
+ * @param {() => URL} wasmUrl the build's binary, resolved at call time
571
+ * @returns {() => Promise<any>} resolves to a ready-to-use module
572
+ */
573
+ function backendLoad(id, importThunk, wasmUrl) {
574
+ /** @type {Promise<any> | undefined} */
575
+ let loaded;
576
+ return () =>
577
+ (loaded ??= (async () => {
578
+ const mod = await importThunk();
579
+ await mod.default({ module_or_path: await toModuleSource(wasmUrl()) });
580
+ return mod;
581
+ })().catch((cause) => {
582
+ // Self-heal, as core's `init` does.
583
+ loaded = undefined;
584
+ throw Object.assign(
585
+ quillmarkError(
586
+ 'runtime::backend_load_failed',
587
+ `Engine: could not load the '${id}' backend: ${
588
+ /** @type {any} */ (cause)?.message ?? cause
589
+ }`,
590
+ 'The backend binary ships beside the package files; check the network tab for a 404 or an HTML response.'
591
+ ),
592
+ { cause }
593
+ );
594
+ }));
595
+ }
596
+
394
597
  // Backend builds are NEVER statically imported here: that would pull a
395
598
  // multi-MB binary into the eager graph and defeat lazy loading. Each entry is a
396
- // DESCRIPTOR: `load` is a thunk returning a dynamic `import()` (a backend's
397
- // chunk is fetched only when something actually renders against that backend),
398
- // and `formats`/`canvas` are the REQUIRED static capability manifest so the
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
399
603
  // cheap probes (`supportedFormats`/`supportsCanvas`) ALWAYS answer without
400
604
  // loading the binary or cloning the quill. The manifest values are verified
401
605
  // against each backend's Rust source (`crates/backends/<id>/src/lib.rs`
@@ -405,12 +609,20 @@ export function isUnknownIsland(island) {
405
609
  // format list includes a visual-page format (`svg` or `png`).
406
610
  const DEFAULT_BACKENDS = {
407
611
  typst: {
408
- load: () => import('../backends/typst/wasm.js'),
612
+ load: backendLoad(
613
+ 'typst',
614
+ () => import('../backends/typst/wasm.js'),
615
+ () => new URL('../backends/typst/wasm_bg.wasm', import.meta.url)
616
+ ),
409
617
  formats: ['pdf', 'svg', 'png'], // crates/backends/typst/src/lib.rs SUPPORTED_FORMATS
410
618
  canvas: true // has svg/png → formats_support_canvas == true
411
619
  },
412
620
  pdfform: {
413
- load: () => import('../backends/pdfform/wasm.js'),
621
+ load: backendLoad(
622
+ 'pdfform',
623
+ () => import('../backends/pdfform/wasm.js'),
624
+ () => new URL('../backends/pdfform/wasm_bg.wasm', import.meta.url)
625
+ ),
414
626
  // crates/backends/pdfform/src/lib.rs SUPPORTED_FORMATS == [Pdf, Svg, Png]
415
627
  formats: ['pdf', 'svg', 'png'],
416
628
  canvas: true // has svg/png → formats_support_canvas == true
@@ -475,6 +687,10 @@ export class Engine {
475
687
  * `supportedFormats`/`supportsCanvas` always free (no binary load, no quill
476
688
  * clone). Malformed entries throw here, at construction. The default
477
689
  * registry maps `"typst"` to the bundled Typst build.
690
+ *
691
+ * `load` resolves to a READY module: a registrant shipping its own
692
+ * `--target web` build instantiates inside the thunk. More than one
693
+ * `Engine` may call it, so memoize (the built-ins do, at module scope).
478
694
  */
479
695
  constructor(options) {
480
696
  const merged = { ...DEFAULT_BACKENDS, ...(options?.backends ?? {}) };
@@ -641,8 +857,8 @@ export class Engine {
641
857
  }
642
858
 
643
859
  /**
644
- * Open a live render session (canvas preview / per-page paint / `apply`).
645
- * The session is self-contained (it retains what it needs for `apply`), so
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
646
862
  * the transient quill and document clones are freed before this returns;
647
863
  * the caller owns the returned session and must `.free()` it. The `quill`
648
864
  * and `doc` handles are read synchronously before the first await, so the
@@ -693,15 +909,15 @@ export class Engine {
693
909
 
694
910
  /**
695
911
  * Thin wrapper over a backend's live render session. Reads serve the current
696
- * compile; `apply(doc)` recompiles in place (transactional: on throw, reads
912
+ * compile; `update(doc)` recompiles in place (transactional: on throw, reads
697
913
  * keep serving the last-good compile). The quill/document clones it was
698
- * opened from have already been freed: the session retains what `apply`
914
+ * opened from have already been freed: the session retains what `update`
699
915
  * needs.
700
916
  *
701
917
  * Geometry reads (`regions`, `positionAt`, `locate`) resolve against the
702
918
  * current compile; anchoring a caret or selection across edits is the editor's
703
919
  * job (its own transaction mapping): re-read geometry after each committed
704
- * `apply`.
920
+ * `update`.
705
921
  *
706
922
  * `paint` writes a COMPLETE page raster (all content visible, no caller-side
707
923
  * compositing) for every backend that supports canvas (Typst rasterizes
@@ -709,8 +925,8 @@ export class Engine {
709
925
  */
710
926
  export class LiveSession {
711
927
  /**
712
- * @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)
713
- * @param {{ Document: { fromJson(json: string): any } }} mod the session's backend build, used to materialize `apply` documents in its linear memory
928
+ * @param {{ pageCount: number, backendId: string, supportsCanvas: boolean, warnings: any[], update: Function, render: Function, regions: Function, pageSize: Function, paint: Function, free: Function }} inner backend-build LiveSession (typst or pdfform)
929
+ * @param {{ Document: { fromJson(json: string): any } }} mod the session's backend build, used to materialize `update` documents in its linear memory
714
930
  */
715
931
  constructor(inner, mod) {
716
932
  this.#inner = inner;
@@ -726,12 +942,12 @@ export class LiveSession {
726
942
  * @param {Document} doc
727
943
  * @returns {import('./runtime.d.ts').ChangeSet}
728
944
  */
729
- apply(doc) {
730
- requireLocalDoc(doc, 'session.apply(doc)');
945
+ update(doc) {
946
+ requireLocalDoc(doc, 'session.update(doc)');
731
947
  let backendDoc = null;
732
948
  try {
733
949
  backendDoc = this.#mod.Document.fromJson(doc.toJson());
734
- return this.#inner.apply(backendDoc);
950
+ return this.#inner.update(backendDoc);
735
951
  } finally {
736
952
  backendDoc?.free();
737
953
  }
@@ -907,14 +1123,15 @@ export class DocumentWriter {
907
1123
  return this.#doc._commitFields(this.#quill, MAIN_CARD_ADDR, fields);
908
1124
  }
909
1125
  /**
910
- * Set the main body from markdown (edit semantics: surviving anchors rebase),
911
- * discarding the text delta, the receipt-free body write. Call
912
- * `doc.revise({}, md)` for the {@link Delta} receipt.
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.
913
1130
  * @param {string} markdown
914
- * @returns {void}
1131
+ * @returns {import('../core/wasm.js').Delta}
915
1132
  */
916
- setBody(markdown) {
917
- this.#doc.revise({}, markdown);
1133
+ reviseBody(markdown) {
1134
+ return this.#doc.revise({}, markdown);
918
1135
  }
919
1136
  /**
920
1137
  * Revise the content main-card field `name` from authored text: typed *and*
@@ -1033,13 +1250,13 @@ export class CardWriter {
1033
1250
  return this.#doc._commitFields(this.#quill, { card: this.#index }, fields);
1034
1251
  }
1035
1252
  /**
1036
- * Set this card's body from markdown (edit semantics), discarding the delta:
1037
- * the card twin of {@link DocumentWriter.setBody}.
1253
+ * Revise this card's body from markdown (edit semantics), returning the text
1254
+ * {@link Delta}: the card twin of {@link DocumentWriter.reviseBody}.
1038
1255
  * @param {string} markdown
1039
- * @returns {void}
1256
+ * @returns {import('../core/wasm.js').Delta}
1040
1257
  */
1041
- setBody(markdown) {
1042
- this.#doc.revise({ card: this.#index }, markdown);
1258
+ reviseBody(markdown) {
1259
+ return this.#doc.revise({ card: this.#index }, markdown);
1043
1260
  }
1044
1261
  /**
1045
1262
  * Revise the content field `name` on this card from authored text: typed *and*
@@ -1084,7 +1301,7 @@ Quill.prototype.writer = function writer(doc) {
1084
1301
  // interpret by declared type: a richtext field to markdown, a plaintext field to
1085
1302
  // its literal text, every other type verbatim, and an unknown name throws
1086
1303
  // `UnknownField`. A field's markdown lives here, not on the body-only
1087
- // `getMarkdown`. Like the writer classes these hold the caller's handles by
1304
+ // `bodyMarkdown`. Like the writer classes these hold the caller's handles by
1088
1305
  // reference, own no WASM object, and have nothing to `free()`.
1089
1306
 
1090
1307
  /**
@@ -1115,7 +1332,7 @@ export class DocumentReader {
1115
1332
  * to markdown, every other type verbatim. A bare string is `Addr` shorthand
1116
1333
  * for `{ field }`; an absent `addr.field` reads the body markdown. `undefined`
1117
1334
  * for an absent field; throws `UnknownField` for a name the schema does not
1118
- * declare, `FieldRichtextDecode` for a richtext field holding an undecodable
1335
+ * declare, `FieldDecode` for a richtext field holding an undecodable
1119
1336
  * value, and `IndexOutOfRange` for a bad `addr.card`.
1120
1337
  * @param {import('../core/wasm.js').Addr | string} addr
1121
1338
  * @returns {unknown}
@@ -1124,14 +1341,14 @@ export class DocumentReader {
1124
1341
  return this.#doc._readerGet(this.#quill, addr);
1125
1342
  }
1126
1343
  /**
1127
- * Read the content field at `addr` as its canonical `Content` corpus: the
1128
- * corpus twin of {@link get}, which projects. Decodes through the codec the
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
1129
1346
  * declared type names (`richtext` as markdown, `plaintext` as literal text),
1130
- * so a field the writer committed as a corpus and one a markdown parse left
1347
+ * so a field the writer committed as a `Content` and one a markdown parse left
1131
1348
  * as an authored string read back the same; no branching on how the
1132
- * document was built. An absent `addr.field` reads the body corpus.
1349
+ * document was built. An absent `addr.field` reads the body `Content`.
1133
1350
  * `undefined` for an absent field; throws `UnknownField`, `FieldNotContent`
1134
- * for a type that is not a content leaf, `FieldRichtextDecode` for an undecodable
1351
+ * for a type that is not a content leaf, `FieldDecode` for an undecodable
1135
1352
  * value, and `IndexOutOfRange` for a bad `addr.card`.
1136
1353
  * @param {import('../core/wasm.js').Addr | string} addr
1137
1354
  * @returns {import('../core/wasm.js').Content | undefined}
@@ -1144,7 +1361,7 @@ export class DocumentReader {
1144
1361
  * format fact, not a schema fact). Equivalent to `get({})`.
1145
1362
  * @returns {string}
1146
1363
  */
1147
- getBody() {
1364
+ bodyMarkdown() {
1148
1365
  return this.#doc._readerGet(this.#quill, {});
1149
1366
  }
1150
1367
  /**
@@ -1163,7 +1380,7 @@ export class DocumentReader {
1163
1380
 
1164
1381
  /**
1165
1382
  * A single composable card bound to its {@link Quill} for typed reads, from
1166
- * {@link DocumentReader.card}. Same `get` / `getBody` verbs as
1383
+ * {@link DocumentReader.card}. Same `get` / `bodyMarkdown` verbs as
1167
1384
  * {@link DocumentReader}, reading the card at its bound index.
1168
1385
  */
1169
1386
  export class CardReader {
@@ -1206,7 +1423,7 @@ export class CardReader {
1206
1423
  }
1207
1424
  /**
1208
1425
  * Read the content field `name` on this card as its canonical `Content`
1209
- * corpus: the card twin of {@link DocumentReader.getContent}.
1426
+ * `Content`: the card twin of {@link DocumentReader.getContent}.
1210
1427
  * @param {string} name
1211
1428
  * @returns {import('../core/wasm.js').Content | undefined}
1212
1429
  */
@@ -1214,10 +1431,10 @@ export class CardReader {
1214
1431
  return this.#doc._readerGetContent(this.#quill, { card: this.#index, field: name });
1215
1432
  }
1216
1433
  /**
1217
- * This card's body markdown: the card twin of {@link DocumentReader.getBody}.
1434
+ * This card's body markdown: the card twin of {@link DocumentReader.bodyMarkdown}.
1218
1435
  * @returns {string}
1219
1436
  */
1220
- getBody() {
1437
+ bodyMarkdown() {
1221
1438
  return this.#doc._readerGet(this.#quill, { card: this.#index });
1222
1439
  }
1223
1440
  }