@quillmark/wasm 0.104.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 +106 -27
- package/README.md +72 -206
- package/backends/pdfform/wasm.d.ts +460 -713
- package/backends/pdfform/wasm.js +365 -491
- package/backends/pdfform/wasm_bg.wasm +0 -0
- package/backends/pdfform/wasm_bg.wasm.d.ts +1 -0
- package/backends/typst/wasm.d.ts +460 -713
- package/backends/typst/wasm.js +365 -491
- package/backends/typst/wasm_bg.wasm +0 -0
- package/backends/typst/wasm_bg.wasm.d.ts +1 -0
- package/core/wasm.d.ts +332 -492
- package/core/wasm.js +310 -411
- package/core/wasm_bg.wasm +0 -0
- package/core/wasm_bg.wasm.d.ts +1 -0
- package/package.json +1 -1
- package/runtime/runtime.d.ts +181 -255
- package/runtime/runtime.js +190 -353
package/runtime/runtime.js
CHANGED
|
@@ -2,121 +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
|
|
94
|
-
// `quill.seedDocument` return synchronously, so there is nowhere to hide an
|
|
95
|
-
// await except in front.
|
|
50
|
+
// inside their lazy load.
|
|
96
51
|
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
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.
|
|
100
57
|
//
|
|
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.
|
|
101
60
|
// `Engine`, `LiveSession` and the four writer/reader classes stay static too,
|
|
102
|
-
// gated by their ARGUMENTS
|
|
103
|
-
//
|
|
104
|
-
// constructors
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
// the six carries a static method, the one member shape an argument cannot
|
|
109
|
-
// gate. `gate.test.js` is the executable guard, driving the whole static
|
|
110
|
-
// surface before `init`. Holding them out of the gate keeps them tree-shakable,
|
|
111
|
-
// so the editor path drops the dispatcher it never calls.
|
|
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.
|
|
112
67
|
//
|
|
113
68
|
// FAILURE DELIVERY follows the FUNCTION kind, not the failure kind: a sync verb
|
|
114
|
-
// throws, a promise-returning verb rejects, and nothing does both.
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
// `init` is the one promise-returning export not declared `async`, because the
|
|
118
|
-
// memo is returned by identity; its conflict guard rejects explicitly to hold
|
|
119
|
-
// 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.
|
|
120
72
|
|
|
121
73
|
/**
|
|
122
74
|
* The gated surface: the core build's values, which are exactly the ones its
|
|
@@ -289,38 +241,29 @@ function quillmarkError(code, message, hint) {
|
|
|
289
241
|
}
|
|
290
242
|
|
|
291
243
|
// ── Handles from another copy: always a bug ─────────────────────────────────
|
|
292
|
-
// A duplicate install
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
// 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.
|
|
297
248
|
//
|
|
298
249
|
// Crossing read-only handles as data is mechanically possible (`toJson` and
|
|
299
|
-
// `toTree` serialize either way) and is not done
|
|
300
|
-
//
|
|
301
|
-
// whole-document `toJson` + `fromJson
|
|
302
|
-
// fifty round trips
|
|
303
|
-
// 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.
|
|
304
254
|
//
|
|
305
|
-
// What the checks deliver is the ERROR, not the rejection. wasm-bindgen's
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
// a foreign handle there would silently work, at the price of the round-trip
|
|
318
|
-
// above and a quill clone cache split per copy.
|
|
319
|
-
//
|
|
320
|
-
// Not an API widening in either direction: the declared parameter types stay
|
|
321
|
-
// `Quill`/`Document`, and the accepted set is exactly this copy's instances.
|
|
322
|
-
|
|
323
|
-
/** 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. */
|
|
324
267
|
const HANDLE_KINDS = {
|
|
325
268
|
Quill: {
|
|
326
269
|
code: 'runtime::not_a_quill',
|
|
@@ -404,10 +347,8 @@ function patchHandleChecked(proto, name, wrap) {
|
|
|
404
347
|
/** @type {any} */ (proto)[name] = patched;
|
|
405
348
|
}
|
|
406
349
|
|
|
407
|
-
// The
|
|
408
|
-
//
|
|
409
|
-
// legible. Only the argument can be foreign, the receiver being whichever copy
|
|
410
|
-
// 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.
|
|
411
352
|
patchHandleChecked(Document.prototype, 'equals', (original) =>
|
|
412
353
|
function equals(/** @type {any} */ other) {
|
|
413
354
|
requireLocalDoc(other, 'Document.equals');
|
|
@@ -503,23 +444,13 @@ export function isListItemContainer(container) {
|
|
|
503
444
|
}
|
|
504
445
|
|
|
505
446
|
// ── Open-set membership guards ──────────────────────────────────────────────
|
|
506
|
-
// The guards above each answer "is this arm X"
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
-
// unknown and round-trips through the consumer's unknown carrier, losing any
|
|
514
|
-
// sibling-key payload.
|
|
515
|
-
//
|
|
516
|
-
// A predicate rather than an exported name list, because the known tables below
|
|
517
|
-
// are upstream's business. They are pinned against the Rust source
|
|
518
|
-
// (`Content::RESERVED_*` and `KnownIslandType`) by the
|
|
519
|
-
// `js_known_name_tables_match_the_rust_open_sets` drift-guard test in
|
|
520
|
-
// `crates/bindings/wasm/tests/known_names_drift.rs`: adding a built-in means
|
|
521
|
-
// editing there, here, and the TS unions in `crates/bindings/wasm/src/engine.rs`
|
|
522
|
-
// 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.
|
|
523
454
|
//
|
|
524
455
|
// These classify unknown *tags*, not unknown *payloads on known tags*. A future
|
|
525
456
|
// `kind: "footnote"` with a sibling `ref` loses `ref` at a consumer that predates
|
|
@@ -565,12 +496,11 @@ export function isUnknownIsland(island) {
|
|
|
565
496
|
/**
|
|
566
497
|
* Build a `load` thunk: dynamic-import a backend build, then instantiate it.
|
|
567
498
|
*
|
|
568
|
-
* Under `--target web` a freshly imported build is inert
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
* 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.
|
|
574
504
|
*
|
|
575
505
|
* @param {string} id backend id, for the failure message
|
|
576
506
|
* @param {() => Promise<any>} importThunk the dynamic `import()`
|
|
@@ -601,19 +531,15 @@ function backendLoad(id, importThunk, wasmUrl) {
|
|
|
601
531
|
}));
|
|
602
532
|
}
|
|
603
533
|
|
|
604
|
-
// Backend builds are NEVER statically imported here: that would pull a
|
|
605
|
-
//
|
|
606
|
-
// DESCRIPTOR: `load`
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
// `
|
|
610
|
-
//
|
|
611
|
-
//
|
|
612
|
-
//
|
|
613
|
-
// `SUPPORTED_FORMATS`) and pinned by the `runtime.test.js` drift-guard test,
|
|
614
|
-
// which renders once and asserts the loaded backend reports the same list.
|
|
615
|
-
// `canvas` mirrors `quillmark_core::formats_support_canvas`: true iff the
|
|
616
|
-
// 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.
|
|
617
543
|
const DEFAULT_BACKENDS = {
|
|
618
544
|
typst: {
|
|
619
545
|
load: backendLoad(
|
|
@@ -637,11 +563,9 @@ const DEFAULT_BACKENDS = {
|
|
|
637
563
|
};
|
|
638
564
|
|
|
639
565
|
/**
|
|
640
|
-
* Validate a backend registry descriptor,
|
|
641
|
-
*
|
|
642
|
-
*
|
|
643
|
-
* boolean `canvas`. Failing at construction (not deep inside a render) keeps the
|
|
644
|
-
* 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.
|
|
645
569
|
* @param {string} id
|
|
646
570
|
* @param {unknown} entry
|
|
647
571
|
* @returns {{ load: () => Promise<unknown>, formats: string[], canvas: boolean }}
|
|
@@ -678,10 +602,9 @@ export class Engine {
|
|
|
678
602
|
/** backendId → descriptor `{ load, formats, canvas }`. */
|
|
679
603
|
#loaders;
|
|
680
604
|
/**
|
|
681
|
-
* backendId → WeakMap<canonical Quill, backend-memory
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
* 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.
|
|
685
608
|
* @type {Map<string, WeakMap<object, any>>}
|
|
686
609
|
*/
|
|
687
610
|
#quillClones = new Map();
|
|
@@ -689,11 +612,9 @@ export class Engine {
|
|
|
689
612
|
/**
|
|
690
613
|
* @param {{ backends?: Record<string, { load: () => Promise<unknown>, formats: string[], canvas: boolean }> }} [options]
|
|
691
614
|
* Extra or overriding backend descriptors, merged over the built-ins. Each
|
|
692
|
-
*
|
|
693
|
-
* `
|
|
694
|
-
*
|
|
695
|
-
* clone). Malformed entries throw here, at construction. The default
|
|
696
|
-
* 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.
|
|
697
618
|
*
|
|
698
619
|
* `load` resolves to a READY module: a registrant shipping its own
|
|
699
620
|
* `--target web` build instantiates inside the thunk. More than one
|
|
@@ -710,8 +631,8 @@ export class Engine {
|
|
|
710
631
|
}
|
|
711
632
|
|
|
712
633
|
/**
|
|
713
|
-
*
|
|
714
|
-
*
|
|
634
|
+
* The registered descriptor for `backendId`, or the "no backend registered"
|
|
635
|
+
* throw. Touches no binary.
|
|
715
636
|
* @param {string} backendId
|
|
716
637
|
* @returns {{ load: () => Promise<unknown>, formats: string[], canvas: boolean }}
|
|
717
638
|
*/
|
|
@@ -729,8 +650,7 @@ export class Engine {
|
|
|
729
650
|
/**
|
|
730
651
|
* `quill`'s backend id, after checking the handle. The ONE way an `Engine`
|
|
731
652
|
* verb reaches `backendId`, so "no verb touches a foreign quill" is
|
|
732
|
-
* structural rather than four remembered calls.
|
|
733
|
-
* copy".
|
|
653
|
+
* structural rather than four remembered calls.
|
|
734
654
|
* @param {Quill} quill
|
|
735
655
|
* @param {string} method the caller's name, for the rejection message
|
|
736
656
|
* @returns {string}
|
|
@@ -772,12 +692,9 @@ export class Engine {
|
|
|
772
692
|
}
|
|
773
693
|
|
|
774
694
|
/**
|
|
775
|
-
* Get (or materialize-and-cache) the backend-memory `Quill` clone for
|
|
776
|
-
*
|
|
777
|
-
*
|
|
778
|
-
* freed by now, and stored in the per-backend `WeakMap` keyed on the
|
|
779
|
-
* canonical `Quill` instance, so a later call with the same instance reuses
|
|
780
|
-
* 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.
|
|
781
698
|
* @param {any} mod the backend build module
|
|
782
699
|
* @param {string} backendId
|
|
783
700
|
* @param {object} quill the canonical instance (cache key only)
|
|
@@ -805,22 +722,15 @@ export class Engine {
|
|
|
805
722
|
*
|
|
806
723
|
* OWNERSHIP WINDOW: both caller handles are snapshotted (`doc.toJson()`, and
|
|
807
724
|
* `quill.toTree()` on a clone-cache miss) BEFORE the first await. The backend
|
|
808
|
-
* load below is a real suspension point
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
* 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").
|
|
812
728
|
*
|
|
813
729
|
* Clone lifetimes differ by design: the `doc` clone is TRANSIENT, freed in
|
|
814
|
-
* the `finally` of every call
|
|
815
|
-
*
|
|
816
|
-
*
|
|
817
|
-
* the
|
|
818
|
-
* the consumer replaces the instance. A cache miss materializes it once;
|
|
819
|
-
* subsequent calls reuse it.
|
|
820
|
-
*
|
|
821
|
-
* Both handles are checked here, so the crossing is core→backend (always two
|
|
822
|
-
* memories, always as data); core→core is a duplicate install and never gets
|
|
823
|
-
* 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.
|
|
824
734
|
* @param {string} method the caller's name, for the rejection message
|
|
825
735
|
* @param {Quill} quill
|
|
826
736
|
* @param {Document} doc
|
|
@@ -832,12 +742,9 @@ export class Engine {
|
|
|
832
742
|
const docJson = doc.toJson();
|
|
833
743
|
const quillTree = this.#quillClones.get(backendId)?.has(quill) ? null : quill.toTree();
|
|
834
744
|
const { mod, engine } = await this.#resolveBackend(backendId);
|
|
835
|
-
// The
|
|
836
|
-
//
|
|
837
|
-
//
|
|
838
|
-
// intentionally NOT freed here. `fn` MUST be synchronous: the doc clone is
|
|
839
|
-
// freed as soon as it returns, so an async `fn` would have it freed
|
|
840
|
-
// 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.
|
|
841
748
|
const backendQuill = this.#cachedQuillClone(mod, backendId, quill, quillTree);
|
|
842
749
|
let backendDoc = null;
|
|
843
750
|
try {
|
|
@@ -849,9 +756,8 @@ export class Engine {
|
|
|
849
756
|
}
|
|
850
757
|
|
|
851
758
|
/**
|
|
852
|
-
* Render `doc` against `quill` in one shot
|
|
853
|
-
*
|
|
854
|
-
* 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.
|
|
855
761
|
* @param {Quill} quill
|
|
856
762
|
* @param {Document} doc
|
|
857
763
|
* @param {object} [options] render options (`{ format, ppi, pages, producer }`)
|
|
@@ -864,12 +770,9 @@ export class Engine {
|
|
|
864
770
|
}
|
|
865
771
|
|
|
866
772
|
/**
|
|
867
|
-
* Open a live render session
|
|
868
|
-
*
|
|
869
|
-
*
|
|
870
|
-
* the caller owns the returned session and must `.free()` it. The `quill`
|
|
871
|
-
* and `doc` handles are read synchronously before the first await, so the
|
|
872
|
-
* 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.
|
|
873
776
|
* @param {Quill} quill
|
|
874
777
|
* @param {Document} doc
|
|
875
778
|
* @returns {Promise<LiveSession>}
|
|
@@ -884,10 +787,8 @@ export class Engine {
|
|
|
884
787
|
}
|
|
885
788
|
|
|
886
789
|
/**
|
|
887
|
-
* The output formats `quill`'s backend can emit
|
|
888
|
-
*
|
|
889
|
-
* `formats` manifest (NO binary load and NO quill clone) depending only on
|
|
890
|
-
* `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.
|
|
891
792
|
* @param {Quill} quill
|
|
892
793
|
* @returns {Promise<import('./runtime.js').OutputFormat[]>}
|
|
893
794
|
*/
|
|
@@ -898,13 +799,9 @@ export class Engine {
|
|
|
898
799
|
}
|
|
899
800
|
|
|
900
801
|
/**
|
|
901
|
-
* Whether `quill`'s
|
|
902
|
-
*
|
|
903
|
-
*
|
|
904
|
-
* manifest, no load and no clone. A specific compile can still refuse to
|
|
905
|
-
* paint (e.g. a 0-page document), so this can answer `true` while the
|
|
906
|
-
* resulting `LiveSession.supportsCanvas` answers `false`: gate mounting a
|
|
907
|
-
* 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`.
|
|
908
805
|
* @param {Quill} quill
|
|
909
806
|
* @returns {Promise<boolean>}
|
|
910
807
|
*/
|
|
@@ -915,20 +812,9 @@ export class Engine {
|
|
|
915
812
|
}
|
|
916
813
|
|
|
917
814
|
/**
|
|
918
|
-
* Thin wrapper over a backend's live render session.
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
* opened from have already been freed: the session retains what `update`
|
|
922
|
-
* needs.
|
|
923
|
-
*
|
|
924
|
-
* Geometry reads (`regions`, `positionAt`, `locate`) resolve against the
|
|
925
|
-
* current compile; anchoring a caret or selection across edits is the editor's
|
|
926
|
-
* job (its own transaction mapping): re-read geometry after each committed
|
|
927
|
-
* `update`.
|
|
928
|
-
*
|
|
929
|
-
* `paint` writes a COMPLETE page raster (all content visible, no caller-side
|
|
930
|
-
* compositing) for every backend that supports canvas (Typst rasterizes
|
|
931
|
-
* 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.
|
|
932
818
|
*/
|
|
933
819
|
export class LiveSession {
|
|
934
820
|
/**
|
|
@@ -943,9 +829,6 @@ export class LiveSession {
|
|
|
943
829
|
#mod;
|
|
944
830
|
|
|
945
831
|
/**
|
|
946
|
-
* Recompile the session against `doc`: the edit verb of a live preview.
|
|
947
|
-
* Transactional: on throw every read keeps serving the last-good compile.
|
|
948
|
-
* On success reads serve the new compile; repaint `dirtyPages ∩ visible`.
|
|
949
832
|
* @param {Document} doc
|
|
950
833
|
* @returns {import('./runtime.d.ts').ChangeSet}
|
|
951
834
|
*/
|
|
@@ -968,12 +851,8 @@ export class LiveSession {
|
|
|
968
851
|
}
|
|
969
852
|
/**
|
|
970
853
|
* `true` iff `paint`/`pageSize` will succeed for THIS compile: the
|
|
971
|
-
* authoritative answer,
|
|
972
|
-
*
|
|
973
|
-
* when `Engine.supportsCanvas` answered `true` for the same `quill` (that
|
|
974
|
-
* probe is a pre-session backend estimate; e.g. a canvas-capable backend
|
|
975
|
-
* compiled to a 0-page document has nothing to paint). Re-check this getter
|
|
976
|
-
* 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.
|
|
977
856
|
* @returns {boolean}
|
|
978
857
|
*/
|
|
979
858
|
get supportsCanvas() {
|
|
@@ -989,10 +868,6 @@ export class LiveSession {
|
|
|
989
868
|
}
|
|
990
869
|
|
|
991
870
|
/**
|
|
992
|
-
* Schema-field geometry for this compiled session: one region per
|
|
993
|
-
* schema-bound field, keyed on its quill schema field path. A session-level
|
|
994
|
-
* query (no render); read it to place field overlays / cross-navigation over
|
|
995
|
-
* a `paint`-ed canvas.
|
|
996
871
|
* @returns {import('./runtime.d.ts').FieldRegion[]}
|
|
997
872
|
*/
|
|
998
873
|
regions() {
|
|
@@ -1000,12 +875,6 @@ export class LiveSession {
|
|
|
1000
875
|
}
|
|
1001
876
|
|
|
1002
877
|
/**
|
|
1003
|
-
* The whole-field highlight boxes for `field`: one union rect per page,
|
|
1004
|
-
* over the field's `span`-bearing content segments. Owns the union
|
|
1005
|
-
* `regions()` leaves derived (span-filter + per-page union), so a "highlight
|
|
1006
|
-
* the focused field" consumer stops reimplementing it. Content only: a field
|
|
1007
|
-
* placed solely as a scalar reference or a bound widget returns `[]`, its
|
|
1008
|
-
* box is a single `regions()` rect.
|
|
1009
878
|
* @param {string} field
|
|
1010
879
|
* @returns {import('./runtime.d.ts').FieldRegion[]}
|
|
1011
880
|
*/
|
|
@@ -1014,11 +883,6 @@ export class LiveSession {
|
|
|
1014
883
|
}
|
|
1015
884
|
|
|
1016
885
|
/**
|
|
1017
|
-
* The schema field whose content is under a point on `page`: the forward
|
|
1018
|
-
* (click → field) direction, resolving *every* placement, not just the first
|
|
1019
|
-
* that `regions` enumerates. `x`/`y` are PDF points with a bottom-left origin
|
|
1020
|
-
* (the `FieldRegion.rect` space). See `runtime.d.ts` for the click-to-point
|
|
1021
|
-
* inverse transform.
|
|
1022
886
|
* @param {number} page
|
|
1023
887
|
* @param {number} x
|
|
1024
888
|
* @param {number} y
|
|
@@ -1053,10 +917,6 @@ export class LiveSession {
|
|
|
1053
917
|
}
|
|
1054
918
|
|
|
1055
919
|
/**
|
|
1056
|
-
* Paint `page` into a 2D canvas context. The painted raster is COMPLETE
|
|
1057
|
-
* (all page content visible, no caller-side compositing) for both the Typst
|
|
1058
|
-
* and pdfform backends. See `runtime.d.ts` for the DPR/clamp math and the
|
|
1059
|
-
* region-overlay coordinate transform.
|
|
1060
920
|
* @param {CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D} ctx
|
|
1061
921
|
* @param {number} page
|
|
1062
922
|
* @param {object} [options]
|
|
@@ -1130,10 +990,8 @@ export class DocumentWriter {
|
|
|
1130
990
|
return this.#doc._commitFields(this.#quill, MAIN_CARD_ADDR, fields);
|
|
1131
991
|
}
|
|
1132
992
|
/**
|
|
1133
|
-
* Revise the main body from markdown
|
|
1134
|
-
*
|
|
1135
|
-
* reached through the writer: a body carries no field schema, so there is
|
|
1136
|
-
* 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.
|
|
1137
995
|
* @param {string} markdown
|
|
1138
996
|
* @returns {import('../core/wasm.js').Delta}
|
|
1139
997
|
*/
|
|
@@ -1142,14 +1000,9 @@ export class DocumentWriter {
|
|
|
1142
1000
|
}
|
|
1143
1001
|
/**
|
|
1144
1002
|
* Revise the content main-card field `name` from authored text: typed *and*
|
|
1145
|
-
* anchor-preserving.
|
|
1146
|
-
* schema-conformed
|
|
1147
|
-
* `
|
|
1148
|
-
* {@link Delta}.
|
|
1149
|
-
*
|
|
1150
|
-
* The codec comes from the declared type: `richtext` diffs markdown, while
|
|
1151
|
-
* `plaintext` diffs the literal text and never imports markdown, so a
|
|
1152
|
-
* 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.
|
|
1153
1006
|
* @param {string} name
|
|
1154
1007
|
* @param {string} text
|
|
1155
1008
|
* @returns {import('../core/wasm.js').Delta}
|
|
@@ -1159,13 +1012,9 @@ export class DocumentWriter {
|
|
|
1159
1012
|
}
|
|
1160
1013
|
/**
|
|
1161
1014
|
* Build a composable card of `kind`, typed-commit `fields` onto it, set its
|
|
1162
|
-
* body from optional markdown, and place it: the
|
|
1163
|
-
*
|
|
1164
|
-
*
|
|
1165
|
-
* atomic call rather than `addCard` + `moveCard`. Transactional: the card is
|
|
1166
|
-
* committed in full before it joins the document, so a rejected field (throws
|
|
1167
|
-
* a per-field diagnostic bundle, `UnknownField` per undeclared name) or an
|
|
1168
|
-
* 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.
|
|
1169
1018
|
* @param {string} kind
|
|
1170
1019
|
* @param {Record<string, unknown>} [fields]
|
|
1171
1020
|
* @param {string} [body]
|
|
@@ -1176,8 +1025,6 @@ export class DocumentWriter {
|
|
|
1176
1025
|
return this.#doc._addCard(this.#quill, kind, fields, body, at);
|
|
1177
1026
|
}
|
|
1178
1027
|
/**
|
|
1179
|
-
* Remove the composable card at `index`, returning it (or `undefined` if the
|
|
1180
|
-
* index is out of range): the writer spelling of `Document.removeCard`.
|
|
1181
1028
|
* @param {number} index
|
|
1182
1029
|
* @returns {import('../core/wasm.js').Card | undefined}
|
|
1183
1030
|
*/
|
|
@@ -1185,14 +1032,9 @@ export class DocumentWriter {
|
|
|
1185
1032
|
return this.#doc.removeCard(index);
|
|
1186
1033
|
}
|
|
1187
1034
|
/**
|
|
1188
|
-
* A {@link CardWriter} bound to the composable card at `index
|
|
1189
|
-
*
|
|
1190
|
-
* `
|
|
1191
|
-
* here.
|
|
1192
|
-
*
|
|
1193
|
-
* The cursor is ephemeral: bind, write, discard. It holds `index`, not the
|
|
1194
|
-
* card: a `removeCard`/`addCard` between binding and writing silently
|
|
1195
|
-
* 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.
|
|
1196
1038
|
* @param {number} index
|
|
1197
1039
|
* @returns {CardWriter}
|
|
1198
1040
|
*/
|
|
@@ -1227,9 +1069,8 @@ export class CardWriter {
|
|
|
1227
1069
|
return this.#index;
|
|
1228
1070
|
}
|
|
1229
1071
|
/**
|
|
1230
|
-
* The bound card's `$kind
|
|
1231
|
-
*
|
|
1232
|
-
* 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.
|
|
1233
1074
|
* @returns {string}
|
|
1234
1075
|
*/
|
|
1235
1076
|
get kind() {
|
|
@@ -1257,8 +1098,7 @@ export class CardWriter {
|
|
|
1257
1098
|
return this.#doc._commitFields(this.#quill, { card: this.#index }, fields);
|
|
1258
1099
|
}
|
|
1259
1100
|
/**
|
|
1260
|
-
*
|
|
1261
|
-
* {@link Delta}: the card twin of {@link DocumentWriter.reviseBody}.
|
|
1101
|
+
* The card twin of {@link DocumentWriter.reviseBody}.
|
|
1262
1102
|
* @param {string} markdown
|
|
1263
1103
|
* @returns {import('../core/wasm.js').Delta}
|
|
1264
1104
|
*/
|
|
@@ -1266,11 +1106,8 @@ export class CardWriter {
|
|
|
1266
1106
|
return this.#doc.revise({ card: this.#index }, markdown);
|
|
1267
1107
|
}
|
|
1268
1108
|
/**
|
|
1269
|
-
*
|
|
1270
|
-
*
|
|
1271
|
-
* codec included. Throws `UnknownField` for an undeclared name and
|
|
1272
|
-
* `IndexOutOfRange` if the bound index is out of range. Returns the text
|
|
1273
|
-
* {@link Delta}.
|
|
1109
|
+
* The card twin of {@link DocumentWriter.reviseField}. Throws `UnknownField`
|
|
1110
|
+
* for an undeclared name and `IndexOutOfRange` for a bad bound index.
|
|
1274
1111
|
* @param {string} name
|
|
1275
1112
|
* @param {string} text
|
|
1276
1113
|
* @returns {import('../core/wasm.js').Delta}
|
|
@@ -1281,17 +1118,12 @@ export class CardWriter {
|
|
|
1281
1118
|
}
|
|
1282
1119
|
|
|
1283
1120
|
// ── `quill.writer(doc)`, the typed front door ──────────────────────────────
|
|
1284
|
-
//
|
|
1285
|
-
//
|
|
1286
|
-
//
|
|
1287
|
-
// re-exported `Quill` prototype rather than wrapped: `Quill === CoreQuill`
|
|
1288
|
-
// stays true (the identity invariant above); this only adds a method that
|
|
1289
|
-
// 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.
|
|
1290
1124
|
/**
|
|
1291
|
-
* A {@link DocumentWriter} binding this quill's schema to `doc
|
|
1292
|
-
*
|
|
1293
|
-
* reference and owns neither, so there is nothing to `free()`. Ephemeral by
|
|
1294
|
-
* 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.
|
|
1295
1127
|
* @this {Quill}
|
|
1296
1128
|
* @param {Document} doc the document to mutate, held by reference (not owned)
|
|
1297
1129
|
* @returns {DocumentWriter}
|
|
@@ -1300,22 +1132,16 @@ Quill.prototype.writer = function writer(doc) {
|
|
|
1300
1132
|
return new DocumentWriter(this, doc);
|
|
1301
1133
|
};
|
|
1302
1134
|
|
|
1303
|
-
// ── Typed-reader sugar: the schema-plane read surface
|
|
1304
|
-
// The
|
|
1305
|
-
// 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
|
|
1306
1137
|
// reads back `undefined` rather than as the typo it is. Binding the quill's
|
|
1307
|
-
// schema
|
|
1308
|
-
//
|
|
1309
|
-
// its literal text, every other type verbatim, and an unknown name throws
|
|
1310
|
-
// `UnknownField`. A field's markdown lives here, not on the body-only
|
|
1311
|
-
// `bodyMarkdown`. Like the writer classes these hold the caller's handles by
|
|
1312
|
-
// 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.
|
|
1313
1140
|
|
|
1314
1141
|
/**
|
|
1315
|
-
* A {@link Document} bound to its {@link Quill} for typed reads
|
|
1316
|
-
*
|
|
1317
|
-
*
|
|
1318
|
-
* 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.
|
|
1319
1145
|
*/
|
|
1320
1146
|
export class DocumentReader {
|
|
1321
1147
|
#quill;
|
|
@@ -1348,15 +1174,11 @@ export class DocumentReader {
|
|
|
1348
1174
|
return this.#doc._readerGet(this.#quill, addr);
|
|
1349
1175
|
}
|
|
1350
1176
|
/**
|
|
1351
|
-
* Read the content field at `addr` as
|
|
1352
|
-
*
|
|
1353
|
-
*
|
|
1354
|
-
*
|
|
1355
|
-
*
|
|
1356
|
-
* document was built. An absent `addr.field` reads the body `Content`.
|
|
1357
|
-
* `undefined` for an absent field; throws `UnknownField`, `FieldNotContent`
|
|
1358
|
-
* for a type that is not a content leaf, `FieldDecode` for an undecodable
|
|
1359
|
-
* 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`.
|
|
1360
1182
|
* @param {import('../core/wasm.js').Addr | string} addr
|
|
1361
1183
|
* @returns {import('../core/wasm.js').Content | undefined}
|
|
1362
1184
|
*/
|
|
@@ -1364,18 +1186,29 @@ export class DocumentReader {
|
|
|
1364
1186
|
return this.#doc._readerGetContent(this.#quill, addr);
|
|
1365
1187
|
}
|
|
1366
1188
|
/**
|
|
1367
|
-
*
|
|
1368
|
-
*
|
|
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({})`.
|
|
1369
1204
|
* @returns {string}
|
|
1370
1205
|
*/
|
|
1371
1206
|
bodyMarkdown() {
|
|
1372
1207
|
return this.#doc._readerGet(this.#quill, {});
|
|
1373
1208
|
}
|
|
1374
1209
|
/**
|
|
1375
|
-
* A {@link CardReader} bound to the composable card at `index
|
|
1376
|
-
*
|
|
1377
|
-
* time), so an out-of-range index does not throw here. Ephemeral like the
|
|
1378
|
-
* 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`
|
|
1379
1212
|
* between binding and reading silently retargets it.
|
|
1380
1213
|
* @param {number} index
|
|
1381
1214
|
* @returns {CardReader}
|
|
@@ -1429,8 +1262,7 @@ export class CardReader {
|
|
|
1429
1262
|
return this.#doc._readerGet(this.#quill, { card: this.#index, field: name });
|
|
1430
1263
|
}
|
|
1431
1264
|
/**
|
|
1432
|
-
*
|
|
1433
|
-
* `Content`: the card twin of {@link DocumentReader.getContent}.
|
|
1265
|
+
* The card twin of {@link DocumentReader.getContent}.
|
|
1434
1266
|
* @param {string} name
|
|
1435
1267
|
* @returns {import('../core/wasm.js').Content | undefined}
|
|
1436
1268
|
*/
|
|
@@ -1438,7 +1270,16 @@ export class CardReader {
|
|
|
1438
1270
|
return this.#doc._readerGetContent(this.#quill, { card: this.#index, field: name });
|
|
1439
1271
|
}
|
|
1440
1272
|
/**
|
|
1441
|
-
*
|
|
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}.
|
|
1442
1283
|
* @returns {string}
|
|
1443
1284
|
*/
|
|
1444
1285
|
bodyMarkdown() {
|
|
@@ -1447,14 +1288,10 @@ export class CardReader {
|
|
|
1447
1288
|
}
|
|
1448
1289
|
|
|
1449
1290
|
// ── `quill.reader(doc)`: the schema-plane read front door ─────────────────────
|
|
1450
|
-
//
|
|
1451
|
-
// prototype (the `Quill === CoreQuill` identity invariant holds: this only adds
|
|
1452
|
-
// a method constructing the pure-JS reader, which owns no WASM handle).
|
|
1291
|
+
// Patched onto the same re-exported `Quill` prototype as `writer`.
|
|
1453
1292
|
/**
|
|
1454
|
-
* A {@link DocumentReader} binding this quill's schema to `doc
|
|
1455
|
-
*
|
|
1456
|
-
* reader holds both handles by reference and owns neither, so there is nothing to
|
|
1457
|
-
* `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.
|
|
1458
1295
|
* @this {Quill}
|
|
1459
1296
|
* @param {Document} doc the document to read, held by reference (not owned)
|
|
1460
1297
|
* @returns {DocumentReader}
|