@quillmark/wasm 0.112.0 → 0.113.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 +1487 -0
- package/README.md +91 -66
- package/core/wasm.d.ts +117 -182
- package/core/wasm.js +245 -391
- package/core/wasm_bg.wasm +0 -0
- package/core/wasm_bg.wasm.d.ts +5 -11
- package/package.json +2 -2
- package/{backends/pdfform → render}/wasm.d.ts +168 -264
- package/{backends/pdfform → render}/wasm.js +326 -450
- package/{backends/typst → render}/wasm_bg.wasm +0 -0
- package/{backends/typst → render}/wasm_bg.wasm.d.ts +7 -15
- package/runtime/runtime.d.ts +84 -178
- package/runtime/runtime.js +141 -583
- package/backends/pdfform/wasm_bg.wasm +0 -0
- package/backends/pdfform/wasm_bg.wasm.d.ts +0 -107
- package/backends/typst/wasm.d.ts +0 -1511
- package/backends/typst/wasm.js +0 -3008
package/runtime/runtime.js
CHANGED
|
@@ -1,34 +1,13 @@
|
|
|
1
1
|
/* @ts-self-types="./runtime.d.ts" */
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// The canonical consumer API, and the package's sole export. Its contract is
|
|
4
|
+
// `runtime.d.ts`, which a consumer's TypeScript reads through the package's
|
|
5
|
+
// `exports.types` map; the multi-binary design it implements is
|
|
6
|
+
// `prose/canon/BINDINGS.md`.
|
|
4
7
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// A handle from one memory cannot be used by another; this module hides that
|
|
9
|
-
// seam behind `await init()` and a static `Engine`.
|
|
10
|
-
//
|
|
11
|
-
// - `Quill` and `Document` ARE the core build's classes, handed out by the
|
|
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.
|
|
17
|
-
//
|
|
18
|
-
// - `Engine` is the render dispatcher. It routes on `quill.backendId`, lazily
|
|
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.
|
|
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.
|
|
8
|
+
// A `Quill` instance's contents never change after construction — mutate by
|
|
9
|
+
// replacing the instance — which is what makes the cached backend clone below
|
|
10
|
+
// safe to hand out for the instance's whole life.
|
|
32
11
|
|
|
33
12
|
// Local bindings, so this module can augment them: `quill.writer(doc)` is
|
|
34
13
|
// patched onto the prototype below, and `instanceof` reads them directly. The
|
|
@@ -43,32 +22,11 @@ import { toModuleSource } from '#quillmark-env';
|
|
|
43
22
|
import { importMarkdown, exportMarkdown, rebase, mapPos, mapMarks } from '../core/wasm.js';
|
|
44
23
|
import { parseDocPath, formatDocPath } from '../core/wasm.js';
|
|
45
24
|
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
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.
|
|
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.
|
|
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.
|
|
67
|
-
//
|
|
68
|
-
// FAILURE DELIVERY follows the FUNCTION kind, not the failure kind: a sync verb
|
|
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.
|
|
25
|
+
// A `--target web` build exports its classes synchronously but carries no wasm
|
|
26
|
+
// instance until something instantiates it. This module owns that for core,
|
|
27
|
+
// behind one awaited gate; `Engine` owns it for the backends, inside their lazy
|
|
28
|
+
// load. Which values that gate holds and which stay static exports:
|
|
29
|
+
// `prose/canon/BINDINGS.md`.
|
|
72
30
|
|
|
73
31
|
/**
|
|
74
32
|
* The gated surface: the core build's values, which are exactly the ones its
|
|
@@ -104,38 +62,8 @@ let coreInit;
|
|
|
104
62
|
let coreInitSource;
|
|
105
63
|
|
|
106
64
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* ```js
|
|
110
|
-
* import { init } from '@quillmark/wasm';
|
|
111
|
-
* const { Quill, Document } = await init();
|
|
112
|
-
* ```
|
|
113
|
-
*
|
|
114
|
-
* The classes and the free functions come from here and nowhere else, so the
|
|
115
|
-
* pre-init mistake is not expressible. Destructure at each entry point (route
|
|
116
|
-
* loader, hydration path, worker) rather than threading one result around: the
|
|
117
|
-
* gate is memoized, so every await after the first is free.
|
|
118
|
-
*
|
|
119
|
-
* Identical in every environment: in a browser the binary is fetched and
|
|
120
|
-
* streamed, under Node it is read off disk, and the call site is the same line.
|
|
121
|
-
*
|
|
122
|
-
* Idempotent and concurrency-safe: every non-conflicting call returns the same
|
|
123
|
-
* promise, so several entry points cost one instantiation. A failed init clears
|
|
124
|
-
* the memo, so a retry is possible.
|
|
125
|
-
*
|
|
126
|
-
* Both failures reject (§ "Initialization", FAILURE DELIVERY): one `catch`
|
|
127
|
-
* around `await init(...)` covers `runtime::init_conflict` and
|
|
128
|
-
* `runtime::init_failed` alike.
|
|
129
|
-
*
|
|
130
|
-
* @param {import('../core/wasm.js').InitInput} [source] override the binary's
|
|
131
|
-
* source (bytes, a `Response`, a `WebAssembly.Module`, a URL) for hosts that
|
|
132
|
-
* route assets themselves or embed the binary. Pass it on the FIRST call; a
|
|
133
|
-
* later call passing a *different* source rejects with
|
|
134
|
-
* `runtime::init_conflict` rather than silently ignoring it. Passing the same
|
|
135
|
-
* value again is fine, so several entry points may each `await init(BYTES)`
|
|
136
|
-
* against one constant.
|
|
137
|
-
* @returns {Promise<import('./runtime.js').CoreSurface>} the core surface, once
|
|
138
|
-
* its instance is live
|
|
65
|
+
* @param {import('../core/wasm.js').InitInput} [source]
|
|
66
|
+
* @returns {Promise<import('./runtime.js').CoreSurface>}
|
|
139
67
|
*/
|
|
140
68
|
export function init(source) {
|
|
141
69
|
if (coreInit) {
|
|
@@ -195,46 +123,17 @@ async function instantiateCore(source) {
|
|
|
195
123
|
}
|
|
196
124
|
}
|
|
197
125
|
|
|
198
|
-
// ── The main-card address ───────────────────────────────────────────────────
|
|
199
126
|
/**
|
|
200
|
-
* The main card's address: the default target of the card-scoped verbs
|
|
201
|
-
* (`storeFields` / `storeExt` / `commitFields` / …). A named, `CardAddr`-typed
|
|
202
|
-
* alias for the empty address `{}`, so a main-card write names its target:
|
|
203
|
-
* `doc.storeFields(MAIN_CARD_ADDR, fields)`. It IS `{}` (frozen), a pure alias:
|
|
204
|
-
* `{}` and `undefined` stay equally valid. Card axis only: a card selector,
|
|
205
|
-
* never a field address.
|
|
206
127
|
* @type {import('../core/wasm.js').CardAddr}
|
|
207
128
|
*/
|
|
208
129
|
export const MAIN_CARD_ADDR = Object.freeze({});
|
|
209
130
|
|
|
210
|
-
// ── The variant discriminant key ────────────────────────────────────────────
|
|
211
131
|
/**
|
|
212
|
-
* The key carrying the discriminant inside a variant-bearing enum's value.
|
|
213
|
-
*
|
|
214
|
-
* A field declaring `variants:` rests as a container, `{value: <member>, …that
|
|
215
|
-
* member's fields}`, so reading or writing one means naming this key:
|
|
216
|
-
* `doc.storeFields(MAIN_CARD_ADDR, { classification: { [VARIANT_DISCRIMINANT_KEY]: 'CUI' } })`.
|
|
217
|
-
* It crosses the boundary inside untyped container data, with no type to read
|
|
218
|
-
* it off.
|
|
219
|
-
*
|
|
220
|
-
* Reserved: no variant may declare a field under it
|
|
221
|
-
* (`quill::variant_reserved_field_name`), and `QuillFieldSchema.variants`,
|
|
222
|
-
* keyed by member, never contains it.
|
|
223
132
|
* @type {'value'}
|
|
224
133
|
*/
|
|
225
134
|
export const VARIANT_DISCRIMINANT_KEY = 'value';
|
|
226
135
|
|
|
227
136
|
/**
|
|
228
|
-
* Narrow an unknown caught value to a `QuillmarkError`, the error every
|
|
229
|
-
* fallible method in this package throws: a real `Error` with a non-empty
|
|
230
|
-
* `diagnostics` array attached (same entry shape as `RenderResult.warnings`).
|
|
231
|
-
*
|
|
232
|
-
* Structural by necessity AND by design: the WASM layer constructs a plain
|
|
233
|
-
* `Error` and attaches the property (there is no error class to `instanceof`),
|
|
234
|
-
* and a structural check narrows errors from any build or WASM instance in the
|
|
235
|
-
* page. The deliberate exception to § "Handles from another copy": an error is
|
|
236
|
-
* data, not a handle, so nothing is gained by refusing one that crossed.
|
|
237
|
-
*
|
|
238
137
|
* @param {unknown} e
|
|
239
138
|
* @returns {e is Error & { diagnostics: import('../core/wasm.js').Diagnostic[] }}
|
|
240
139
|
*/
|
|
@@ -258,62 +157,35 @@ function quillmarkError(code, message, hint) {
|
|
|
258
157
|
return err;
|
|
259
158
|
}
|
|
260
159
|
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
// `toTree` serialize either way) and is not done: it leaves a package where some
|
|
269
|
-
// verbs work and some throw, and it hides a cliff, since a crossed read is a
|
|
270
|
-
// whole-document `toJson` + `fromJson` and a form reading fifty fields pays
|
|
271
|
-
// fifty round trips.
|
|
272
|
-
//
|
|
273
|
-
// What the checks deliver is the ERROR, not the rejection. wasm-bindgen's glue
|
|
274
|
-
// already rejects a foreign class wherever a method declares a reference
|
|
275
|
-
// parameter, but its `_assertClass` throws a bare `Error` reading
|
|
276
|
-
// `expected instance of Document` at a value that IS a `Document`, so
|
|
277
|
-
// `isQuillmarkError` returns false and the failure leaves this package's error
|
|
278
|
-
// contract. The checks front-run it with a `QuillmarkError` naming both cause
|
|
279
|
-
// and cure, and cover the seams with no `_assertClass` to front-run: `Engine`
|
|
280
|
-
// and `LiveSession.update` cross into backend memory as data, where a foreign
|
|
281
|
-
// handle would silently work at the price of that round trip and a per-copy
|
|
282
|
-
// split of the quill clone cache.
|
|
160
|
+
// These checks deliver the ERROR, not the rejection, at the seams that cross
|
|
161
|
+
// into backend memory as DATA — `Engine` and `LiveSession.update` — where a
|
|
162
|
+
// foreign handle would otherwise work silently at the price of a whole-document
|
|
163
|
+
// round trip and a per-copy split of the quill clone cache, and at the four
|
|
164
|
+
// writer/reader binds, whose constructors hold both handles. A method declaring
|
|
165
|
+
// a reference parameter needs none of this: wasm-bindgen's own `_assertClass`
|
|
166
|
+
// refuses a foreign class there.
|
|
283
167
|
|
|
284
|
-
/** Per class: the
|
|
168
|
+
/** Per class: the diagnostic for a value that is not one of THIS copy's handles. */
|
|
285
169
|
const HANDLE_KINDS = {
|
|
286
170
|
Quill: {
|
|
287
171
|
code: 'runtime::not_a_quill',
|
|
288
|
-
|
|
289
|
-
hint: 'Pass a Quill built by Quill.fromTree.'
|
|
172
|
+
hint: 'Pass a Quill built by Quill.fromTree. A Quill from another copy of @quillmark/wasm is refused too — each copy is its own WASM linear memory and its own class — so run `npm ls @quillmark/wasm` and dedupe to one.'
|
|
290
173
|
},
|
|
291
174
|
Document: {
|
|
292
175
|
code: 'runtime::not_a_document',
|
|
293
|
-
|
|
294
|
-
hint: 'Pass a Document built by Document.fromMarkdown / fromJson or quill.seedDocument.'
|
|
176
|
+
hint: 'Pass a Document built by Document.fromMarkdown / fromStored or quill.seedDocument. A Document from another copy of @quillmark/wasm is refused too — each copy is its own WASM linear memory and its own class — so run `npm ls @quillmark/wasm` and dedupe to one.'
|
|
295
177
|
}
|
|
296
178
|
};
|
|
297
179
|
|
|
298
180
|
/**
|
|
299
|
-
* The rejection for a value that is not one of this copy's handles.
|
|
300
|
-
* so two diagnostics: a value carrying the class's serializer is that class
|
|
301
|
-
* from ANOTHER copy (dedupe the install), anything else is the wrong argument
|
|
302
|
-
* (fix the call).
|
|
181
|
+
* The rejection for a value that is not one of this copy's handles.
|
|
303
182
|
* @param {unknown} value
|
|
304
183
|
* @param {string} method
|
|
305
184
|
* @param {'Quill' | 'Document'} className
|
|
306
185
|
* @returns {Error & { diagnostics: import('../core/wasm.js').Diagnostic[] }}
|
|
307
186
|
*/
|
|
308
187
|
function notLocal(value, method, className) {
|
|
309
|
-
const { code,
|
|
310
|
-
if (value && typeof (/** @type {any} */ (value)[probe]) === 'function') {
|
|
311
|
-
return quillmarkError(
|
|
312
|
-
'runtime::foreign_handle',
|
|
313
|
-
`${method}: the ${className} belongs to a different copy of @quillmark/wasm. Handles never cross between copies: each copy is its own WASM linear memory and its own ${className} class.`,
|
|
314
|
-
'Two copies of @quillmark/wasm are installed. Run `npm ls @quillmark/wasm` and dedupe to one.'
|
|
315
|
-
);
|
|
316
|
-
}
|
|
188
|
+
const { code, hint } = HANDLE_KINDS[className];
|
|
317
189
|
return quillmarkError(
|
|
318
190
|
code,
|
|
319
191
|
`${method}: expected a ${className}, got ${value === null ? 'null' : typeof value}.`,
|
|
@@ -343,213 +215,26 @@ function requireLocalQuill(quill, method) {
|
|
|
343
215
|
throw notLocal(quill, method, 'Quill');
|
|
344
216
|
}
|
|
345
217
|
|
|
346
|
-
// Marker for the patches below. `Symbol.for`, not a module-local `Symbol()`:
|
|
347
|
-
// re-evaluating THIS module (Vite HMR, a Vitest worker sharing a module graph)
|
|
348
|
-
// against an already-patched (because cached) core build must see the existing
|
|
349
|
-
// marker, or each pass wraps the previous wrapper.
|
|
350
|
-
const HANDLE_CHECKED = Symbol.for('@quillmark/wasm:handle-checked');
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Replace `proto[name]` with `wrap(original)`, once.
|
|
354
|
-
* @param {object} proto
|
|
355
|
-
* @param {string} name
|
|
356
|
-
* @param {(original: Function) => Function} wrap
|
|
357
|
-
*/
|
|
358
|
-
function patchHandleChecked(proto, name, wrap) {
|
|
359
|
-
const original = /** @type {any} */ (proto)[name];
|
|
360
|
-
if (typeof original !== 'function' || original[HANDLE_CHECKED]) return;
|
|
361
|
-
const patched = wrap(original);
|
|
362
|
-
/** @type {any} */ (patched)[HANDLE_CHECKED] = true;
|
|
363
|
-
// Keep the method's name so a stack trace still reads `Quill.validate`.
|
|
364
|
-
Object.defineProperty(patched, 'name', { value: name, configurable: true });
|
|
365
|
-
/** @type {any} */ (proto)[name] = patched;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// The core methods declaring a `&Document` parameter. Each already refuses a
|
|
369
|
-
// foreign handle inside `_assertClass`; the patch makes the refusal legible.
|
|
370
|
-
patchHandleChecked(Document.prototype, 'equals', (original) =>
|
|
371
|
-
function equals(/** @type {any} */ other) {
|
|
372
|
-
requireLocalDoc(other, 'Document.equals');
|
|
373
|
-
return original.call(this, other);
|
|
374
|
-
}
|
|
375
|
-
);
|
|
376
|
-
for (const name of /** @type {const} */ (['validate', 'resolve', 'conform'])) {
|
|
377
|
-
// Named once per patch, not per call: `validate` runs per keystroke.
|
|
378
|
-
const method = `Quill.${name}`;
|
|
379
|
-
patchHandleChecked(Quill.prototype, name, (original) =>
|
|
380
|
-
function (/** @type {any} */ doc) {
|
|
381
|
-
requireLocalDoc(doc, method);
|
|
382
|
-
return original.call(this, doc);
|
|
383
|
-
}
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// The typed writer/reader primitives (`Document._commitField` and friends) take
|
|
388
|
-
// the QUILL by reference, so they hit the same `_assertClass` from the other
|
|
389
|
-
// direction. Checked at the four writer/reader classes below, not patched onto
|
|
390
|
-
// `Document`: a foreign document carries its OWN prototype, so patching this
|
|
391
|
-
// copy's would never run.
|
|
392
|
-
|
|
393
|
-
// ── Open-set discriminant guards ────────────────────────────────────────────
|
|
394
|
-
// `ContentIsland.type`, `ContentMark.type`, `ContentLine.kind`, and
|
|
395
|
-
// `ContentContainer.container` are OPEN sets: each union carries a residual
|
|
396
|
-
// `{ …: string; … }` arm, so a bare `x.type === 'table'` check never narrows the
|
|
397
|
-
// payload, TS keeps the residual arm live (a `string` can be `'table'`),
|
|
398
|
-
// leaving `props` / the mark payload / `level` opaque at every consumer. These
|
|
399
|
-
// are the checked narrowing path: on the true branch the payload's pinned shape
|
|
400
|
-
// is asserted. Only the payload-carrying arms get a guard: an island always
|
|
401
|
-
// carries `props`, and a `link`/`anchor` mark, a `heading`/`code` line and a
|
|
402
|
-
// `list_item` container each carry their payload in `attrs`; the payload-free
|
|
403
|
-
// arms (`strong`/`emph`/`underline`/`strike`/`code` marks, `para`/`island`/
|
|
404
|
-
// `rule` lines, `quote`) omit `attrs` and narrow to nothing. An unrecognized
|
|
405
|
-
// discriminant fails every guard and carries the same `attrs` a known one
|
|
406
|
-
// would.
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* @param {import('../core/wasm.js').ContentIsland} island
|
|
410
|
-
* @returns {island is import('../core/wasm.js').ContentIsland & { type: 'table'; props: import('../core/wasm.js').TableProps }}
|
|
411
|
-
*/
|
|
412
|
-
export function isTableIsland(island) {
|
|
413
|
-
return island.type === 'table';
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* @param {import('../core/wasm.js').ContentIsland} island
|
|
418
|
-
* @returns {island is import('../core/wasm.js').ContentIsland & { type: 'image'; props: import('../core/wasm.js').ImageProps }}
|
|
419
|
-
*/
|
|
420
|
-
export function isImageIsland(island) {
|
|
421
|
-
return island.type === 'image';
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* @param {import('../core/wasm.js').ContentMark} mark
|
|
426
|
-
* @returns {mark is import('../core/wasm.js').ContentMark & { type: 'link'; attrs: { url: string } }}
|
|
427
|
-
*/
|
|
428
|
-
export function isLinkMark(mark) {
|
|
429
|
-
return mark.type === 'link';
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* @param {import('../core/wasm.js').ContentMark} mark
|
|
434
|
-
* @returns {mark is import('../core/wasm.js').ContentMark & { type: 'anchor'; attrs: { id: string } }}
|
|
435
|
-
*/
|
|
436
|
-
export function isAnchorMark(mark) {
|
|
437
|
-
return mark.type === 'anchor';
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* @param {import('../core/wasm.js').ContentLine} line
|
|
442
|
-
* @returns {line is import('../core/wasm.js').ContentLine & { kind: 'heading'; attrs: { level: number } }}
|
|
443
|
-
*/
|
|
444
|
-
export function isHeadingLine(line) {
|
|
445
|
-
return line.kind === 'heading';
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* @param {import('../core/wasm.js').ContentLine} line
|
|
450
|
-
* @returns {line is import('../core/wasm.js').ContentLine & { kind: 'code'; attrs?: { lang?: string } }}
|
|
451
|
-
*/
|
|
452
|
-
export function isCodeLine(line) {
|
|
453
|
-
return line.kind === 'code';
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* @param {import('../core/wasm.js').ContentContainer} container
|
|
458
|
-
* @returns {container is import('../core/wasm.js').ContentContainer & { container: 'list_item'; attrs: { ordered: boolean; start: number; ordinal: number }; instance?: number }}
|
|
459
|
-
*/
|
|
460
|
-
export function isListItemContainer(container) {
|
|
461
|
-
return container.container === 'list_item';
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// ── Open-set membership guards ──────────────────────────────────────────────
|
|
465
|
-
// The guards above each answer "is this arm X". These four answer "is this a
|
|
466
|
-
// value this build knows?", the question any read-modify-write consumer must
|
|
467
|
-
// ask, since lowering an edit restates every line's kind and containers. A
|
|
468
|
-
// predicate rather than an exported name list, because the tables below are
|
|
469
|
-
// upstream's business: they are pinned against the Rust source by
|
|
470
|
-
// `tests/known_names_drift.rs`, so adding a built-in means editing there, here,
|
|
471
|
-
// and the TS unions in `src/engine.rs` in one commit.
|
|
472
|
-
//
|
|
473
|
-
// These classify unknown *tags*, not unknown *payloads on known tags*. A future
|
|
474
|
-
// `kind: "footnote"` with a sibling `ref` loses `ref` at a consumer that predates
|
|
475
|
-
// it either way.
|
|
476
|
-
|
|
477
|
-
const KNOWN_LINE_KINDS = new Set(['para', 'heading', 'code', 'island', 'rule']);
|
|
478
|
-
const KNOWN_CONTAINERS = new Set(['list_item', 'quote']);
|
|
479
|
-
const KNOWN_MARK_TYPES = new Set(['strong', 'emph', 'underline', 'strike', 'code', 'link', 'anchor']);
|
|
480
|
-
const KNOWN_ISLAND_TYPES = new Set(['table', 'image']);
|
|
481
|
-
|
|
482
|
-
/**
|
|
483
|
-
* @param {import('../core/wasm.js').ContentLine} line
|
|
484
|
-
* @returns {line is import('../core/wasm.js').ContentLine & { kind: string; attrs: unknown }}
|
|
485
|
-
*/
|
|
486
|
-
export function isUnknownLine(line) {
|
|
487
|
-
return typeof line?.kind === 'string' && !KNOWN_LINE_KINDS.has(line.kind);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* @param {import('../core/wasm.js').ContentContainer} container
|
|
492
|
-
* @returns {container is import('../core/wasm.js').ContentContainer & { container: string; attrs: unknown }}
|
|
493
|
-
*/
|
|
494
|
-
export function isUnknownContainer(container) {
|
|
495
|
-
return typeof container?.container === 'string' && !KNOWN_CONTAINERS.has(container.container);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* @param {import('../core/wasm.js').ContentMark} mark
|
|
500
|
-
* @returns {mark is import('../core/wasm.js').ContentMark & { type: string; attrs: unknown }}
|
|
501
|
-
*/
|
|
502
|
-
export function isUnknownMark(mark) {
|
|
503
|
-
return typeof mark?.type === 'string' && !KNOWN_MARK_TYPES.has(mark.type);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
/**
|
|
507
|
-
* @param {import('../core/wasm.js').ContentIsland} island
|
|
508
|
-
* @returns {island is import('../core/wasm.js').ContentIsland & { type: string; props: unknown }}
|
|
509
|
-
*/
|
|
510
|
-
export function isUnknownIsland(island) {
|
|
511
|
-
return typeof island?.type === 'string' && !KNOWN_ISLAND_TYPES.has(island.type);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// ── Container run boundaries ────────────────────────────────────────────────
|
|
515
|
-
// `ContentContainer.instance` is what keeps two adjacent runs of one shape
|
|
516
|
-
// apart, and only a writer knows where a boundary is: the flat `containers`
|
|
517
|
-
// form cannot tell a list ending beside another from one list of two items, so
|
|
518
|
-
// an omitted discriminator welds them and nothing reports it.
|
|
519
|
-
//
|
|
520
218
|
// WELD_KEYS is the rule `Container::same_weld` owns upstream: which `attrs`
|
|
521
219
|
// entries two adjacent runs must share for the markdown projection to read them
|
|
522
|
-
// as one
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
// `tests/known_names_drift.rs` can pin it against the Rust predicate.
|
|
220
|
+
// as one. `start` is not among them, since CommonMark reads only a list's first
|
|
221
|
+
// number — a subset, which is why every container needs its own entry. A table
|
|
222
|
+
// rather than a switch, so `tests/known_names_drift.rs` can pin it against the
|
|
223
|
+
// Rust predicate.
|
|
527
224
|
|
|
528
225
|
const WELD_KEYS = { list_item: ['ordered'], quote: [] };
|
|
529
226
|
|
|
530
|
-
function sameJson(a, b) {
|
|
531
|
-
if (a === b) return true;
|
|
532
|
-
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;
|
|
533
|
-
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
534
|
-
const ka = Object.keys(a);
|
|
535
|
-
return (
|
|
536
|
-
ka.length === Object.keys(b).length &&
|
|
537
|
-
ka.every((k) => Object.hasOwn(b, k) && sameJson(a[k], b[k]))
|
|
538
|
-
);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
227
|
/**
|
|
542
228
|
* @param {import('../core/wasm.js').ContentContainer} a
|
|
543
229
|
* @param {import('../core/wasm.js').ContentContainer} b
|
|
544
230
|
* @returns {boolean}
|
|
545
231
|
*/
|
|
546
232
|
function weldsWith(a, b) {
|
|
547
|
-
// A malformed value welds with nothing
|
|
548
|
-
// answer rather than throw.
|
|
233
|
+
// A malformed value welds with nothing: answer rather than throw.
|
|
549
234
|
if (typeof a?.container !== 'string' || a.container !== b?.container) return false;
|
|
550
|
-
// `hasOwn`, so a tag colliding with an `Object.prototype` member
|
|
551
|
-
//
|
|
552
|
-
if (!Object.hasOwn(WELD_KEYS, a.container)) return
|
|
235
|
+
// `hasOwn`, so a tag colliding with an `Object.prototype` member answers
|
|
236
|
+
// `false` rather than reaching a function.
|
|
237
|
+
if (!Object.hasOwn(WELD_KEYS, a.container)) return false;
|
|
553
238
|
return WELD_KEYS[a.container].every((k) => a.attrs?.[k] === b.attrs?.[k]);
|
|
554
239
|
}
|
|
555
240
|
|
|
@@ -579,7 +264,7 @@ export function assignInstances(runs) {
|
|
|
579
264
|
* generated entry's own `wasm !== undefined` guard only catches a call arriving
|
|
580
265
|
* after one finished, not one already in flight.
|
|
581
266
|
*
|
|
582
|
-
* @param {string} id
|
|
267
|
+
* @param {string} id the build's name, for the failure message
|
|
583
268
|
* @param {() => Promise<any>} importThunk the dynamic `import()`
|
|
584
269
|
* @param {() => URL} wasmUrl the build's binary, resolved at call time
|
|
585
270
|
* @returns {() => Promise<any>} resolves to a ready-to-use module
|
|
@@ -598,108 +283,89 @@ function backendLoad(id, importThunk, wasmUrl) {
|
|
|
598
283
|
throw Object.assign(
|
|
599
284
|
quillmarkError(
|
|
600
285
|
'runtime::backend_load_failed',
|
|
601
|
-
`Engine: could not load the '${id}'
|
|
286
|
+
`Engine: could not load the '${id}' build: ${
|
|
602
287
|
/** @type {any} */ (cause)?.message ?? cause
|
|
603
288
|
}`,
|
|
604
|
-
'The
|
|
289
|
+
'The build ships beside the package files; check the network tab for a 404 or an HTML response.'
|
|
605
290
|
),
|
|
606
291
|
{ cause }
|
|
607
292
|
);
|
|
608
293
|
}));
|
|
609
294
|
}
|
|
610
295
|
|
|
611
|
-
//
|
|
612
|
-
// binary into the eager graph and defeat lazy loading.
|
|
613
|
-
//
|
|
614
|
-
//
|
|
615
|
-
// `
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
296
|
+
// The render build is NEVER statically imported here: that would pull a
|
|
297
|
+
// multi-MB binary into the eager graph and defeat lazy loading. Both built-in
|
|
298
|
+
// backends ship in it, so they share one `load`; each `formats` manifest
|
|
299
|
+
// mirrors that backend's Rust `SUPPORTED_FORMATS`, pinned by a
|
|
300
|
+
// `runtime.test.js` drift guard that renders once and compares.
|
|
301
|
+
const RENDER_BUILD = backendLoad(
|
|
302
|
+
'render',
|
|
303
|
+
() => import('../render/wasm.js'),
|
|
304
|
+
() => new URL('../render/wasm_bg.wasm', import.meta.url)
|
|
305
|
+
);
|
|
620
306
|
const DEFAULT_BACKENDS = {
|
|
621
307
|
typst: {
|
|
622
|
-
load:
|
|
623
|
-
|
|
624
|
-
() => import('../backends/typst/wasm.js'),
|
|
625
|
-
() => new URL('../backends/typst/wasm_bg.wasm', import.meta.url)
|
|
626
|
-
),
|
|
627
|
-
formats: ['pdf', 'svg', 'png'], // crates/backends/typst/src/lib.rs SUPPORTED_FORMATS
|
|
628
|
-
canvas: true // has svg/png → formats_support_canvas == true
|
|
308
|
+
load: RENDER_BUILD,
|
|
309
|
+
formats: ['pdf', 'svg', 'png'] // crates/backends/typst/src/lib.rs SUPPORTED_FORMATS
|
|
629
310
|
},
|
|
630
|
-
|
|
631
|
-
load:
|
|
632
|
-
|
|
633
|
-
() => import('../backends/pdfform/wasm.js'),
|
|
634
|
-
() => new URL('../backends/pdfform/wasm_bg.wasm', import.meta.url)
|
|
635
|
-
),
|
|
636
|
-
// crates/backends/pdfform/src/lib.rs SUPPORTED_FORMATS == [Pdf, Svg, Png]
|
|
637
|
-
formats: ['pdf', 'svg', 'png'],
|
|
638
|
-
canvas: true // has svg/png → formats_support_canvas == true
|
|
311
|
+
acroform: {
|
|
312
|
+
load: RENDER_BUILD,
|
|
313
|
+
formats: ['pdf'] // crates/backends/acroform/src/lib.rs SUPPORTED_FORMATS
|
|
639
314
|
}
|
|
640
315
|
};
|
|
641
316
|
|
|
642
317
|
/**
|
|
643
318
|
* Validate a backend registry descriptor, naming the backend id on any
|
|
644
319
|
* malformed entry. Failing at construction rather than deep inside a render is
|
|
645
|
-
* what lets
|
|
320
|
+
* what lets `supportedFormats` answer from the manifest unconditionally.
|
|
646
321
|
* @param {string} id
|
|
647
322
|
* @param {unknown} entry
|
|
648
|
-
* @returns {{ load: () => Promise<unknown>, formats: string[]
|
|
323
|
+
* @returns {{ load: () => Promise<unknown>, formats: string[] }}
|
|
649
324
|
*/
|
|
650
325
|
function validateBackend(id, entry) {
|
|
651
326
|
if (!entry || typeof entry !== 'object') {
|
|
652
|
-
throw new Error(
|
|
653
|
-
`Engine: backend '${id}' must be a descriptor { load, formats, canvas }.`
|
|
654
|
-
);
|
|
327
|
+
throw new Error(`Engine: backend '${id}' must be a descriptor { load, formats }.`);
|
|
655
328
|
}
|
|
656
|
-
const { load, formats
|
|
329
|
+
const { load, formats } = /** @type {any} */ (entry);
|
|
657
330
|
if (typeof load !== 'function') {
|
|
658
331
|
throw new Error(`Engine: backend '${id}' descriptor needs a callable 'load'.`);
|
|
659
332
|
}
|
|
660
333
|
if (!Array.isArray(formats)) {
|
|
661
334
|
throw new Error(`Engine: backend '${id}' descriptor needs a 'formats' array.`);
|
|
662
335
|
}
|
|
663
|
-
|
|
664
|
-
throw new Error(`Engine: backend '${id}' descriptor needs a boolean 'canvas'.`);
|
|
665
|
-
}
|
|
666
|
-
return { load, formats, canvas };
|
|
336
|
+
return { load, formats };
|
|
667
337
|
}
|
|
668
338
|
|
|
669
|
-
/**
|
|
670
|
-
* Render dispatcher over the canonical `Quill`/`Document`. One `Engine`
|
|
671
|
-
* instance can drive every backend; it resolves the right backend build from
|
|
672
|
-
* each quill's declared `backendId` and loads it lazily on first use.
|
|
673
|
-
*/
|
|
674
339
|
export class Engine {
|
|
675
|
-
/**
|
|
340
|
+
/**
|
|
341
|
+
* The three caches below key on a descriptor's `load` thunk, the build's
|
|
342
|
+
* identity: two backend ids sharing one build (the built-ins) share one
|
|
343
|
+
* module, one engine and one clone cache, and a consumer's override build
|
|
344
|
+
* gets its own.
|
|
345
|
+
*/
|
|
346
|
+
/** load → Promise<build module>, memoized so each build loads once. */
|
|
676
347
|
#modules = new Map();
|
|
677
|
-
/**
|
|
348
|
+
/** load → that build's engine instance (the WASM backend registry). */
|
|
678
349
|
#engines = new Map();
|
|
679
|
-
/** backendId → descriptor `{ load, formats
|
|
350
|
+
/** backendId → descriptor `{ load, formats }`. */
|
|
680
351
|
#loaders;
|
|
681
352
|
/**
|
|
682
|
-
*
|
|
353
|
+
* load → WeakMap<canonical Quill, build-memory clone>, caching the
|
|
683
354
|
* expensive materialization. WeakMap so dropping the canonical quill makes
|
|
684
355
|
* its clone collectable, and wasm-bindgen weak-refs then free the handle.
|
|
685
|
-
* @type {Map<
|
|
356
|
+
* @type {Map<() => Promise<unknown>, WeakMap<object, any>>}
|
|
686
357
|
*/
|
|
687
358
|
#quillClones = new Map();
|
|
688
359
|
|
|
689
360
|
/**
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
* throw here, at construction.
|
|
695
|
-
*
|
|
696
|
-
* `load` resolves to a READY module: a registrant shipping its own
|
|
697
|
-
* `--target web` build instantiates inside the thunk. More than one
|
|
698
|
-
* `Engine` may call it, so memoize (the built-ins do, at module scope).
|
|
361
|
+
* `load` must resolve to a READY module: a registrant shipping its own
|
|
362
|
+
* `--target web` build instantiates inside the thunk, and more than one
|
|
363
|
+
* `Engine` may call it, so memoize (the built-ins do, at module scope).
|
|
364
|
+
* @param {{ backends?: Record<string, { load: () => Promise<unknown>, formats: string[] }> }} [options]
|
|
699
365
|
*/
|
|
700
366
|
constructor(options) {
|
|
701
367
|
const merged = { ...DEFAULT_BACKENDS, ...(options?.backends ?? {}) };
|
|
702
|
-
/** @type {Record<string, { load: () => Promise<unknown>, formats: string[]
|
|
368
|
+
/** @type {Record<string, { load: () => Promise<unknown>, formats: string[] }>} */
|
|
703
369
|
const loaders = {};
|
|
704
370
|
for (const [id, entry] of Object.entries(merged)) {
|
|
705
371
|
loaders[id] = validateBackend(id, entry);
|
|
@@ -708,17 +374,19 @@ export class Engine {
|
|
|
708
374
|
}
|
|
709
375
|
|
|
710
376
|
/**
|
|
711
|
-
* The registered descriptor for `backendId`, or the
|
|
712
|
-
* throw
|
|
377
|
+
* The registered descriptor for `backendId`, or the
|
|
378
|
+
* `engine::backend_not_found` throw core raises for the same condition.
|
|
379
|
+
* Touches no binary.
|
|
713
380
|
* @param {string} backendId
|
|
714
|
-
* @returns {{ load: () => Promise<unknown>, formats: string[]
|
|
381
|
+
* @returns {{ load: () => Promise<unknown>, formats: string[] }}
|
|
715
382
|
*/
|
|
716
383
|
#descriptorFor(backendId) {
|
|
717
384
|
const descriptor = this.#loaders[backendId];
|
|
718
385
|
if (!descriptor) {
|
|
719
|
-
throw
|
|
720
|
-
|
|
721
|
-
|
|
386
|
+
throw quillmarkError(
|
|
387
|
+
'engine::backend_not_found',
|
|
388
|
+
`Engine: backend '${backendId}' not registered or not enabled.`,
|
|
389
|
+
`Available backends: ${Object.keys(this.#loaders).join(', ') || '(none)'}`
|
|
722
390
|
);
|
|
723
391
|
}
|
|
724
392
|
return descriptor;
|
|
@@ -738,51 +406,52 @@ export class Engine {
|
|
|
738
406
|
}
|
|
739
407
|
|
|
740
408
|
/**
|
|
741
|
-
* Resolve (and lazily load) the
|
|
409
|
+
* Resolve (and lazily load) the build module + its engine for `backendId`.
|
|
742
410
|
* @param {string} backendId
|
|
743
411
|
* @returns {Promise<{ mod: any, engine: any }>}
|
|
744
412
|
*/
|
|
745
413
|
async #resolveBackend(backendId) {
|
|
746
|
-
const
|
|
414
|
+
const { load } = this.#descriptorFor(backendId);
|
|
747
415
|
|
|
748
|
-
let modPromise = this.#modules.get(
|
|
416
|
+
let modPromise = this.#modules.get(load);
|
|
749
417
|
if (!modPromise) {
|
|
750
418
|
// Set the promise synchronously (before any await) so concurrent first
|
|
751
419
|
// renders share ONE import. Self-heal on failure so a transient load
|
|
752
420
|
// error doesn't poison every later attempt.
|
|
753
421
|
modPromise = Promise.resolve()
|
|
754
|
-
.then(
|
|
422
|
+
.then(load)
|
|
755
423
|
.catch((err) => {
|
|
756
|
-
this.#modules.delete(
|
|
424
|
+
this.#modules.delete(load);
|
|
757
425
|
throw err;
|
|
758
426
|
});
|
|
759
|
-
this.#modules.set(
|
|
427
|
+
this.#modules.set(load, modPromise);
|
|
760
428
|
}
|
|
761
429
|
const mod = await modPromise;
|
|
762
430
|
|
|
763
|
-
let engine = this.#engines.get(
|
|
431
|
+
let engine = this.#engines.get(load);
|
|
764
432
|
if (!engine) {
|
|
765
433
|
engine = new mod.Quillmark();
|
|
766
|
-
this.#engines.set(
|
|
434
|
+
this.#engines.set(load, engine);
|
|
767
435
|
}
|
|
768
436
|
return { mod, engine };
|
|
769
437
|
}
|
|
770
438
|
|
|
771
439
|
/**
|
|
772
|
-
* Get (or materialize-and-cache) the
|
|
773
|
-
* under `backendId
|
|
774
|
-
* pre-await snapshot, since the canonical handle may be freed by now.
|
|
775
|
-
* @param {any} mod the
|
|
440
|
+
* Get (or materialize-and-cache) the build-memory `Quill` clone for `quill`
|
|
441
|
+
* under `backendId`'s build. On a miss the clone is built from `tree`, the
|
|
442
|
+
* caller's pre-await snapshot, since the canonical handle may be freed by now.
|
|
443
|
+
* @param {any} mod the build module
|
|
776
444
|
* @param {string} backendId
|
|
777
445
|
* @param {object} quill the canonical instance (cache key only)
|
|
778
446
|
* @param {Map<string, Uint8Array> | null} tree pre-await snapshot; `null` on a cache hit
|
|
779
|
-
* @returns {any} the
|
|
447
|
+
* @returns {any} the build-memory quill clone
|
|
780
448
|
*/
|
|
781
449
|
#cachedQuillClone(mod, backendId, quill, tree) {
|
|
782
|
-
|
|
450
|
+
const { load } = this.#descriptorFor(backendId);
|
|
451
|
+
let perQuill = this.#quillClones.get(load);
|
|
783
452
|
if (!perQuill) {
|
|
784
453
|
perQuill = new WeakMap();
|
|
785
|
-
this.#quillClones.set(
|
|
454
|
+
this.#quillClones.set(load, perQuill);
|
|
786
455
|
}
|
|
787
456
|
let backendQuill = perQuill.get(quill);
|
|
788
457
|
if (!backendQuill) {
|
|
@@ -797,11 +466,15 @@ export class Engine {
|
|
|
797
466
|
* memory and run `fn` against the backend engine. Only `render`/`open` call
|
|
798
467
|
* this, so `doc` is always present.
|
|
799
468
|
*
|
|
800
|
-
* OWNERSHIP WINDOW: both caller handles are snapshotted (`doc.
|
|
801
|
-
* `quill.toTree()` on a clone-cache miss) BEFORE the first
|
|
802
|
-
* load below is a real suspension point, so reading the
|
|
803
|
-
* would race a caller that `free()`s them as soon as this
|
|
804
|
-
* promise ("null pointer passed to rust").
|
|
469
|
+
* OWNERSHIP WINDOW: both caller handles are snapshotted (`doc.toStored()` and
|
|
470
|
+
* `doc.warnings`, and `quill.toTree()` on a clone-cache miss) BEFORE the first
|
|
471
|
+
* await. The backend load below is a real suspension point, so reading the
|
|
472
|
+
* handles after it would race a caller that `free()`s them as soon as this
|
|
473
|
+
* call returns its promise ("null pointer passed to rust").
|
|
474
|
+
*
|
|
475
|
+
* `docWarnings` rides the context because the storage DTO does not carry
|
|
476
|
+
* them: `fromStored` clears the load's warnings, so the backend clone knows
|
|
477
|
+
* nothing of them and `render` splices the snapshot back in.
|
|
805
478
|
*
|
|
806
479
|
* Clone lifetimes differ by design: the `doc` clone is TRANSIENT, freed in
|
|
807
480
|
* the `finally` of every call, while the `quill` clone is CACHED and is not
|
|
@@ -811,13 +484,16 @@ export class Engine {
|
|
|
811
484
|
* @param {string} method the caller's name, for the rejection message
|
|
812
485
|
* @param {Quill} quill
|
|
813
486
|
* @param {Document} doc
|
|
814
|
-
* @param {(ctx: { mod: any, engine: any, quill: any, doc: any }) => any} fn
|
|
487
|
+
* @param {(ctx: { mod: any, engine: any, quill: any, doc: any, docWarnings: any[] }) => any} fn
|
|
815
488
|
*/
|
|
816
489
|
async #withClones(method, quill, doc, fn) {
|
|
817
490
|
const backendId = this.#backendOf(quill, method);
|
|
818
491
|
requireLocalDoc(doc, method);
|
|
819
|
-
const docJson = doc.
|
|
820
|
-
const
|
|
492
|
+
const docJson = doc.toStored();
|
|
493
|
+
const docWarnings = doc.warnings;
|
|
494
|
+
const quillTree = this.#quillClones.get(this.#descriptorFor(backendId).load)?.has(quill)
|
|
495
|
+
? null
|
|
496
|
+
: quill.toTree();
|
|
821
497
|
const { mod, engine } = await this.#resolveBackend(backendId);
|
|
822
498
|
// The doc clone and `fn` share one try so the clone is freed even if a
|
|
823
499
|
// later step throws; the cached quill clone is intentionally not freed.
|
|
@@ -825,31 +501,33 @@ export class Engine {
|
|
|
825
501
|
const backendQuill = this.#cachedQuillClone(mod, backendId, quill, quillTree);
|
|
826
502
|
let backendDoc = null;
|
|
827
503
|
try {
|
|
828
|
-
backendDoc = mod.Document.
|
|
829
|
-
return fn({ mod, engine, quill: backendQuill, doc: backendDoc });
|
|
504
|
+
backendDoc = mod.Document.fromStored(docJson);
|
|
505
|
+
return fn({ mod, engine, quill: backendQuill, doc: backendDoc, docWarnings });
|
|
830
506
|
} finally {
|
|
831
507
|
backendDoc?.free();
|
|
832
508
|
}
|
|
833
509
|
}
|
|
834
510
|
|
|
835
511
|
/**
|
|
836
|
-
* Render `doc` against `quill` in one shot. Both handles are read
|
|
837
|
-
* synchronously before the first await.
|
|
838
512
|
* @param {Quill} quill
|
|
839
513
|
* @param {Document} doc
|
|
840
|
-
* @param {object} [options] render options (`{ format, ppi, pages,
|
|
514
|
+
* @param {object} [options] render options (`{ format, ppi, pages, regions }`)
|
|
841
515
|
* @returns {Promise<import('./runtime.js').RenderResult>}
|
|
842
516
|
*/
|
|
843
517
|
async render(quill, doc, options) {
|
|
844
|
-
return this.#withClones(
|
|
845
|
-
engine.render(
|
|
518
|
+
return this.#withClones(
|
|
519
|
+
'engine.render(quill, doc)',
|
|
520
|
+
quill,
|
|
521
|
+
doc,
|
|
522
|
+
({ engine, quill: q, doc: d, docWarnings }) => {
|
|
523
|
+
const result = engine.render(q, d, options ?? undefined);
|
|
524
|
+
result.warnings = docWarnings.concat(result.warnings);
|
|
525
|
+
return result;
|
|
526
|
+
}
|
|
846
527
|
);
|
|
847
528
|
}
|
|
848
529
|
|
|
849
530
|
/**
|
|
850
|
-
* Open a live render session. It retains what `update` needs, so the
|
|
851
|
-
* transient clones are freed before this returns; the caller owns the session
|
|
852
|
-
* and must `.free()` it.
|
|
853
531
|
* @param {Quill} quill
|
|
854
532
|
* @param {Document} doc
|
|
855
533
|
* @returns {Promise<LiveSession>}
|
|
@@ -864,8 +542,6 @@ export class Engine {
|
|
|
864
542
|
}
|
|
865
543
|
|
|
866
544
|
/**
|
|
867
|
-
* The output formats `quill`'s backend can emit: an always-free probe over
|
|
868
|
-
* the descriptor's manifest. `async` for API stability; it awaits nothing.
|
|
869
545
|
* @param {Quill} quill
|
|
870
546
|
* @returns {Promise<import('./runtime.js').OutputFormat[]>}
|
|
871
547
|
*/
|
|
@@ -874,29 +550,17 @@ export class Engine {
|
|
|
874
550
|
// Defensive copy so callers can't mutate the shared manifest.
|
|
875
551
|
return descriptor.formats.slice();
|
|
876
552
|
}
|
|
877
|
-
|
|
878
|
-
/**
|
|
879
|
-
* Whether `quill`'s backend can paint to a canvas: a pre-session estimate over
|
|
880
|
-
* the descriptor's manifest, so it can answer `true` where the resulting
|
|
881
|
-
* `LiveSession.supportsCanvas` answers `false`.
|
|
882
|
-
* @param {Quill} quill
|
|
883
|
-
* @returns {Promise<boolean>}
|
|
884
|
-
*/
|
|
885
|
-
async supportsCanvas(quill) {
|
|
886
|
-
const descriptor = this.#descriptorFor(this.#backendOf(quill, 'engine.supportsCanvas(quill)'));
|
|
887
|
-
return descriptor.canvas;
|
|
888
|
-
}
|
|
889
553
|
}
|
|
890
554
|
|
|
891
555
|
/**
|
|
892
|
-
* Thin wrapper over a backend's live render session
|
|
893
|
-
*
|
|
894
|
-
*
|
|
556
|
+
* Thin wrapper over a backend's live render session. The quill/document clones
|
|
557
|
+
* it was opened from have already been freed: the session retains what `update`
|
|
558
|
+
* needs.
|
|
895
559
|
*/
|
|
896
560
|
export class LiveSession {
|
|
897
561
|
/**
|
|
898
|
-
* @param {
|
|
899
|
-
* @param {{ Document: {
|
|
562
|
+
* @param {object} inner the render build's LiveSession, whose members the delegations below name
|
|
563
|
+
* @param {{ Document: { fromStored(json: string): any } }} mod the session's backend build, used to materialize `update` documents in its linear memory
|
|
900
564
|
*/
|
|
901
565
|
constructor(inner, mod) {
|
|
902
566
|
this.#inner = inner;
|
|
@@ -913,7 +577,7 @@ export class LiveSession {
|
|
|
913
577
|
requireLocalDoc(doc, 'session.update(doc)');
|
|
914
578
|
let backendDoc = null;
|
|
915
579
|
try {
|
|
916
|
-
backendDoc = this.#mod.Document.
|
|
580
|
+
backendDoc = this.#mod.Document.fromStored(doc.toStored());
|
|
917
581
|
return this.#inner.update(backendDoc);
|
|
918
582
|
} finally {
|
|
919
583
|
backendDoc?.free();
|
|
@@ -926,15 +590,6 @@ export class LiveSession {
|
|
|
926
590
|
get backendId() {
|
|
927
591
|
return this.#inner.backendId;
|
|
928
592
|
}
|
|
929
|
-
/**
|
|
930
|
-
* `true` iff `paint`/`pageSize` will succeed for THIS compile: the
|
|
931
|
-
* authoritative answer, which can be `false` where `Engine.supportsCanvas`
|
|
932
|
-
* answered `true` for the same quill.
|
|
933
|
-
* @returns {boolean}
|
|
934
|
-
*/
|
|
935
|
-
get supportsCanvas() {
|
|
936
|
-
return this.#inner.supportsCanvas;
|
|
937
|
-
}
|
|
938
593
|
get warnings() {
|
|
939
594
|
return this.#inner.warnings;
|
|
940
595
|
}
|
|
@@ -1009,28 +664,11 @@ export class LiveSession {
|
|
|
1009
664
|
}
|
|
1010
665
|
}
|
|
1011
666
|
|
|
1012
|
-
//
|
|
1013
|
-
//
|
|
1014
|
-
//
|
|
1015
|
-
//
|
|
1016
|
-
// schema, so each `commit*` method takes the `quill` handle as its first
|
|
1017
|
-
// argument. These pure-JS classes restore the Rust ergonomics: bind `quill` +
|
|
1018
|
-
// `doc` once, then issue `set` / `setAll` / `card(i).set`.
|
|
1019
|
-
//
|
|
1020
|
-
// They hold JS references to the caller's EXISTING handles (no WASM object of
|
|
1021
|
-
// their own, no `free()` burden, no second owner of either handle) and every
|
|
1022
|
-
// write delegates straight to the underlying `commit*` verb: a schema field is
|
|
1023
|
-
// typed-committed (coerced to canonical form, mismatch throws now), and a name
|
|
1024
|
-
// the schema does not declare throws `UnknownField` rather than falling to the
|
|
1025
|
-
// opaque store, on the typed path an undeclared name is a typo. Opaque storage
|
|
1026
|
-
// stays available through the raw addressed `Document.storeField` verb.
|
|
667
|
+
// These pure-JS classes bind a `quill` + `doc` pair once and delegate to the
|
|
668
|
+
// `_commit*` / `_reader*` verbs, which take the quill per call. They hold JS
|
|
669
|
+
// references to the caller's existing handles, so there is no WASM object of
|
|
670
|
+
// their own, no `free()` burden, and no second owner of either handle.
|
|
1027
671
|
|
|
1028
|
-
/**
|
|
1029
|
-
* A {@link Document} bound to its {@link Quill} for typed writes: the JS twin
|
|
1030
|
-
* of Rust's `quill.writer(&mut doc)`. Writes target the main card; use
|
|
1031
|
-
* {@link card} for a composable card. Holds both handles by reference and owns
|
|
1032
|
-
* neither, so there is nothing to `free()`.
|
|
1033
|
-
*/
|
|
1034
672
|
export class DocumentWriter {
|
|
1035
673
|
#quill;
|
|
1036
674
|
#doc;
|
|
@@ -1044,13 +682,10 @@ export class DocumentWriter {
|
|
|
1044
682
|
this.#quill = quill;
|
|
1045
683
|
this.#doc = doc;
|
|
1046
684
|
}
|
|
1047
|
-
/** The bound document: the same instance passed in, mutated in place. */
|
|
1048
685
|
get document() {
|
|
1049
686
|
return this.#doc;
|
|
1050
687
|
}
|
|
1051
688
|
/**
|
|
1052
|
-
* Typed-commit one main-card field (strict coerce, mismatch throws now).
|
|
1053
|
-
* Throws `UnknownField` for a name the schema does not declare.
|
|
1054
689
|
* @param {string} name
|
|
1055
690
|
* @param {unknown} value
|
|
1056
691
|
* @returns {void}
|
|
@@ -1059,9 +694,6 @@ export class DocumentWriter {
|
|
|
1059
694
|
return this.#doc._commitField(this.#quill, name, value);
|
|
1060
695
|
}
|
|
1061
696
|
/**
|
|
1062
|
-
* Typed-commit several main-card fields atomically: nothing is applied on
|
|
1063
|
-
* error (throws a per-field diagnostic bundle, including an `UnknownField`
|
|
1064
|
-
* for each undeclared name).
|
|
1065
697
|
* @param {Record<string, unknown>} fields
|
|
1066
698
|
* @returns {void}
|
|
1067
699
|
*/
|
|
@@ -1069,8 +701,6 @@ export class DocumentWriter {
|
|
|
1069
701
|
return this.#doc._commitFields(this.#quill, MAIN_CARD_ADDR, fields);
|
|
1070
702
|
}
|
|
1071
703
|
/**
|
|
1072
|
-
* Revise the main body from markdown; anchors rebase. A body carries no field
|
|
1073
|
-
* schema, so this is the content lane's `revise` reached through the writer.
|
|
1074
704
|
* @param {string} markdown
|
|
1075
705
|
* @returns {import('../core/wasm.js').Delta}
|
|
1076
706
|
*/
|
|
@@ -1078,10 +708,6 @@ export class DocumentWriter {
|
|
|
1078
708
|
return this.#doc.revise({}, markdown);
|
|
1079
709
|
}
|
|
1080
710
|
/**
|
|
1081
|
-
* Revise the content main-card field `name` from authored text: typed *and*
|
|
1082
|
-
* anchor-preserving. Anchors rebase, then the diffed result is
|
|
1083
|
-
* schema-conformed. The codec comes from the declared type: `richtext` diffs
|
|
1084
|
-
* markdown, `plaintext` the literal text.
|
|
1085
711
|
* @param {string} name
|
|
1086
712
|
* @param {string} text
|
|
1087
713
|
* @returns {import('../core/wasm.js').Delta}
|
|
@@ -1090,10 +716,6 @@ export class DocumentWriter {
|
|
|
1090
716
|
return this.#doc._reviseField(this.#quill, name, text);
|
|
1091
717
|
}
|
|
1092
718
|
/**
|
|
1093
|
-
* Build a composable card of `kind`, typed-commit `fields` onto it, set its
|
|
1094
|
-
* body from optional markdown, and place it. Transactional: the card is
|
|
1095
|
-
* committed in full before it joins the document, so a rejected field, kind,
|
|
1096
|
-
* body, or position leaves the document untouched.
|
|
1097
719
|
* @param {string} kind
|
|
1098
720
|
* @param {Record<string, unknown>} [fields]
|
|
1099
721
|
* @param {string} [body]
|
|
@@ -1111,9 +733,6 @@ export class DocumentWriter {
|
|
|
1111
733
|
return this.#doc.removeCard(index);
|
|
1112
734
|
}
|
|
1113
735
|
/**
|
|
1114
|
-
* A {@link CardWriter} bound to the composable card at `index`, checked
|
|
1115
|
-
* lazily at the write. It holds `index`, not the card, so a
|
|
1116
|
-
* `removeCard`/`addCard` between binding and writing silently retargets it.
|
|
1117
736
|
* @param {number} index
|
|
1118
737
|
* @returns {CardWriter}
|
|
1119
738
|
*/
|
|
@@ -1122,11 +741,6 @@ export class DocumentWriter {
|
|
|
1122
741
|
}
|
|
1123
742
|
}
|
|
1124
743
|
|
|
1125
|
-
/**
|
|
1126
|
-
* A single composable card bound to its {@link Quill} for typed writes, from
|
|
1127
|
-
* {@link DocumentWriter.card}. Same `set` / `setAll` verbs as
|
|
1128
|
-
* {@link DocumentWriter}, targeting the card at its bound index.
|
|
1129
|
-
*/
|
|
1130
744
|
export class CardWriter {
|
|
1131
745
|
#quill;
|
|
1132
746
|
#doc;
|
|
@@ -1143,22 +757,16 @@ export class CardWriter {
|
|
|
1143
757
|
this.#doc = doc;
|
|
1144
758
|
this.#index = index;
|
|
1145
759
|
}
|
|
1146
|
-
/** The bound card index. */
|
|
1147
760
|
get index() {
|
|
1148
761
|
return this.#index;
|
|
1149
762
|
}
|
|
1150
763
|
/**
|
|
1151
|
-
* The bound card's `$kind`, empty string when it carries none. Throws
|
|
1152
|
-
* `IndexOutOfRange` for a bad bound index.
|
|
1153
764
|
* @returns {string}
|
|
1154
765
|
*/
|
|
1155
766
|
get kind() {
|
|
1156
767
|
return this.#doc.card(this.#index).kind;
|
|
1157
768
|
}
|
|
1158
769
|
/**
|
|
1159
|
-
* Typed-commit one field on this card, addressed at `{ card, field }`. Throws
|
|
1160
|
-
* `UnknownField` for an undeclared name and `IndexOutOfRange` if the bound
|
|
1161
|
-
* index is out of range.
|
|
1162
770
|
* @param {string} name
|
|
1163
771
|
* @param {unknown} value
|
|
1164
772
|
* @returns {void}
|
|
@@ -1167,9 +775,6 @@ export class CardWriter {
|
|
|
1167
775
|
return this.#doc._commitField(this.#quill, { card: this.#index, field: name }, value);
|
|
1168
776
|
}
|
|
1169
777
|
/**
|
|
1170
|
-
* Typed-commit several fields on this card atomically, addressed at
|
|
1171
|
-
* `{ card }`. Throws a per-field diagnostic bundle on error and
|
|
1172
|
-
* `IndexOutOfRange` if the bound index is out of range.
|
|
1173
778
|
* @param {Record<string, unknown>} fields
|
|
1174
779
|
* @returns {void}
|
|
1175
780
|
*/
|
|
@@ -1177,7 +782,6 @@ export class CardWriter {
|
|
|
1177
782
|
return this.#doc._commitFields(this.#quill, { card: this.#index }, fields);
|
|
1178
783
|
}
|
|
1179
784
|
/**
|
|
1180
|
-
* The card twin of {@link DocumentWriter.reviseBody}.
|
|
1181
785
|
* @param {string} markdown
|
|
1182
786
|
* @returns {import('../core/wasm.js').Delta}
|
|
1183
787
|
*/
|
|
@@ -1185,8 +789,6 @@ export class CardWriter {
|
|
|
1185
789
|
return this.#doc.revise({ card: this.#index }, markdown);
|
|
1186
790
|
}
|
|
1187
791
|
/**
|
|
1188
|
-
* The card twin of {@link DocumentWriter.reviseField}. Throws `UnknownField`
|
|
1189
|
-
* for an undeclared name and `IndexOutOfRange` for a bad bound index.
|
|
1190
792
|
* @param {string} name
|
|
1191
793
|
* @param {string} text
|
|
1192
794
|
* @returns {import('../core/wasm.js').Delta}
|
|
@@ -1196,13 +798,10 @@ export class CardWriter {
|
|
|
1196
798
|
}
|
|
1197
799
|
}
|
|
1198
800
|
|
|
1199
|
-
// ── `quill.writer(doc)`, the typed front door ──────────────────────────────
|
|
1200
801
|
// Patched onto the re-exported `Quill` prototype rather than wrapped, so
|
|
1201
802
|
// `Quill === CoreQuill` stays true: this only adds a method constructing the
|
|
1202
803
|
// pure-JS writer, which owns no WASM handle.
|
|
1203
804
|
/**
|
|
1204
|
-
* A {@link DocumentWriter} binding this quill's schema to `doc`. It holds both
|
|
1205
|
-
* handles by reference and owns neither: bind, write, discard.
|
|
1206
805
|
* @this {Quill}
|
|
1207
806
|
* @param {Document} doc the document to mutate, held by reference (not owned)
|
|
1208
807
|
* @returns {DocumentWriter}
|
|
@@ -1211,17 +810,10 @@ Quill.prototype.writer = function writer(doc) {
|
|
|
1211
810
|
return new DocumentWriter(this, doc);
|
|
1212
811
|
};
|
|
1213
812
|
|
|
1214
|
-
// ── Typed-reader sugar: the schema-plane read surface ─────────────────────────
|
|
1215
813
|
// The transport `Document.getStored` is schema-free, so an unknown field name
|
|
1216
814
|
// reads back `undefined` rather than as the typo it is. Binding the quill's
|
|
1217
|
-
// schema lets one `get` interpret by declared type and throw `UnknownField
|
|
1218
|
-
// name the schema does not declare.
|
|
815
|
+
// schema lets one `get` interpret by declared type and throw `UnknownField`.
|
|
1219
816
|
|
|
1220
|
-
/**
|
|
1221
|
-
* A {@link Document} bound to its {@link Quill} for typed reads, the read
|
|
1222
|
-
* counterpart of {@link DocumentWriter}. Reads target the main card; use
|
|
1223
|
-
* {@link card} for a composable one. Owns neither handle.
|
|
1224
|
-
*/
|
|
1225
817
|
export class DocumentReader {
|
|
1226
818
|
#quill;
|
|
1227
819
|
#doc;
|
|
@@ -1235,17 +827,10 @@ export class DocumentReader {
|
|
|
1235
827
|
this.#quill = quill;
|
|
1236
828
|
this.#doc = doc;
|
|
1237
829
|
}
|
|
1238
|
-
/** The bound document: the same instance passed in. */
|
|
1239
830
|
get document() {
|
|
1240
831
|
return this.#doc;
|
|
1241
832
|
}
|
|
1242
833
|
/**
|
|
1243
|
-
* Read the value at `addr`, interpreted by its declared type: a richtext field
|
|
1244
|
-
* to markdown, every other type verbatim. A bare string is `Addr` shorthand
|
|
1245
|
-
* for `{ field }`; an absent `addr.field` reads the body markdown. `undefined`
|
|
1246
|
-
* for an absent field; throws `UnknownField` for a name the schema does not
|
|
1247
|
-
* declare, `FieldDecode` for a richtext field holding an undecodable
|
|
1248
|
-
* value, and `IndexOutOfRange` for a bad `addr.card`.
|
|
1249
834
|
* @param {import('../core/wasm.js').Addr | string} addr
|
|
1250
835
|
* @returns {unknown}
|
|
1251
836
|
*/
|
|
@@ -1253,11 +838,6 @@ export class DocumentReader {
|
|
|
1253
838
|
return this.#doc._readerGet(this.#quill, addr);
|
|
1254
839
|
}
|
|
1255
840
|
/**
|
|
1256
|
-
* Read the content field at `addr` as canonical `Content`: the twin of
|
|
1257
|
-
* {@link get}, which projects. An absent `addr.field` reads the body
|
|
1258
|
-
* `Content`. `undefined` for an absent field; throws `UnknownField`,
|
|
1259
|
-
* `FieldNotContent` for a type that is not a content leaf, `FieldDecode` for
|
|
1260
|
-
* an undecodable value, and `IndexOutOfRange` for a bad `addr.card`.
|
|
1261
841
|
* @param {import('../core/wasm.js').Addr | string} addr
|
|
1262
842
|
* @returns {import('../core/wasm.js').Content | undefined}
|
|
1263
843
|
*/
|
|
@@ -1265,13 +845,6 @@ export class DocumentReader {
|
|
|
1265
845
|
return this.#doc._readerGetContent(this.#quill, addr);
|
|
1266
846
|
}
|
|
1267
847
|
/**
|
|
1268
|
-
* Read the `Content` nested inside the composite field at `addr`, at `path`:
|
|
1269
|
-
* `[0]` an `array<richtext>` element, `["motto"]` an object's content property,
|
|
1270
|
-
* `[1, "notes"]` a leaf under both, `["controlled_by"]` a variant's cell. The
|
|
1271
|
-
* codec is the leaf's declared type's.
|
|
1272
|
-
* `undefined` for an absent field and for a path that names nothing stored;
|
|
1273
|
-
* throws `UnknownField`, `FieldNotContent` when `path` resolves to no content
|
|
1274
|
-
* leaf, `FieldDecode` anchored at the addressed path, and `IndexOutOfRange`.
|
|
1275
848
|
* @param {import('../core/wasm.js').Addr | string} addr
|
|
1276
849
|
* @param {import('../core/wasm.js').PathStep[]} path
|
|
1277
850
|
* @returns {import('../core/wasm.js').Content | undefined}
|
|
@@ -1280,16 +853,18 @@ export class DocumentReader {
|
|
|
1280
853
|
return this.#doc._readerGetContentAt(this.#quill, addr, path);
|
|
1281
854
|
}
|
|
1282
855
|
/**
|
|
1283
|
-
* The main body's markdown: the quill-free body read. Equals `get({})`.
|
|
1284
856
|
* @returns {string}
|
|
1285
857
|
*/
|
|
1286
858
|
bodyMarkdown() {
|
|
1287
859
|
return this.#doc._readerGet(this.#quill, {});
|
|
1288
860
|
}
|
|
1289
861
|
/**
|
|
1290
|
-
*
|
|
1291
|
-
|
|
1292
|
-
|
|
862
|
+
* @returns {Resolved}
|
|
863
|
+
*/
|
|
864
|
+
resolve() {
|
|
865
|
+
return this.#quill._resolve(this.#doc);
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
1293
868
|
* @param {number} index
|
|
1294
869
|
* @returns {CardReader}
|
|
1295
870
|
*/
|
|
@@ -1298,11 +873,6 @@ export class DocumentReader {
|
|
|
1298
873
|
}
|
|
1299
874
|
}
|
|
1300
875
|
|
|
1301
|
-
/**
|
|
1302
|
-
* A single composable card bound to its {@link Quill} for typed reads, from
|
|
1303
|
-
* {@link DocumentReader.card}. Same `get` / `bodyMarkdown` verbs as
|
|
1304
|
-
* {@link DocumentReader}, reading the card at its bound index.
|
|
1305
|
-
*/
|
|
1306
876
|
export class CardReader {
|
|
1307
877
|
#quill;
|
|
1308
878
|
#doc;
|
|
@@ -1319,22 +889,16 @@ export class CardReader {
|
|
|
1319
889
|
this.#doc = doc;
|
|
1320
890
|
this.#index = index;
|
|
1321
891
|
}
|
|
1322
|
-
/** The bound card index. */
|
|
1323
892
|
get index() {
|
|
1324
893
|
return this.#index;
|
|
1325
894
|
}
|
|
1326
895
|
/**
|
|
1327
|
-
* The bound card's `$kind` (empty string when it carries none), read through
|
|
1328
|
-
* the document. Throws `IndexOutOfRange` if the bound index is out of range.
|
|
1329
896
|
* @returns {string}
|
|
1330
897
|
*/
|
|
1331
898
|
get kind() {
|
|
1332
899
|
return this.#doc.card(this.#index).kind;
|
|
1333
900
|
}
|
|
1334
901
|
/**
|
|
1335
|
-
* Read the field `name` on this card, interpreted by its declared type,
|
|
1336
|
-
* addressed at `{ card, field }`. `undefined` when absent; throws
|
|
1337
|
-
* `UnknownField` for an undeclared name and `IndexOutOfRange` for a bad index.
|
|
1338
902
|
* @param {string} name
|
|
1339
903
|
* @returns {unknown}
|
|
1340
904
|
*/
|
|
@@ -1342,7 +906,6 @@ export class CardReader {
|
|
|
1342
906
|
return this.#doc._readerGet(this.#quill, { card: this.#index, field: name });
|
|
1343
907
|
}
|
|
1344
908
|
/**
|
|
1345
|
-
* The card twin of {@link DocumentReader.getContent}.
|
|
1346
909
|
* @param {string} name
|
|
1347
910
|
* @returns {import('../core/wasm.js').Content | undefined}
|
|
1348
911
|
*/
|
|
@@ -1350,7 +913,6 @@ export class CardReader {
|
|
|
1350
913
|
return this.#doc._readerGetContent(this.#quill, { card: this.#index, field: name });
|
|
1351
914
|
}
|
|
1352
915
|
/**
|
|
1353
|
-
* The card twin of {@link DocumentReader.getContentAt}.
|
|
1354
916
|
* @param {string} name
|
|
1355
917
|
* @param {import('../core/wasm.js').PathStep[]} path
|
|
1356
918
|
* @returns {import('../core/wasm.js').Content | undefined}
|
|
@@ -1359,7 +921,6 @@ export class CardReader {
|
|
|
1359
921
|
return this.#doc._readerGetContentAt(this.#quill, { card: this.#index, field: name }, path);
|
|
1360
922
|
}
|
|
1361
923
|
/**
|
|
1362
|
-
* The card twin of {@link DocumentReader.bodyMarkdown}.
|
|
1363
924
|
* @returns {string}
|
|
1364
925
|
*/
|
|
1365
926
|
bodyMarkdown() {
|
|
@@ -1367,11 +928,8 @@ export class CardReader {
|
|
|
1367
928
|
}
|
|
1368
929
|
}
|
|
1369
930
|
|
|
1370
|
-
// ── `quill.reader(doc)`: the schema-plane read front door ─────────────────────
|
|
1371
931
|
// Patched onto the same re-exported `Quill` prototype as `writer`.
|
|
1372
932
|
/**
|
|
1373
|
-
* A {@link DocumentReader} binding this quill's schema to `doc`. It holds both
|
|
1374
|
-
* handles by reference and owns neither: bind, read, discard.
|
|
1375
933
|
* @this {Quill}
|
|
1376
934
|
* @param {Document} doc the document to read, held by reference (not owned)
|
|
1377
935
|
* @returns {DocumentReader}
|