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