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