@quillmark/wasm 0.98.0 → 0.100.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 +208 -1
- package/LICENSE +13 -0
- package/README.md +116 -64
- package/backends/pdfform/wasm.d.ts +336 -146
- package/backends/pdfform/wasm_bg.js +223 -122
- package/backends/pdfform/wasm_bg.wasm +0 -0
- package/backends/pdfform/wasm_bg.wasm.d.ts +3 -1
- package/backends/typst/wasm.d.ts +336 -146
- package/backends/typst/wasm_bg.js +223 -122
- package/backends/typst/wasm_bg.wasm +0 -0
- package/backends/typst/wasm_bg.wasm.d.ts +3 -1
- package/core/wasm.d.ts +163 -96
- package/core/wasm_bg.js +206 -102
- package/core/wasm_bg.wasm +0 -0
- package/core/wasm_bg.wasm.d.ts +3 -1
- package/package.json +2 -2
- package/runtime/runtime.d.ts +151 -76
- package/runtime/runtime.js +381 -100
package/runtime/runtime.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/* @ts-self-types="./runtime.d.ts" */
|
|
2
2
|
//
|
|
3
|
-
// @quillmark/wasm/runtime
|
|
3
|
+
// @quillmark/wasm/runtime: the canonical consumer API.
|
|
4
4
|
//
|
|
5
5
|
// Consumers import `Quill`, `Document`, and `Engine` from here and never touch
|
|
6
6
|
// the build-specific subpaths. The package ships multiple WASM binaries with
|
|
7
|
-
// SEPARATE linear memories
|
|
7
|
+
// SEPARATE linear memories: a Typst-less `core` build (small, eager) that is
|
|
8
8
|
// the canonical home of `Quill`/`Document`, and one private backend binary per
|
|
9
9
|
// backend (`backends/typst/` today; more later) that carries an engine. A
|
|
10
10
|
// handle from one memory cannot be used by another. This module hides that seam
|
|
@@ -22,16 +22,16 @@
|
|
|
22
22
|
// renders, and the backend handles never escape.
|
|
23
23
|
//
|
|
24
24
|
// CLONE LIFETIMES (not all transient): the per-call `Document` clone IS
|
|
25
|
-
// transient
|
|
25
|
+
// transient, built and freed inside each call, because documents are small
|
|
26
26
|
// and mutate freely. The `Quill` clone is CACHED instead: re-cloning a quill
|
|
27
27
|
// per call means re-serializing its whole file tree Rust→JS, copying it into
|
|
28
|
-
// backend memory, and re-parsing + re-validating the bundle every time
|
|
28
|
+
// backend memory, and re-parsing + re-validating the bundle every time, and
|
|
29
29
|
// quills are validated, effectively-immutable bundles. So each `Engine`
|
|
30
30
|
// memoizes the backend-memory quill per (engine, backendId, canonical quill
|
|
31
31
|
// instance) in a `WeakMap` keyed on the canonical `Quill`: when the consumer
|
|
32
32
|
// drops the core quill the cache entry becomes collectable and wasm-bindgen
|
|
33
33
|
// weak-refs (`--weak-refs`) free the backend handle. The CONTRACT this buys:
|
|
34
|
-
// a `Quill` instance's contents never change after construction
|
|
34
|
+
// a `Quill` instance's contents never change after construction, mutate by
|
|
35
35
|
// replacing the instance (the clone is dropped with it via WeakMap +
|
|
36
36
|
// weak-refs).
|
|
37
37
|
//
|
|
@@ -39,26 +39,30 @@
|
|
|
39
39
|
// `Quill`/`Document` to `engine.render(...)` and gets a `RenderResult` back.
|
|
40
40
|
|
|
41
41
|
// ── CANONICAL INVARIANT: re-export the core build, never wrap ───────────────
|
|
42
|
-
// The root re-exports the core build's `Quill`/`Document` classes verbatim
|
|
42
|
+
// The root re-exports the core build's `Quill`/`Document` classes verbatim,
|
|
43
43
|
// NOT subclasses or wrappers. There is exactly ONE public entry point (this
|
|
44
44
|
// module), so this identity is a structural fact: `Quill`/`Document` ARE the
|
|
45
45
|
// core classes, and the only boundary that needs crossing is core→backend (a
|
|
46
46
|
// separate WASM memory), which `Engine` does internally as data
|
|
47
47
|
// (`toTree`/`toJson`).
|
|
48
48
|
//
|
|
49
|
-
// Do NOT replace this with a wrapper class
|
|
49
|
+
// Do NOT replace this with a wrapper class: that breaks the identity and turns
|
|
50
50
|
// a structural fact into a converted type (a breaking design change, not a
|
|
51
|
-
// refactor).
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
51
|
+
// refactor). The `runtime.test.js` "re-exports the internal core build classes
|
|
52
|
+
// verbatim" case (`Quill === CoreQuill`) is the executable guard for this
|
|
53
|
+
// invariant.
|
|
54
|
+
//
|
|
55
|
+
// The identity is what makes `instanceof` the whole membership test: a handle
|
|
56
|
+
// either belongs to this copy's classes or it belongs to another copy, and the
|
|
57
|
+
// second is always a consumer bug. `Engine` is NOT duck-typed on its inputs; it
|
|
58
|
+
// checks them. See § "Handles from another copy" below.
|
|
55
59
|
//
|
|
56
60
|
// Imported (not bare re-exported) so `Quill` is a local binding this module can
|
|
57
|
-
// augment
|
|
61
|
+
// augment: `quill.writer(doc)` is patched onto its prototype below. The
|
|
58
62
|
// re-export keeps the identity: the exported `Quill` IS the core class.
|
|
59
63
|
import { Quill, Document, init } from '../core/wasm.js';
|
|
60
64
|
export { Quill, Document, init };
|
|
61
|
-
// The document-free content codec
|
|
65
|
+
// The document-free content codec: re-exported verbatim from the core build so
|
|
62
66
|
// the runtime subpath exposes `exportMarkdown(body)` (the on-demand markdown
|
|
63
67
|
// projection), `importMarkdown`, and the position-mapping pair (`rebase`,
|
|
64
68
|
// `mapPos`).
|
|
@@ -70,10 +74,10 @@ export { parseDocPath, formatDocPath } from '../core/wasm.js';
|
|
|
70
74
|
|
|
71
75
|
// ── The main-card address ───────────────────────────────────────────────────
|
|
72
76
|
/**
|
|
73
|
-
* The main card's address
|
|
77
|
+
* The main card's address: the default target of the card-scoped verbs
|
|
74
78
|
* (`storeFields` / `storeExt` / `commitFields` / …). A named, `CardAddr`-typed
|
|
75
79
|
* alias for the empty address `{}`, so a main-card write names its target:
|
|
76
|
-
* `doc.storeFields(MAIN_CARD_ADDR, fields)`. It IS `{}` (frozen), a pure alias
|
|
80
|
+
* `doc.storeFields(MAIN_CARD_ADDR, fields)`. It IS `{}` (frozen), a pure alias:
|
|
77
81
|
* `{}` and `undefined` stay equally valid. Card axis only: a card selector,
|
|
78
82
|
* never a field address.
|
|
79
83
|
* @type {import('../core/wasm.js').CardAddr}
|
|
@@ -81,15 +85,15 @@ export { parseDocPath, formatDocPath } from '../core/wasm.js';
|
|
|
81
85
|
export const MAIN_CARD_ADDR = Object.freeze({});
|
|
82
86
|
|
|
83
87
|
/**
|
|
84
|
-
* Narrow an unknown caught value to a `QuillmarkError
|
|
88
|
+
* Narrow an unknown caught value to a `QuillmarkError`, the error every
|
|
85
89
|
* fallible method in this package throws: a real `Error` with a non-empty
|
|
86
90
|
* `diagnostics` array attached (same entry shape as `RenderResult.warnings`).
|
|
87
91
|
*
|
|
88
92
|
* Structural by necessity AND by design: the WASM layer constructs a plain
|
|
89
93
|
* `Error` and attaches the property (there is no error class to `instanceof`),
|
|
90
|
-
* and a structural check
|
|
91
|
-
*
|
|
92
|
-
*
|
|
94
|
+
* and a structural check narrows errors from any build or WASM instance in the
|
|
95
|
+
* page. The deliberate exception to § "Handles from another copy": an error is
|
|
96
|
+
* data, not a handle, so nothing is gained by refusing one that crossed.
|
|
93
97
|
*
|
|
94
98
|
* @param {unknown} e
|
|
95
99
|
* @returns {e is Error & { diagnostics: import('../core/wasm.js').Diagnostic[] }}
|
|
@@ -98,14 +102,173 @@ export function isQuillmarkError(e) {
|
|
|
98
102
|
return e instanceof Error && Array.isArray(/** @type {any} */ (e).diagnostics);
|
|
99
103
|
}
|
|
100
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Build a `QuillmarkError` JS-side: a real `Error` carrying `diagnostics`, the
|
|
107
|
+
* shape `isQuillmarkError` narrows and the shape Rust's `WasmError::to_js_value`
|
|
108
|
+
* produces. Errors raised by this hand-written layer belong to the same contract
|
|
109
|
+
* as the ones raised across the WASM boundary.
|
|
110
|
+
* @param {string} code
|
|
111
|
+
* @param {string} message
|
|
112
|
+
* @param {string} hint
|
|
113
|
+
* @returns {Error & { diagnostics: import('../core/wasm.js').Diagnostic[] }}
|
|
114
|
+
*/
|
|
115
|
+
function quillmarkError(code, message, hint) {
|
|
116
|
+
const err = /** @type {any} */ (new Error(message));
|
|
117
|
+
err.diagnostics = [{ severity: 'error', code, message, hint }];
|
|
118
|
+
return err;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Handles from another copy: always a bug ─────────────────────────────────
|
|
122
|
+
// A duplicate install (two copies of this package in one `node_modules` tree)
|
|
123
|
+
// is two `core` builds: two linear memories and two distinct `Quill`/`Document`
|
|
124
|
+
// classes. No topology legitimately loads a multi-megabyte WASM package twice
|
|
125
|
+
// AND needs handles to cross between the copies, so a crossing is a consumer
|
|
126
|
+
// bug. Every seam taking a core handle says so, uniformly, at the crossing.
|
|
127
|
+
//
|
|
128
|
+
// Crossing read-only handles as data is mechanically possible (`toJson` and
|
|
129
|
+
// `toTree` serialize either way) and is not done. It leaves a package where
|
|
130
|
+
// some verbs work and some throw, and it hides a cliff: a crossed read is a
|
|
131
|
+
// whole-document `toJson` + `fromJson`, so a form reading fifty fields pays
|
|
132
|
+
// fifty round trips and the symptom is "the editor got slow". A duplicate
|
|
133
|
+
// install that limps is a duplicate install nobody removes.
|
|
134
|
+
//
|
|
135
|
+
// What the checks deliver is the ERROR, not the rejection. wasm-bindgen's
|
|
136
|
+
// generated glue already rejects a foreign class on every method declaring a
|
|
137
|
+
// reference parameter (`Document.equals`, `Quill.validate`, `Quill.resolve`,
|
|
138
|
+
// the `&Quill`-taking `Document._commitField` and friends): its `_assertClass`
|
|
139
|
+
// is emitted unconditionally and runs before any of our code. It throws a bare
|
|
140
|
+
// `Error` reading `expected instance of Document` at a value that IS a
|
|
141
|
+
// `Document`, so `isQuillmarkError` returns false and the failure leaves this
|
|
142
|
+
// package's error contract, naming neither the cause nor the cure. The checks
|
|
143
|
+
// front-run it with a `QuillmarkError` that names both.
|
|
144
|
+
//
|
|
145
|
+
// They also cover the seams with NO `_assertClass` to front-run: `Engine` and
|
|
146
|
+
// `LiveSession.apply` cross into backend memory as data (`toTree`/`toJson`), so
|
|
147
|
+
// a foreign handle there would silently work, at the price of the round-trip
|
|
148
|
+
// above and a quill clone cache split per copy.
|
|
149
|
+
//
|
|
150
|
+
// Not an API widening in either direction: the declared parameter types stay
|
|
151
|
+
// `Quill`/`Document`, and the accepted set is exactly this copy's instances.
|
|
152
|
+
|
|
153
|
+
/** Per class: the code and hint for "not one at all", and the probe for "from another copy". */
|
|
154
|
+
const HANDLE_KINDS = {
|
|
155
|
+
Quill: {
|
|
156
|
+
code: 'runtime::not_a_quill',
|
|
157
|
+
probe: 'toTree',
|
|
158
|
+
hint: 'Pass a Quill built by Quill.fromTree.'
|
|
159
|
+
},
|
|
160
|
+
Document: {
|
|
161
|
+
code: 'runtime::not_a_document',
|
|
162
|
+
probe: 'toJson',
|
|
163
|
+
hint: 'Pass a Document built by Document.fromMarkdown / fromJson or quill.seedDocument.'
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The rejection for a value that is not one of this copy's handles. Two cures,
|
|
169
|
+
* so two diagnostics: a value carrying the class's serializer is that class
|
|
170
|
+
* from ANOTHER copy (dedupe the install), anything else is the wrong argument
|
|
171
|
+
* (fix the call).
|
|
172
|
+
* @param {unknown} value
|
|
173
|
+
* @param {string} method
|
|
174
|
+
* @param {'Quill' | 'Document'} className
|
|
175
|
+
* @returns {Error & { diagnostics: import('../core/wasm.js').Diagnostic[] }}
|
|
176
|
+
*/
|
|
177
|
+
function notLocal(value, method, className) {
|
|
178
|
+
const { code, probe, hint } = HANDLE_KINDS[className];
|
|
179
|
+
if (value && typeof (/** @type {any} */ (value)[probe]) === 'function') {
|
|
180
|
+
return quillmarkError(
|
|
181
|
+
'runtime::foreign_handle',
|
|
182
|
+
`${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.`,
|
|
183
|
+
'Two copies of @quillmark/wasm are installed. Run `npm ls @quillmark/wasm` and dedupe to one.'
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return quillmarkError(
|
|
187
|
+
code,
|
|
188
|
+
`${method}: expected a ${className}, got ${value === null ? 'null' : typeof value}.`,
|
|
189
|
+
hint
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Throw unless `doc` is THIS copy's `Document`.
|
|
195
|
+
* @param {unknown} doc
|
|
196
|
+
* @param {string} method
|
|
197
|
+
* @returns {void}
|
|
198
|
+
*/
|
|
199
|
+
function requireLocalDoc(doc, method) {
|
|
200
|
+
if (doc instanceof Document) return;
|
|
201
|
+
throw notLocal(doc, method, 'Document');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Throw unless `quill` is THIS copy's `Quill`.
|
|
206
|
+
* @param {unknown} quill
|
|
207
|
+
* @param {string} method
|
|
208
|
+
* @returns {void}
|
|
209
|
+
*/
|
|
210
|
+
function requireLocalQuill(quill, method) {
|
|
211
|
+
if (quill instanceof Quill) return;
|
|
212
|
+
throw notLocal(quill, method, 'Quill');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Marker for the patches below. `Symbol.for`, not a module-local `Symbol()`:
|
|
216
|
+
// re-evaluating THIS module (Vite HMR, a Vitest worker sharing a module graph)
|
|
217
|
+
// against an already-patched (because cached) core build must see the existing
|
|
218
|
+
// marker, or each pass wraps the previous wrapper.
|
|
219
|
+
const HANDLE_CHECKED = Symbol.for('@quillmark/wasm:handle-checked');
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Replace `proto[name]` with `wrap(original)`, once.
|
|
223
|
+
* @param {object} proto
|
|
224
|
+
* @param {string} name
|
|
225
|
+
* @param {(original: Function) => Function} wrap
|
|
226
|
+
*/
|
|
227
|
+
function patchHandleChecked(proto, name, wrap) {
|
|
228
|
+
const original = /** @type {any} */ (proto)[name];
|
|
229
|
+
if (typeof original !== 'function' || original[HANDLE_CHECKED]) return;
|
|
230
|
+
const patched = wrap(original);
|
|
231
|
+
/** @type {any} */ (patched)[HANDLE_CHECKED] = true;
|
|
232
|
+
// Keep the method's name so a stack trace still reads `Quill.validate`.
|
|
233
|
+
Object.defineProperty(patched, 'name', { value: name, configurable: true });
|
|
234
|
+
/** @type {any} */ (proto)[name] = patched;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// The three core methods declaring a `&Document` parameter. Each already refuses
|
|
238
|
+
// a foreign handle inside `_assertClass`; the patch is what makes the refusal
|
|
239
|
+
// legible. Only the argument can be foreign, the receiver being whichever copy
|
|
240
|
+
// the caller reached for.
|
|
241
|
+
patchHandleChecked(Document.prototype, 'equals', (original) =>
|
|
242
|
+
function equals(/** @type {any} */ other) {
|
|
243
|
+
requireLocalDoc(other, 'Document.equals');
|
|
244
|
+
return original.call(this, other);
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
for (const name of /** @type {const} */ (['validate', 'resolve', 'conform'])) {
|
|
248
|
+
// Named once per patch, not per call: `validate` runs per keystroke.
|
|
249
|
+
const method = `Quill.${name}`;
|
|
250
|
+
patchHandleChecked(Quill.prototype, name, (original) =>
|
|
251
|
+
function (/** @type {any} */ doc) {
|
|
252
|
+
requireLocalDoc(doc, method);
|
|
253
|
+
return original.call(this, doc);
|
|
254
|
+
}
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// The typed writer/reader primitives (`Document._commitField` and friends) take
|
|
259
|
+
// the QUILL by reference, so they hit the same `_assertClass` from the other
|
|
260
|
+
// direction. Checked at the four writer/reader classes below, not patched onto
|
|
261
|
+
// `Document`: a foreign document carries its OWN prototype, so patching this
|
|
262
|
+
// copy's would never run.
|
|
263
|
+
|
|
101
264
|
// ── Open-set discriminant guards ────────────────────────────────────────────
|
|
102
265
|
// `ContentIsland.type`, `ContentMark.type`, `ContentLine.kind`, and
|
|
103
266
|
// `ContentContainer.container` are OPEN sets: each union carries a residual
|
|
104
267
|
// `{ …: string; … }` arm, so a bare `x.type === 'table'` check never narrows the
|
|
105
|
-
// payload
|
|
268
|
+
// payload, TS keeps the residual arm live (a `string` can be `'table'`),
|
|
106
269
|
// leaving `props` / the mark payload / `level` opaque at every consumer. These
|
|
107
270
|
// are the checked narrowing path: on the true branch the payload's pinned shape
|
|
108
|
-
// is asserted. Only the payload-carrying arms get a guard
|
|
271
|
+
// is asserted. Only the payload-carrying arms get a guard: an island always
|
|
109
272
|
// carries `props`, a `link` mark carries `url`, an `anchor` mark carries `id`, a
|
|
110
273
|
// `heading` line carries `level` and a `code` line `lang`, a `list_item`
|
|
111
274
|
// container its shape; the payload-free arms (`strong`/`emph`/`underline`/
|
|
@@ -169,7 +332,66 @@ export function isListItemContainer(container) {
|
|
|
169
332
|
return container.container === 'list_item';
|
|
170
333
|
}
|
|
171
334
|
|
|
172
|
-
//
|
|
335
|
+
// ── Open-set membership guards ──────────────────────────────────────────────
|
|
336
|
+
// The guards above each answer "is this arm X": one pinned arm at a time. These
|
|
337
|
+
// four answer the other question: "is this a value this build knows?" A consumer
|
|
338
|
+
// that must branch known-vs-unknown, any read-modify-write consumer, since
|
|
339
|
+
// lowering an edit restates every line's kind and containers, otherwise
|
|
340
|
+
// enumerates the built-in names in its own source, recreating the closed-set
|
|
341
|
+
// coupling the open set exists to remove. That list is correct until the release
|
|
342
|
+
// that adds a built-in, at which point the new construct is misclassified as
|
|
343
|
+
// unknown and round-trips through the consumer's unknown carrier, losing any
|
|
344
|
+
// sibling-key payload.
|
|
345
|
+
//
|
|
346
|
+
// A predicate rather than an exported name list, because the known tables below
|
|
347
|
+
// are upstream's business. They are pinned against the Rust source
|
|
348
|
+
// (`Content::RESERVED_*` and `KnownIslandType`) by the
|
|
349
|
+
// `known_open_set_names_are_pinned` drift-guard test in
|
|
350
|
+
// `crates/content/src/model.rs`: adding a built-in means editing there, here, and
|
|
351
|
+
// the TS unions in `crates/bindings/wasm/src/engine.rs` in one commit.
|
|
352
|
+
//
|
|
353
|
+
// These classify unknown *tags*, not unknown *payloads on known tags*. A future
|
|
354
|
+
// `kind: "footnote"` with a sibling `ref` loses `ref` at a consumer that predates
|
|
355
|
+
// it either way.
|
|
356
|
+
|
|
357
|
+
const KNOWN_LINE_KINDS = new Set(['para', 'heading', 'code', 'island', 'rule']);
|
|
358
|
+
const KNOWN_CONTAINERS = new Set(['list_item', 'quote']);
|
|
359
|
+
const KNOWN_MARK_TYPES = new Set(['strong', 'emph', 'underline', 'strike', 'code', 'link', 'anchor']);
|
|
360
|
+
const KNOWN_ISLAND_TYPES = new Set(['table', 'image']);
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* @param {import('../core/wasm.js').ContentLine} line
|
|
364
|
+
* @returns {line is import('../core/wasm.js').ContentLine & { kind: string; attrs: unknown }}
|
|
365
|
+
*/
|
|
366
|
+
export function isUnknownLine(line) {
|
|
367
|
+
return typeof line?.kind === 'string' && !KNOWN_LINE_KINDS.has(line.kind);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* @param {import('../core/wasm.js').ContentContainer} container
|
|
372
|
+
* @returns {container is import('../core/wasm.js').ContentContainer & { container: string; attrs: unknown }}
|
|
373
|
+
*/
|
|
374
|
+
export function isUnknownContainer(container) {
|
|
375
|
+
return typeof container?.container === 'string' && !KNOWN_CONTAINERS.has(container.container);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* @param {import('../core/wasm.js').ContentMark} mark
|
|
380
|
+
* @returns {mark is import('../core/wasm.js').ContentMark & { type: string; attrs: unknown }}
|
|
381
|
+
*/
|
|
382
|
+
export function isUnknownMark(mark) {
|
|
383
|
+
return typeof mark?.type === 'string' && !KNOWN_MARK_TYPES.has(mark.type);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* @param {import('../core/wasm.js').ContentIsland} island
|
|
388
|
+
* @returns {island is import('../core/wasm.js').ContentIsland & { type: string; props: unknown }}
|
|
389
|
+
*/
|
|
390
|
+
export function isUnknownIsland(island) {
|
|
391
|
+
return typeof island?.type === 'string' && !KNOWN_ISLAND_TYPES.has(island.type);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Backend builds are NEVER statically imported here: that would pull a
|
|
173
395
|
// multi-MB binary into the eager graph and defeat lazy loading. Each entry is a
|
|
174
396
|
// DESCRIPTOR: `load` is a thunk returning a dynamic `import()` (a backend's
|
|
175
397
|
// chunk is fetched only when something actually renders against that backend),
|
|
@@ -200,7 +422,7 @@ const DEFAULT_BACKENDS = {
|
|
|
200
422
|
* backend id on any malformed entry. Descriptors are the ONLY accepted form:
|
|
201
423
|
* `{ load, formats, canvas }` with a callable `load`, a `formats` array, and a
|
|
202
424
|
* boolean `canvas`. Failing at construction (not deep inside a render) keeps the
|
|
203
|
-
* capability probes free
|
|
425
|
+
* capability probes free; they can answer from the manifest unconditionally.
|
|
204
426
|
* @param {string} id
|
|
205
427
|
* @param {unknown} entry
|
|
206
428
|
* @returns {{ load: () => Promise<unknown>, formats: string[], canvas: boolean }}
|
|
@@ -249,7 +471,7 @@ export class Engine {
|
|
|
249
471
|
* @param {{ backends?: Record<string, { load: () => Promise<unknown>, formats: string[], canvas: boolean }> }} [options]
|
|
250
472
|
* Extra or overriding backend descriptors, merged over the built-ins. Each
|
|
251
473
|
* entry is a descriptor (`{ load, formats, canvas }`) with `formats` and
|
|
252
|
-
* `canvas` REQUIRED
|
|
474
|
+
* `canvas` REQUIRED: that static manifest is what makes
|
|
253
475
|
* `supportedFormats`/`supportsCanvas` always free (no binary load, no quill
|
|
254
476
|
* clone). Malformed entries throw here, at construction. The default
|
|
255
477
|
* registry maps `"typst"` to the bundled Typst build.
|
|
@@ -266,7 +488,7 @@ export class Engine {
|
|
|
266
488
|
|
|
267
489
|
/**
|
|
268
490
|
* Look up the registered descriptor for `backendId`, throwing the canonical
|
|
269
|
-
* "no backend registered" error if none. Pure
|
|
491
|
+
* "no backend registered" error if none. Pure, touches no binary.
|
|
270
492
|
* @param {string} backendId
|
|
271
493
|
* @returns {{ load: () => Promise<unknown>, formats: string[], canvas: boolean }}
|
|
272
494
|
*/
|
|
@@ -281,6 +503,20 @@ export class Engine {
|
|
|
281
503
|
return descriptor;
|
|
282
504
|
}
|
|
283
505
|
|
|
506
|
+
/**
|
|
507
|
+
* `quill`'s backend id, after checking the handle. The ONE way an `Engine`
|
|
508
|
+
* verb reaches `backendId`, so "no verb touches a foreign quill" is
|
|
509
|
+
* structural rather than four remembered calls. See § "Handles from another
|
|
510
|
+
* copy".
|
|
511
|
+
* @param {Quill} quill
|
|
512
|
+
* @param {string} method the caller's name, for the rejection message
|
|
513
|
+
* @returns {string}
|
|
514
|
+
*/
|
|
515
|
+
#backendOf(quill, method) {
|
|
516
|
+
requireLocalQuill(quill, method);
|
|
517
|
+
return quill.backendId;
|
|
518
|
+
}
|
|
519
|
+
|
|
284
520
|
/**
|
|
285
521
|
* Resolve (and lazily load) the backend module + its engine for `backendId`.
|
|
286
522
|
* @param {string} backendId
|
|
@@ -314,9 +550,9 @@ export class Engine {
|
|
|
314
550
|
|
|
315
551
|
/**
|
|
316
552
|
* Get (or materialize-and-cache) the backend-memory `Quill` clone for
|
|
317
|
-
* `quill` under `backendId`. On a cache miss the clone is built from `tree
|
|
318
|
-
*
|
|
319
|
-
* freed by now
|
|
553
|
+
* `quill` under `backendId`. On a cache miss the clone is built from `tree`,
|
|
554
|
+
* the caller's pre-await `toTree()` snapshot; the canonical handle may be
|
|
555
|
+
* freed by now, and stored in the per-backend `WeakMap` keyed on the
|
|
320
556
|
* canonical `Quill` instance, so a later call with the same instance reuses
|
|
321
557
|
* it (the hot-path fix: no re-serialize / re-copy / re-validate per call).
|
|
322
558
|
* @param {any} mod the backend build module
|
|
@@ -346,33 +582,39 @@ export class Engine {
|
|
|
346
582
|
*
|
|
347
583
|
* OWNERSHIP WINDOW: both caller handles are snapshotted (`doc.toJson()`, and
|
|
348
584
|
* `quill.toTree()` on a clone-cache miss) BEFORE the first await. The backend
|
|
349
|
-
* load below is a real suspension point
|
|
350
|
-
* render
|
|
585
|
+
* load below is a real suspension point (a multi-MB `import()` on first
|
|
586
|
+
* render) so reading the handles after it would race a caller that
|
|
351
587
|
* `free()`s them as soon as this call returns its promise ("null pointer
|
|
352
588
|
* passed to rust"). The snapshot makes that natural calling pattern correct.
|
|
353
589
|
*
|
|
354
|
-
* Clone lifetimes differ by design: the `doc` clone is TRANSIENT
|
|
590
|
+
* Clone lifetimes differ by design: the `doc` clone is TRANSIENT, freed in
|
|
355
591
|
* the `finally` of every call. The `quill` clone is CACHED per (engine,
|
|
356
592
|
* backend, canonical quill instance) and is NOT freed here; a `Quill`
|
|
357
593
|
* instance's contents never change after construction, so it is dropped with
|
|
358
594
|
* the canonical quill (WeakMap collection → wasm-bindgen weak-ref free) when
|
|
359
595
|
* the consumer replaces the instance. A cache miss materializes it once;
|
|
360
596
|
* subsequent calls reuse it.
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
597
|
+
*
|
|
598
|
+
* Both handles are checked here, so the crossing is core→backend (always two
|
|
599
|
+
* memories, always as data); core→core is a duplicate install and never gets
|
|
600
|
+
* past the first line. See § "Handles from another copy".
|
|
601
|
+
* @param {string} method the caller's name, for the rejection message
|
|
602
|
+
* @param {Quill} quill
|
|
603
|
+
* @param {Document} doc
|
|
364
604
|
* @param {(ctx: { mod: any, engine: any, quill: any, doc: any }) => any} fn
|
|
365
605
|
*/
|
|
366
|
-
async #withClones(
|
|
606
|
+
async #withClones(method, quill, doc, fn) {
|
|
607
|
+
const backendId = this.#backendOf(quill, method);
|
|
608
|
+
requireLocalDoc(doc, method);
|
|
367
609
|
const docJson = doc.toJson();
|
|
368
610
|
const quillTree = this.#quillClones.get(backendId)?.has(quill) ? null : quill.toTree();
|
|
369
611
|
const { mod, engine } = await this.#resolveBackend(backendId);
|
|
370
612
|
// The quill clone is cached (see #cachedQuillClone); only the per-call doc
|
|
371
613
|
// clone is transient. Bring the doc clone + `fn` under one try so the doc
|
|
372
|
-
// clone is freed even if a later step throws
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
//
|
|
614
|
+
// clone is freed even if a later step throws. The cached quill clone is
|
|
615
|
+
// intentionally NOT freed here. `fn` MUST be synchronous: the doc clone is
|
|
616
|
+
// freed as soon as it returns, so an async `fn` would have it freed
|
|
617
|
+
// mid-flight.
|
|
376
618
|
const backendQuill = this.#cachedQuillClone(mod, backendId, quill, quillTree);
|
|
377
619
|
let backendDoc = null;
|
|
378
620
|
try {
|
|
@@ -393,7 +635,7 @@ export class Engine {
|
|
|
393
635
|
* @returns {Promise<import('./runtime.js').RenderResult>}
|
|
394
636
|
*/
|
|
395
637
|
async render(quill, doc, options) {
|
|
396
|
-
return this.#withClones(quill
|
|
638
|
+
return this.#withClones('engine.render(quill, doc)', quill, doc, ({ engine, quill: q, doc: d }) =>
|
|
397
639
|
engine.render(q, d, options ?? undefined)
|
|
398
640
|
);
|
|
399
641
|
}
|
|
@@ -411,7 +653,7 @@ export class Engine {
|
|
|
411
653
|
*/
|
|
412
654
|
async open(quill, doc) {
|
|
413
655
|
return this.#withClones(
|
|
414
|
-
quill
|
|
656
|
+
'engine.open(quill, doc)',
|
|
415
657
|
quill,
|
|
416
658
|
doc,
|
|
417
659
|
({ mod, engine, quill: q, doc: d }) => new LiveSession(engine.open(q, d), mod)
|
|
@@ -421,30 +663,30 @@ export class Engine {
|
|
|
421
663
|
/**
|
|
422
664
|
* The output formats `quill`'s backend can emit. A cheap, non-failing,
|
|
423
665
|
* ALWAYS-free pre-render probe: it answers from the descriptor's required
|
|
424
|
-
* `formats` manifest
|
|
666
|
+
* `formats` manifest (NO binary load and NO quill clone) depending only on
|
|
425
667
|
* `quill.backendId`. Stays `async` for API stability (it never awaits a load).
|
|
426
668
|
* @param {Quill} quill
|
|
427
669
|
* @returns {Promise<import('./runtime.js').OutputFormat[]>}
|
|
428
670
|
*/
|
|
429
671
|
async supportedFormats(quill) {
|
|
430
|
-
const descriptor = this.#descriptorFor(quill.
|
|
672
|
+
const descriptor = this.#descriptorFor(this.#backendOf(quill, 'engine.supportedFormats(quill)'));
|
|
431
673
|
// Defensive copy so callers can't mutate the shared manifest.
|
|
432
674
|
return descriptor.formats.slice();
|
|
433
675
|
}
|
|
434
676
|
|
|
435
677
|
/**
|
|
436
|
-
* Whether `quill`'s BACKEND can paint sessions to a canvas
|
|
678
|
+
* Whether `quill`'s BACKEND can paint sessions to a canvas: a pre-session
|
|
437
679
|
* ESTIMATE, not a fact about any particular compile. Same ALWAYS-free probe
|
|
438
680
|
* as `supportedFormats`: answered from the descriptor's required `canvas`
|
|
439
681
|
* manifest, no load and no clone. A specific compile can still refuse to
|
|
440
682
|
* paint (e.g. a 0-page document), so this can answer `true` while the
|
|
441
|
-
* resulting `LiveSession.supportsCanvas` answers `false
|
|
683
|
+
* resulting `LiveSession.supportsCanvas` answers `false`: gate mounting a
|
|
442
684
|
* canvas UI on this, gate the actual `paint` call on the session's getter.
|
|
443
685
|
* @param {Quill} quill
|
|
444
686
|
* @returns {Promise<boolean>}
|
|
445
687
|
*/
|
|
446
688
|
async supportsCanvas(quill) {
|
|
447
|
-
const descriptor = this.#descriptorFor(quill.
|
|
689
|
+
const descriptor = this.#descriptorFor(this.#backendOf(quill, 'engine.supportsCanvas(quill)'));
|
|
448
690
|
return descriptor.canvas;
|
|
449
691
|
}
|
|
450
692
|
}
|
|
@@ -453,16 +695,16 @@ export class Engine {
|
|
|
453
695
|
* Thin wrapper over a backend's live render session. Reads serve the current
|
|
454
696
|
* compile; `apply(doc)` recompiles in place (transactional: on throw, reads
|
|
455
697
|
* keep serving the last-good compile). The quill/document clones it was
|
|
456
|
-
* opened from have already been freed
|
|
698
|
+
* opened from have already been freed: the session retains what `apply`
|
|
457
699
|
* needs.
|
|
458
700
|
*
|
|
459
701
|
* Geometry reads (`regions`, `positionAt`, `locate`) resolve against the
|
|
460
702
|
* current compile; anchoring a caret or selection across edits is the editor's
|
|
461
|
-
* job (its own transaction mapping)
|
|
703
|
+
* job (its own transaction mapping): re-read geometry after each committed
|
|
462
704
|
* `apply`.
|
|
463
705
|
*
|
|
464
|
-
* `paint` writes a COMPLETE page raster
|
|
465
|
-
* compositing
|
|
706
|
+
* `paint` writes a COMPLETE page raster (all content visible, no caller-side
|
|
707
|
+
* compositing) for every backend that supports canvas (Typst rasterizes
|
|
466
708
|
* natively; pdfform rasterizes its pre-flattened page). See `runtime.d.ts`.
|
|
467
709
|
*/
|
|
468
710
|
export class LiveSession {
|
|
@@ -478,13 +720,14 @@ export class LiveSession {
|
|
|
478
720
|
#mod;
|
|
479
721
|
|
|
480
722
|
/**
|
|
481
|
-
* Recompile the session against `doc
|
|
723
|
+
* Recompile the session against `doc`: the edit verb of a live preview.
|
|
482
724
|
* Transactional: on throw every read keeps serving the last-good compile.
|
|
483
725
|
* On success reads serve the new compile; repaint `dirtyPages ∩ visible`.
|
|
484
726
|
* @param {Document} doc
|
|
485
727
|
* @returns {import('./runtime.d.ts').ChangeSet}
|
|
486
728
|
*/
|
|
487
729
|
apply(doc) {
|
|
730
|
+
requireLocalDoc(doc, 'session.apply(doc)');
|
|
488
731
|
let backendDoc = null;
|
|
489
732
|
try {
|
|
490
733
|
backendDoc = this.#mod.Document.fromJson(doc.toJson());
|
|
@@ -501,7 +744,7 @@ export class LiveSession {
|
|
|
501
744
|
return this.#inner.backendId;
|
|
502
745
|
}
|
|
503
746
|
/**
|
|
504
|
-
* `true` iff `paint`/`pageSize` will succeed for THIS compile
|
|
747
|
+
* `true` iff `paint`/`pageSize` will succeed for THIS compile: the
|
|
505
748
|
* authoritative answer, derived from the session's canvas seam, so it can
|
|
506
749
|
* never disagree with what `paint` actually does. This can be `false` even
|
|
507
750
|
* when `Engine.supportsCanvas` answered `true` for the same `quill` (that
|
|
@@ -523,7 +766,7 @@ export class LiveSession {
|
|
|
523
766
|
}
|
|
524
767
|
|
|
525
768
|
/**
|
|
526
|
-
* Schema-field geometry for this compiled session
|
|
769
|
+
* Schema-field geometry for this compiled session: one region per
|
|
527
770
|
* schema-bound field, keyed on its quill schema field path. A session-level
|
|
528
771
|
* query (no render); read it to place field overlays / cross-navigation over
|
|
529
772
|
* a `paint`-ed canvas.
|
|
@@ -534,11 +777,11 @@ export class LiveSession {
|
|
|
534
777
|
}
|
|
535
778
|
|
|
536
779
|
/**
|
|
537
|
-
* The whole-field highlight boxes for `field
|
|
780
|
+
* The whole-field highlight boxes for `field`: one union rect per page,
|
|
538
781
|
* over the field's `span`-bearing content segments. Owns the union
|
|
539
782
|
* `regions()` leaves derived (span-filter + per-page union), so a "highlight
|
|
540
783
|
* the focused field" consumer stops reimplementing it. Content only: a field
|
|
541
|
-
* placed solely as a scalar reference or a bound widget returns `[]
|
|
784
|
+
* placed solely as a scalar reference or a bound widget returns `[]`, its
|
|
542
785
|
* box is a single `regions()` rect.
|
|
543
786
|
* @param {string} field
|
|
544
787
|
* @returns {import('./runtime.d.ts').FieldRegion[]}
|
|
@@ -548,7 +791,7 @@ export class LiveSession {
|
|
|
548
791
|
}
|
|
549
792
|
|
|
550
793
|
/**
|
|
551
|
-
* The schema field whose content is under a point on `page
|
|
794
|
+
* The schema field whose content is under a point on `page`: the forward
|
|
552
795
|
* (click → field) direction, resolving *every* placement, not just the first
|
|
553
796
|
* that `regions` enumerates. `x`/`y` are PDF points with a bottom-left origin
|
|
554
797
|
* (the `FieldRegion.rect` space). See `runtime.d.ts` for the click-to-point
|
|
@@ -587,8 +830,8 @@ export class LiveSession {
|
|
|
587
830
|
}
|
|
588
831
|
|
|
589
832
|
/**
|
|
590
|
-
* Paint `page` into a 2D canvas context. The painted raster is COMPLETE
|
|
591
|
-
* all page content visible, no caller-side compositing
|
|
833
|
+
* Paint `page` into a 2D canvas context. The painted raster is COMPLETE
|
|
834
|
+
* (all page content visible, no caller-side compositing) for both the Typst
|
|
592
835
|
* and pdfform backends. See `runtime.d.ts` for the DPR/clamp math and the
|
|
593
836
|
* region-overlay coordinate transform.
|
|
594
837
|
* @param {CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D} ctx
|
|
@@ -607,21 +850,21 @@ export class LiveSession {
|
|
|
607
850
|
// ── Typed-writer sugar: bind the quill once ─────────────────────────────────
|
|
608
851
|
// Rust exposes `quill.writer(&mut doc)` so a caller issues bare `set` / `set_all`
|
|
609
852
|
// without threading the schema per write. The WASM `commit*` verbs can't borrow
|
|
610
|
-
// like that
|
|
853
|
+
// like that: a `Document` carries only a `$quill` REFERENCE, not the resolved
|
|
611
854
|
// schema, so each `commit*` method takes the `quill` handle as its first
|
|
612
855
|
// argument. These pure-JS classes restore the Rust ergonomics: bind `quill` +
|
|
613
856
|
// `doc` once, then issue `set` / `setAll` / `card(i).set`.
|
|
614
857
|
//
|
|
615
|
-
// They hold JS references to the caller's EXISTING handles
|
|
616
|
-
// their own, no `free()` burden, no second owner of either handle
|
|
858
|
+
// They hold JS references to the caller's EXISTING handles (no WASM object of
|
|
859
|
+
// their own, no `free()` burden, no second owner of either handle) and every
|
|
617
860
|
// write delegates straight to the underlying `commit*` verb: a schema field is
|
|
618
861
|
// typed-committed (coerced to canonical form, mismatch throws now), and a name
|
|
619
862
|
// the schema does not declare throws `UnknownField` rather than falling to the
|
|
620
|
-
// opaque store
|
|
863
|
+
// opaque store, on the typed path an undeclared name is a typo. Opaque storage
|
|
621
864
|
// stays available through the raw addressed `Document.storeField` verb.
|
|
622
865
|
|
|
623
866
|
/**
|
|
624
|
-
* A {@link Document} bound to its {@link Quill} for typed writes
|
|
867
|
+
* A {@link Document} bound to its {@link Quill} for typed writes: the JS twin
|
|
625
868
|
* of Rust's `quill.writer(&mut doc)`. Writes target the main card; use
|
|
626
869
|
* {@link card} for a composable card. Holds both handles by reference and owns
|
|
627
870
|
* neither, so there is nothing to `free()`.
|
|
@@ -634,10 +877,12 @@ export class DocumentWriter {
|
|
|
634
877
|
* @param {Document} doc the document to mutate, held by reference (not owned)
|
|
635
878
|
*/
|
|
636
879
|
constructor(quill, doc) {
|
|
880
|
+
requireLocalQuill(quill, 'quill.writer(doc)');
|
|
881
|
+
requireLocalDoc(doc, 'quill.writer(doc)');
|
|
637
882
|
this.#quill = quill;
|
|
638
883
|
this.#doc = doc;
|
|
639
884
|
}
|
|
640
|
-
/** The bound document
|
|
885
|
+
/** The bound document: the same instance passed in, mutated in place. */
|
|
641
886
|
get document() {
|
|
642
887
|
return this.#doc;
|
|
643
888
|
}
|
|
@@ -652,7 +897,7 @@ export class DocumentWriter {
|
|
|
652
897
|
return this.#doc._commitField(this.#quill, name, value);
|
|
653
898
|
}
|
|
654
899
|
/**
|
|
655
|
-
* Typed-commit several main-card fields atomically
|
|
900
|
+
* Typed-commit several main-card fields atomically: nothing is applied on
|
|
656
901
|
* error (throws a per-field diagnostic bundle, including an `UnknownField`
|
|
657
902
|
* for each undeclared name).
|
|
658
903
|
* @param {Record<string, unknown>} fields
|
|
@@ -663,7 +908,7 @@ export class DocumentWriter {
|
|
|
663
908
|
}
|
|
664
909
|
/**
|
|
665
910
|
* Set the main body from markdown (edit semantics: surviving anchors rebase),
|
|
666
|
-
* discarding the text delta
|
|
911
|
+
* discarding the text delta, the receipt-free body write. Call
|
|
667
912
|
* `doc.revise({}, md)` for the {@link Delta} receipt.
|
|
668
913
|
* @param {string} markdown
|
|
669
914
|
* @returns {void}
|
|
@@ -672,21 +917,25 @@ export class DocumentWriter {
|
|
|
672
917
|
this.#doc.revise({}, markdown);
|
|
673
918
|
}
|
|
674
919
|
/**
|
|
675
|
-
* Revise the
|
|
920
|
+
* Revise the content main-card field `name` from authored text: typed *and*
|
|
676
921
|
* anchor-preserving. Surviving anchors rebase, then the diffed result is
|
|
677
922
|
* schema-conformed (`richtext(inline)` rejects a multi-block result). Throws
|
|
678
923
|
* `UnknownField` for a name the schema does not declare. Returns the text
|
|
679
924
|
* {@link Delta}.
|
|
925
|
+
*
|
|
926
|
+
* The codec comes from the declared type: `richtext` diffs markdown, while
|
|
927
|
+
* `plaintext` diffs the literal text and never imports markdown, so a
|
|
928
|
+
* byte-identical revise of a value carrying escapes is a byte no-op.
|
|
680
929
|
* @param {string} name
|
|
681
|
-
* @param {string}
|
|
930
|
+
* @param {string} text
|
|
682
931
|
* @returns {import('../core/wasm.js').Delta}
|
|
683
932
|
*/
|
|
684
|
-
reviseField(name,
|
|
685
|
-
return this.#doc._reviseField(this.#quill, name,
|
|
933
|
+
reviseField(name, text) {
|
|
934
|
+
return this.#doc._reviseField(this.#quill, name, text);
|
|
686
935
|
}
|
|
687
936
|
/**
|
|
688
937
|
* Build a composable card of `kind`, typed-commit `fields` onto it, set its
|
|
689
|
-
* body from optional markdown, and place it
|
|
938
|
+
* body from optional markdown, and place it: the fused `makeCard` + typed
|
|
690
939
|
* commit + insertion. `at` picks the position: omitted appends, a number
|
|
691
940
|
* inserts at that index (`0..=cardCount`), so a positioned typed insert is one
|
|
692
941
|
* atomic call rather than `addCard` + `moveCard`. Transactional: the card is
|
|
@@ -704,7 +953,7 @@ export class DocumentWriter {
|
|
|
704
953
|
}
|
|
705
954
|
/**
|
|
706
955
|
* Remove the composable card at `index`, returning it (or `undefined` if the
|
|
707
|
-
* index is out of range)
|
|
956
|
+
* index is out of range): the writer spelling of `Document.removeCard`.
|
|
708
957
|
* @param {number} index
|
|
709
958
|
* @returns {import('../core/wasm.js').Card | undefined}
|
|
710
959
|
*/
|
|
@@ -714,12 +963,12 @@ export class DocumentWriter {
|
|
|
714
963
|
/**
|
|
715
964
|
* A {@link CardWriter} bound to the composable card at `index`. Index
|
|
716
965
|
* validity is checked lazily by the underlying write (it throws
|
|
717
|
-
* `IndexOutOfRange` at commit time), so
|
|
966
|
+
* `IndexOutOfRange` at commit time), so an out-of-range index does not throw
|
|
967
|
+
* here.
|
|
718
968
|
*
|
|
719
|
-
* The cursor is ephemeral
|
|
969
|
+
* The cursor is ephemeral: bind, write, discard. It holds `index`, not the
|
|
720
970
|
* card: a `removeCard`/`addCard` between binding and writing silently
|
|
721
|
-
* retargets it.
|
|
722
|
-
* at write time.
|
|
971
|
+
* retargets it. Re-resolve the index at write time when cards may move.
|
|
723
972
|
* @param {number} index
|
|
724
973
|
* @returns {CardWriter}
|
|
725
974
|
*/
|
|
@@ -743,6 +992,8 @@ export class CardWriter {
|
|
|
743
992
|
* @param {number} index the composable card's index
|
|
744
993
|
*/
|
|
745
994
|
constructor(quill, doc, index) {
|
|
995
|
+
requireLocalQuill(quill, 'writer.card(index)');
|
|
996
|
+
requireLocalDoc(doc, 'writer.card(index)');
|
|
746
997
|
this.#quill = quill;
|
|
747
998
|
this.#doc = doc;
|
|
748
999
|
this.#index = index;
|
|
@@ -753,7 +1004,7 @@ export class CardWriter {
|
|
|
753
1004
|
}
|
|
754
1005
|
/**
|
|
755
1006
|
* The bound card's `$kind` (empty string when it carries none), read through
|
|
756
|
-
* the document
|
|
1007
|
+
* the document: mirrors core `CardWriter::kind()`. Ephemeral like the cursor
|
|
757
1008
|
* itself: throws `IndexOutOfRange` if the bound index is out of range.
|
|
758
1009
|
* @returns {string}
|
|
759
1010
|
*/
|
|
@@ -782,7 +1033,7 @@ export class CardWriter {
|
|
|
782
1033
|
return this.#doc._commitFields(this.#quill, { card: this.#index }, fields);
|
|
783
1034
|
}
|
|
784
1035
|
/**
|
|
785
|
-
* Set this card's body from markdown (edit semantics), discarding the delta
|
|
1036
|
+
* Set this card's body from markdown (edit semantics), discarding the delta:
|
|
786
1037
|
* the card twin of {@link DocumentWriter.setBody}.
|
|
787
1038
|
* @param {string} markdown
|
|
788
1039
|
* @returns {void}
|
|
@@ -791,29 +1042,30 @@ export class CardWriter {
|
|
|
791
1042
|
this.#doc.revise({ card: this.#index }, markdown);
|
|
792
1043
|
}
|
|
793
1044
|
/**
|
|
794
|
-
* Revise the
|
|
795
|
-
* anchor-preserving; the card twin of {@link DocumentWriter.reviseField}
|
|
796
|
-
* Throws `UnknownField` for an undeclared name and
|
|
797
|
-
* bound index is out of range. Returns the text
|
|
1045
|
+
* Revise the content field `name` on this card from authored text: typed *and*
|
|
1046
|
+
* anchor-preserving; the card twin of {@link DocumentWriter.reviseField},
|
|
1047
|
+
* codec included. Throws `UnknownField` for an undeclared name and
|
|
1048
|
+
* `IndexOutOfRange` if the bound index is out of range. Returns the text
|
|
1049
|
+
* {@link Delta}.
|
|
798
1050
|
* @param {string} name
|
|
799
|
-
* @param {string}
|
|
1051
|
+
* @param {string} text
|
|
800
1052
|
* @returns {import('../core/wasm.js').Delta}
|
|
801
1053
|
*/
|
|
802
|
-
reviseField(name,
|
|
803
|
-
return this.#doc._reviseField(this.#quill, { card: this.#index, field: name },
|
|
1054
|
+
reviseField(name, text) {
|
|
1055
|
+
return this.#doc._reviseField(this.#quill, { card: this.#index, field: name }, text);
|
|
804
1056
|
}
|
|
805
1057
|
}
|
|
806
1058
|
|
|
807
|
-
// ── `quill.writer(doc)
|
|
1059
|
+
// ── `quill.writer(doc)`, the typed front door ──────────────────────────────
|
|
808
1060
|
// The schema-bound writer: bind the quill's schema to a document and issue bare
|
|
809
|
-
// typed writes. Mirrors core's `quill.writer(&mut doc)
|
|
1061
|
+
// typed writes. Mirrors core's `quill.writer(&mut doc)`: the schema grants the
|
|
810
1062
|
// typing, so the quill (not the document) is the factory. Patched onto the
|
|
811
1063
|
// re-exported `Quill` prototype rather than wrapped: `Quill === CoreQuill`
|
|
812
1064
|
// stays true (the identity invariant above); this only adds a method that
|
|
813
1065
|
// constructs the pure-JS writer, which owns no WASM handle.
|
|
814
1066
|
/**
|
|
815
1067
|
* A {@link DocumentWriter} binding this quill's schema to `doc` for typed
|
|
816
|
-
* writes
|
|
1068
|
+
* writes: the documented front door. The returned writer holds both handles by
|
|
817
1069
|
* reference and owns neither, so there is nothing to `free()`. Ephemeral by
|
|
818
1070
|
* convention: bind, write, discard.
|
|
819
1071
|
* @this {Quill}
|
|
@@ -825,8 +1077,8 @@ Quill.prototype.writer = function writer(doc) {
|
|
|
825
1077
|
};
|
|
826
1078
|
|
|
827
1079
|
// ── Typed-reader sugar: the schema-plane read surface ──────────────────────────
|
|
828
|
-
// The read twin of the writer above. The transport `Document.getStored` is schema-free
|
|
829
|
-
//
|
|
1080
|
+
// The read twin of the writer above. The transport `Document.getStored` is schema-free:
|
|
1081
|
+
// a `Document` cannot say which fields are richtext, so an unknown field name
|
|
830
1082
|
// reads back `undefined` rather than as the typo it is. Binding the quill's
|
|
831
1083
|
// schema (`_readerGet` takes the handle, like the `commit*` verbs) lets one `get`
|
|
832
1084
|
// interpret by declared type: a richtext field to markdown, a plaintext field to
|
|
@@ -836,7 +1088,7 @@ Quill.prototype.writer = function writer(doc) {
|
|
|
836
1088
|
// reference, own no WASM object, and have nothing to `free()`.
|
|
837
1089
|
|
|
838
1090
|
/**
|
|
839
|
-
* A {@link Document} bound to its {@link Quill} for typed reads
|
|
1091
|
+
* A {@link Document} bound to its {@link Quill} for typed reads: the JS twin of
|
|
840
1092
|
* Rust's `quill.reader(&doc)` and the read counterpart of {@link DocumentWriter}.
|
|
841
1093
|
* Reads target the main card; use {@link card} for a composable card. Holds both
|
|
842
1094
|
* handles by reference and owns neither, so there is nothing to `free()`.
|
|
@@ -849,10 +1101,12 @@ export class DocumentReader {
|
|
|
849
1101
|
* @param {Document} doc the document to read, held by reference (not owned)
|
|
850
1102
|
*/
|
|
851
1103
|
constructor(quill, doc) {
|
|
1104
|
+
requireLocalQuill(quill, 'quill.reader(doc)');
|
|
1105
|
+
requireLocalDoc(doc, 'quill.reader(doc)');
|
|
852
1106
|
this.#quill = quill;
|
|
853
1107
|
this.#doc = doc;
|
|
854
1108
|
}
|
|
855
|
-
/** The bound document
|
|
1109
|
+
/** The bound document: the same instance passed in. */
|
|
856
1110
|
get document() {
|
|
857
1111
|
return this.#doc;
|
|
858
1112
|
}
|
|
@@ -870,7 +1124,23 @@ export class DocumentReader {
|
|
|
870
1124
|
return this.#doc._readerGet(this.#quill, addr);
|
|
871
1125
|
}
|
|
872
1126
|
/**
|
|
873
|
-
*
|
|
1127
|
+
* Read the content field at `addr` as its canonical `Content` corpus: the
|
|
1128
|
+
* corpus twin of {@link get}, which projects. Decodes through the codec the
|
|
1129
|
+
* declared type names (`richtext` as markdown, `plaintext` as literal text),
|
|
1130
|
+
* so a field the writer committed as a corpus and one a markdown parse left
|
|
1131
|
+
* as an authored string read back the same; no branching on how the
|
|
1132
|
+
* document was built. An absent `addr.field` reads the body corpus.
|
|
1133
|
+
* `undefined` for an absent field; throws `UnknownField`, `FieldNotContent`
|
|
1134
|
+
* for a type that is not a content leaf, `FieldRichtextDecode` for an undecodable
|
|
1135
|
+
* value, and `IndexOutOfRange` for a bad `addr.card`.
|
|
1136
|
+
* @param {import('../core/wasm.js').Addr | string} addr
|
|
1137
|
+
* @returns {import('../core/wasm.js').Content | undefined}
|
|
1138
|
+
*/
|
|
1139
|
+
getContent(addr) {
|
|
1140
|
+
return this.#doc._readerGetContent(this.#quill, addr);
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* The main body's markdown: the quill-free body read (a body's type is a
|
|
874
1144
|
* format fact, not a schema fact). Equivalent to `get({})`.
|
|
875
1145
|
* @returns {string}
|
|
876
1146
|
*/
|
|
@@ -880,9 +1150,9 @@ export class DocumentReader {
|
|
|
880
1150
|
/**
|
|
881
1151
|
* A {@link CardReader} bound to the composable card at `index`. Index validity
|
|
882
1152
|
* is checked lazily by the underlying read (it throws `IndexOutOfRange` at read
|
|
883
|
-
* time), so
|
|
884
|
-
* it holds `index`, not the card, so a `removeCard`/`addCard`
|
|
885
|
-
* and reading silently retargets it.
|
|
1153
|
+
* time), so an out-of-range index does not throw here. Ephemeral like the
|
|
1154
|
+
* writer cursor: it holds `index`, not the card, so a `removeCard`/`addCard`
|
|
1155
|
+
* between binding and reading silently retargets it.
|
|
886
1156
|
* @param {number} index
|
|
887
1157
|
* @returns {CardReader}
|
|
888
1158
|
*/
|
|
@@ -906,6 +1176,8 @@ export class CardReader {
|
|
|
906
1176
|
* @param {number} index the composable card's index
|
|
907
1177
|
*/
|
|
908
1178
|
constructor(quill, doc, index) {
|
|
1179
|
+
requireLocalQuill(quill, 'reader.card(index)');
|
|
1180
|
+
requireLocalDoc(doc, 'reader.card(index)');
|
|
909
1181
|
this.#quill = quill;
|
|
910
1182
|
this.#doc = doc;
|
|
911
1183
|
this.#index = index;
|
|
@@ -933,7 +1205,16 @@ export class CardReader {
|
|
|
933
1205
|
return this.#doc._readerGet(this.#quill, { card: this.#index, field: name });
|
|
934
1206
|
}
|
|
935
1207
|
/**
|
|
936
|
-
*
|
|
1208
|
+
* Read the content field `name` on this card as its canonical `Content`
|
|
1209
|
+
* corpus: the card twin of {@link DocumentReader.getContent}.
|
|
1210
|
+
* @param {string} name
|
|
1211
|
+
* @returns {import('../core/wasm.js').Content | undefined}
|
|
1212
|
+
*/
|
|
1213
|
+
getContent(name) {
|
|
1214
|
+
return this.#doc._readerGetContent(this.#quill, { card: this.#index, field: name });
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* This card's body markdown: the card twin of {@link DocumentReader.getBody}.
|
|
937
1218
|
* @returns {string}
|
|
938
1219
|
*/
|
|
939
1220
|
getBody() {
|
|
@@ -941,13 +1222,13 @@ export class CardReader {
|
|
|
941
1222
|
}
|
|
942
1223
|
}
|
|
943
1224
|
|
|
944
|
-
// ── `quill.reader(doc)
|
|
1225
|
+
// ── `quill.reader(doc)`: the schema-plane read front door ─────────────────────
|
|
945
1226
|
// The read twin of `quill.writer(doc)`, patched onto the same re-exported `Quill`
|
|
946
|
-
// prototype (the `Quill === CoreQuill` identity invariant holds
|
|
1227
|
+
// prototype (the `Quill === CoreQuill` identity invariant holds: this only adds
|
|
947
1228
|
// a method constructing the pure-JS reader, which owns no WASM handle).
|
|
948
1229
|
/**
|
|
949
1230
|
* A {@link DocumentReader} binding this quill's schema to `doc` for interpreted
|
|
950
|
-
* reads
|
|
1231
|
+
* reads: the read front door, mirroring core's `quill.reader(&doc)`. The returned
|
|
951
1232
|
* reader holds both handles by reference and owns neither, so there is nothing to
|
|
952
1233
|
* `free()`. Ephemeral by convention: bind, read, discard.
|
|
953
1234
|
* @this {Quill}
|