@quillmark/wasm 0.97.0 → 0.99.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 +303 -0
- package/LICENSE +13 -0
- package/README.md +63 -60
- package/backends/pdfform/wasm.d.ts +300 -143
- package/backends/pdfform/wasm_bg.js +101 -102
- package/backends/pdfform/wasm_bg.wasm +0 -0
- package/backends/typst/wasm.d.ts +300 -143
- package/backends/typst/wasm_bg.js +101 -102
- package/backends/typst/wasm_bg.wasm +0 -0
- package/core/wasm.d.ts +127 -93
- package/core/wasm_bg.js +84 -82
- package/core/wasm_bg.wasm +0 -0
- package/package.json +2 -2
- package/runtime/runtime.d.ts +157 -79
- package/runtime/runtime.js +375 -95
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,16 +102,179 @@ 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'])) {
|
|
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
|
-
// `ContentIsland.type
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
265
|
+
// `ContentIsland.type`, `ContentMark.type`, `ContentLine.kind`, and
|
|
266
|
+
// `ContentContainer.container` are OPEN sets: each union carries a residual
|
|
267
|
+
// `{ …: string; … }` arm, so a bare `x.type === 'table'` check never narrows the
|
|
268
|
+
// payload, TS keeps the residual arm live (a `string` can be `'table'`),
|
|
269
|
+
// leaving `props` / the mark payload / `level` opaque at every consumer. These
|
|
106
270
|
// are the checked narrowing path: on the true branch the payload's pinned shape
|
|
107
|
-
// is asserted. Only the payload-carrying arms get a guard
|
|
108
|
-
// carries `props`, a `link` mark carries `url`, an `anchor` mark carries `id
|
|
109
|
-
//
|
|
110
|
-
//
|
|
271
|
+
// is asserted. Only the payload-carrying arms get a guard: an island always
|
|
272
|
+
// carries `props`, a `link` mark carries `url`, an `anchor` mark carries `id`, a
|
|
273
|
+
// `heading` line carries `level` and a `code` line `lang`, a `list_item`
|
|
274
|
+
// container its shape; the payload-free arms (`strong`/`emph`/`underline`/
|
|
275
|
+
// `strike`/`code` marks, `para`/`island`/`rule` lines, `quote`) narrow to
|
|
276
|
+
// nothing. An unrecognized discriminant fails every guard and keeps its opaque
|
|
277
|
+
// `attrs`/`props`.
|
|
111
278
|
|
|
112
279
|
/**
|
|
113
280
|
* @param {import('../core/wasm.js').ContentIsland} island
|
|
@@ -141,7 +308,90 @@ export function isAnchorMark(mark) {
|
|
|
141
308
|
return mark.type === 'anchor';
|
|
142
309
|
}
|
|
143
310
|
|
|
144
|
-
|
|
311
|
+
/**
|
|
312
|
+
* @param {import('../core/wasm.js').ContentLine} line
|
|
313
|
+
* @returns {line is import('../core/wasm.js').ContentLine & { kind: 'heading'; level: number }}
|
|
314
|
+
*/
|
|
315
|
+
export function isHeadingLine(line) {
|
|
316
|
+
return line.kind === 'heading';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* @param {import('../core/wasm.js').ContentLine} line
|
|
321
|
+
* @returns {line is import('../core/wasm.js').ContentLine & { kind: 'code'; lang?: string }}
|
|
322
|
+
*/
|
|
323
|
+
export function isCodeLine(line) {
|
|
324
|
+
return line.kind === 'code';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* @param {import('../core/wasm.js').ContentContainer} container
|
|
329
|
+
* @returns {container is import('../core/wasm.js').ContentContainer & { container: 'list_item'; ordered: boolean; start: number; ordinal: number }}
|
|
330
|
+
*/
|
|
331
|
+
export function isListItemContainer(container) {
|
|
332
|
+
return container.container === 'list_item';
|
|
333
|
+
}
|
|
334
|
+
|
|
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
|
|
145
395
|
// multi-MB binary into the eager graph and defeat lazy loading. Each entry is a
|
|
146
396
|
// DESCRIPTOR: `load` is a thunk returning a dynamic `import()` (a backend's
|
|
147
397
|
// chunk is fetched only when something actually renders against that backend),
|
|
@@ -172,7 +422,7 @@ const DEFAULT_BACKENDS = {
|
|
|
172
422
|
* backend id on any malformed entry. Descriptors are the ONLY accepted form:
|
|
173
423
|
* `{ load, formats, canvas }` with a callable `load`, a `formats` array, and a
|
|
174
424
|
* boolean `canvas`. Failing at construction (not deep inside a render) keeps the
|
|
175
|
-
* capability probes free
|
|
425
|
+
* capability probes free; they can answer from the manifest unconditionally.
|
|
176
426
|
* @param {string} id
|
|
177
427
|
* @param {unknown} entry
|
|
178
428
|
* @returns {{ load: () => Promise<unknown>, formats: string[], canvas: boolean }}
|
|
@@ -221,7 +471,7 @@ export class Engine {
|
|
|
221
471
|
* @param {{ backends?: Record<string, { load: () => Promise<unknown>, formats: string[], canvas: boolean }> }} [options]
|
|
222
472
|
* Extra or overriding backend descriptors, merged over the built-ins. Each
|
|
223
473
|
* entry is a descriptor (`{ load, formats, canvas }`) with `formats` and
|
|
224
|
-
* `canvas` REQUIRED
|
|
474
|
+
* `canvas` REQUIRED: that static manifest is what makes
|
|
225
475
|
* `supportedFormats`/`supportsCanvas` always free (no binary load, no quill
|
|
226
476
|
* clone). Malformed entries throw here, at construction. The default
|
|
227
477
|
* registry maps `"typst"` to the bundled Typst build.
|
|
@@ -238,7 +488,7 @@ export class Engine {
|
|
|
238
488
|
|
|
239
489
|
/**
|
|
240
490
|
* Look up the registered descriptor for `backendId`, throwing the canonical
|
|
241
|
-
* "no backend registered" error if none. Pure
|
|
491
|
+
* "no backend registered" error if none. Pure, touches no binary.
|
|
242
492
|
* @param {string} backendId
|
|
243
493
|
* @returns {{ load: () => Promise<unknown>, formats: string[], canvas: boolean }}
|
|
244
494
|
*/
|
|
@@ -253,6 +503,20 @@ export class Engine {
|
|
|
253
503
|
return descriptor;
|
|
254
504
|
}
|
|
255
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
|
+
|
|
256
520
|
/**
|
|
257
521
|
* Resolve (and lazily load) the backend module + its engine for `backendId`.
|
|
258
522
|
* @param {string} backendId
|
|
@@ -286,9 +550,9 @@ export class Engine {
|
|
|
286
550
|
|
|
287
551
|
/**
|
|
288
552
|
* Get (or materialize-and-cache) the backend-memory `Quill` clone for
|
|
289
|
-
* `quill` under `backendId`. On a cache miss the clone is built from `tree
|
|
290
|
-
*
|
|
291
|
-
* 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
|
|
292
556
|
* canonical `Quill` instance, so a later call with the same instance reuses
|
|
293
557
|
* it (the hot-path fix: no re-serialize / re-copy / re-validate per call).
|
|
294
558
|
* @param {any} mod the backend build module
|
|
@@ -318,33 +582,39 @@ export class Engine {
|
|
|
318
582
|
*
|
|
319
583
|
* OWNERSHIP WINDOW: both caller handles are snapshotted (`doc.toJson()`, and
|
|
320
584
|
* `quill.toTree()` on a clone-cache miss) BEFORE the first await. The backend
|
|
321
|
-
* load below is a real suspension point
|
|
322
|
-
* 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
|
|
323
587
|
* `free()`s them as soon as this call returns its promise ("null pointer
|
|
324
588
|
* passed to rust"). The snapshot makes that natural calling pattern correct.
|
|
325
589
|
*
|
|
326
|
-
* Clone lifetimes differ by design: the `doc` clone is TRANSIENT
|
|
590
|
+
* Clone lifetimes differ by design: the `doc` clone is TRANSIENT, freed in
|
|
327
591
|
* the `finally` of every call. The `quill` clone is CACHED per (engine,
|
|
328
592
|
* backend, canonical quill instance) and is NOT freed here; a `Quill`
|
|
329
593
|
* instance's contents never change after construction, so it is dropped with
|
|
330
594
|
* the canonical quill (WeakMap collection → wasm-bindgen weak-ref free) when
|
|
331
595
|
* the consumer replaces the instance. A cache miss materializes it once;
|
|
332
596
|
* subsequent calls reuse it.
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
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
|
|
336
604
|
* @param {(ctx: { mod: any, engine: any, quill: any, doc: any }) => any} fn
|
|
337
605
|
*/
|
|
338
|
-
async #withClones(
|
|
606
|
+
async #withClones(method, quill, doc, fn) {
|
|
607
|
+
const backendId = this.#backendOf(quill, method);
|
|
608
|
+
requireLocalDoc(doc, method);
|
|
339
609
|
const docJson = doc.toJson();
|
|
340
610
|
const quillTree = this.#quillClones.get(backendId)?.has(quill) ? null : quill.toTree();
|
|
341
611
|
const { mod, engine } = await this.#resolveBackend(backendId);
|
|
342
612
|
// The quill clone is cached (see #cachedQuillClone); only the per-call doc
|
|
343
613
|
// clone is transient. Bring the doc clone + `fn` under one try so the doc
|
|
344
|
-
// clone is freed even if a later step throws
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
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.
|
|
348
618
|
const backendQuill = this.#cachedQuillClone(mod, backendId, quill, quillTree);
|
|
349
619
|
let backendDoc = null;
|
|
350
620
|
try {
|
|
@@ -365,7 +635,7 @@ export class Engine {
|
|
|
365
635
|
* @returns {Promise<import('./runtime.js').RenderResult>}
|
|
366
636
|
*/
|
|
367
637
|
async render(quill, doc, options) {
|
|
368
|
-
return this.#withClones(quill
|
|
638
|
+
return this.#withClones('engine.render(quill, doc)', quill, doc, ({ engine, quill: q, doc: d }) =>
|
|
369
639
|
engine.render(q, d, options ?? undefined)
|
|
370
640
|
);
|
|
371
641
|
}
|
|
@@ -383,7 +653,7 @@ export class Engine {
|
|
|
383
653
|
*/
|
|
384
654
|
async open(quill, doc) {
|
|
385
655
|
return this.#withClones(
|
|
386
|
-
quill
|
|
656
|
+
'engine.open(quill, doc)',
|
|
387
657
|
quill,
|
|
388
658
|
doc,
|
|
389
659
|
({ mod, engine, quill: q, doc: d }) => new LiveSession(engine.open(q, d), mod)
|
|
@@ -393,30 +663,30 @@ export class Engine {
|
|
|
393
663
|
/**
|
|
394
664
|
* The output formats `quill`'s backend can emit. A cheap, non-failing,
|
|
395
665
|
* ALWAYS-free pre-render probe: it answers from the descriptor's required
|
|
396
|
-
* `formats` manifest
|
|
666
|
+
* `formats` manifest (NO binary load and NO quill clone) depending only on
|
|
397
667
|
* `quill.backendId`. Stays `async` for API stability (it never awaits a load).
|
|
398
668
|
* @param {Quill} quill
|
|
399
669
|
* @returns {Promise<import('./runtime.js').OutputFormat[]>}
|
|
400
670
|
*/
|
|
401
671
|
async supportedFormats(quill) {
|
|
402
|
-
const descriptor = this.#descriptorFor(quill.
|
|
672
|
+
const descriptor = this.#descriptorFor(this.#backendOf(quill, 'engine.supportedFormats(quill)'));
|
|
403
673
|
// Defensive copy so callers can't mutate the shared manifest.
|
|
404
674
|
return descriptor.formats.slice();
|
|
405
675
|
}
|
|
406
676
|
|
|
407
677
|
/**
|
|
408
|
-
* Whether `quill`'s BACKEND can paint sessions to a canvas
|
|
678
|
+
* Whether `quill`'s BACKEND can paint sessions to a canvas: a pre-session
|
|
409
679
|
* ESTIMATE, not a fact about any particular compile. Same ALWAYS-free probe
|
|
410
680
|
* as `supportedFormats`: answered from the descriptor's required `canvas`
|
|
411
681
|
* manifest, no load and no clone. A specific compile can still refuse to
|
|
412
682
|
* paint (e.g. a 0-page document), so this can answer `true` while the
|
|
413
|
-
* resulting `LiveSession.supportsCanvas` answers `false
|
|
683
|
+
* resulting `LiveSession.supportsCanvas` answers `false`: gate mounting a
|
|
414
684
|
* canvas UI on this, gate the actual `paint` call on the session's getter.
|
|
415
685
|
* @param {Quill} quill
|
|
416
686
|
* @returns {Promise<boolean>}
|
|
417
687
|
*/
|
|
418
688
|
async supportsCanvas(quill) {
|
|
419
|
-
const descriptor = this.#descriptorFor(quill.
|
|
689
|
+
const descriptor = this.#descriptorFor(this.#backendOf(quill, 'engine.supportsCanvas(quill)'));
|
|
420
690
|
return descriptor.canvas;
|
|
421
691
|
}
|
|
422
692
|
}
|
|
@@ -425,16 +695,16 @@ export class Engine {
|
|
|
425
695
|
* Thin wrapper over a backend's live render session. Reads serve the current
|
|
426
696
|
* compile; `apply(doc)` recompiles in place (transactional: on throw, reads
|
|
427
697
|
* keep serving the last-good compile). The quill/document clones it was
|
|
428
|
-
* opened from have already been freed
|
|
698
|
+
* opened from have already been freed: the session retains what `apply`
|
|
429
699
|
* needs.
|
|
430
700
|
*
|
|
431
701
|
* Geometry reads (`regions`, `positionAt`, `locate`) resolve against the
|
|
432
702
|
* current compile; anchoring a caret or selection across edits is the editor's
|
|
433
|
-
* job (its own transaction mapping)
|
|
703
|
+
* job (its own transaction mapping): re-read geometry after each committed
|
|
434
704
|
* `apply`.
|
|
435
705
|
*
|
|
436
|
-
* `paint` writes a COMPLETE page raster
|
|
437
|
-
* compositing
|
|
706
|
+
* `paint` writes a COMPLETE page raster (all content visible, no caller-side
|
|
707
|
+
* compositing) for every backend that supports canvas (Typst rasterizes
|
|
438
708
|
* natively; pdfform rasterizes its pre-flattened page). See `runtime.d.ts`.
|
|
439
709
|
*/
|
|
440
710
|
export class LiveSession {
|
|
@@ -450,13 +720,14 @@ export class LiveSession {
|
|
|
450
720
|
#mod;
|
|
451
721
|
|
|
452
722
|
/**
|
|
453
|
-
* Recompile the session against `doc
|
|
723
|
+
* Recompile the session against `doc`: the edit verb of a live preview.
|
|
454
724
|
* Transactional: on throw every read keeps serving the last-good compile.
|
|
455
725
|
* On success reads serve the new compile; repaint `dirtyPages ∩ visible`.
|
|
456
726
|
* @param {Document} doc
|
|
457
727
|
* @returns {import('./runtime.d.ts').ChangeSet}
|
|
458
728
|
*/
|
|
459
729
|
apply(doc) {
|
|
730
|
+
requireLocalDoc(doc, 'session.apply(doc)');
|
|
460
731
|
let backendDoc = null;
|
|
461
732
|
try {
|
|
462
733
|
backendDoc = this.#mod.Document.fromJson(doc.toJson());
|
|
@@ -473,7 +744,7 @@ export class LiveSession {
|
|
|
473
744
|
return this.#inner.backendId;
|
|
474
745
|
}
|
|
475
746
|
/**
|
|
476
|
-
* `true` iff `paint`/`pageSize` will succeed for THIS compile
|
|
747
|
+
* `true` iff `paint`/`pageSize` will succeed for THIS compile: the
|
|
477
748
|
* authoritative answer, derived from the session's canvas seam, so it can
|
|
478
749
|
* never disagree with what `paint` actually does. This can be `false` even
|
|
479
750
|
* when `Engine.supportsCanvas` answered `true` for the same `quill` (that
|
|
@@ -495,7 +766,7 @@ export class LiveSession {
|
|
|
495
766
|
}
|
|
496
767
|
|
|
497
768
|
/**
|
|
498
|
-
* Schema-field geometry for this compiled session
|
|
769
|
+
* Schema-field geometry for this compiled session: one region per
|
|
499
770
|
* schema-bound field, keyed on its quill schema field path. A session-level
|
|
500
771
|
* query (no render); read it to place field overlays / cross-navigation over
|
|
501
772
|
* a `paint`-ed canvas.
|
|
@@ -506,11 +777,11 @@ export class LiveSession {
|
|
|
506
777
|
}
|
|
507
778
|
|
|
508
779
|
/**
|
|
509
|
-
* The whole-field highlight boxes for `field
|
|
780
|
+
* The whole-field highlight boxes for `field`: one union rect per page,
|
|
510
781
|
* over the field's `span`-bearing content segments. Owns the union
|
|
511
782
|
* `regions()` leaves derived (span-filter + per-page union), so a "highlight
|
|
512
783
|
* the focused field" consumer stops reimplementing it. Content only: a field
|
|
513
|
-
* placed solely as a scalar reference or a bound widget returns `[]
|
|
784
|
+
* placed solely as a scalar reference or a bound widget returns `[]`, its
|
|
514
785
|
* box is a single `regions()` rect.
|
|
515
786
|
* @param {string} field
|
|
516
787
|
* @returns {import('./runtime.d.ts').FieldRegion[]}
|
|
@@ -520,7 +791,7 @@ export class LiveSession {
|
|
|
520
791
|
}
|
|
521
792
|
|
|
522
793
|
/**
|
|
523
|
-
* 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
|
|
524
795
|
* (click → field) direction, resolving *every* placement, not just the first
|
|
525
796
|
* that `regions` enumerates. `x`/`y` are PDF points with a bottom-left origin
|
|
526
797
|
* (the `FieldRegion.rect` space). See `runtime.d.ts` for the click-to-point
|
|
@@ -559,8 +830,8 @@ export class LiveSession {
|
|
|
559
830
|
}
|
|
560
831
|
|
|
561
832
|
/**
|
|
562
|
-
* Paint `page` into a 2D canvas context. The painted raster is COMPLETE
|
|
563
|
-
* 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
|
|
564
835
|
* and pdfform backends. See `runtime.d.ts` for the DPR/clamp math and the
|
|
565
836
|
* region-overlay coordinate transform.
|
|
566
837
|
* @param {CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D} ctx
|
|
@@ -579,21 +850,21 @@ export class LiveSession {
|
|
|
579
850
|
// ── Typed-writer sugar: bind the quill once ─────────────────────────────────
|
|
580
851
|
// Rust exposes `quill.writer(&mut doc)` so a caller issues bare `set` / `set_all`
|
|
581
852
|
// without threading the schema per write. The WASM `commit*` verbs can't borrow
|
|
582
|
-
// like that
|
|
853
|
+
// like that: a `Document` carries only a `$quill` REFERENCE, not the resolved
|
|
583
854
|
// schema, so each `commit*` method takes the `quill` handle as its first
|
|
584
855
|
// argument. These pure-JS classes restore the Rust ergonomics: bind `quill` +
|
|
585
856
|
// `doc` once, then issue `set` / `setAll` / `card(i).set`.
|
|
586
857
|
//
|
|
587
|
-
// They hold JS references to the caller's EXISTING handles
|
|
588
|
-
// 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
|
|
589
860
|
// write delegates straight to the underlying `commit*` verb: a schema field is
|
|
590
861
|
// typed-committed (coerced to canonical form, mismatch throws now), and a name
|
|
591
862
|
// the schema does not declare throws `UnknownField` rather than falling to the
|
|
592
|
-
// opaque store
|
|
863
|
+
// opaque store, on the typed path an undeclared name is a typo. Opaque storage
|
|
593
864
|
// stays available through the raw addressed `Document.storeField` verb.
|
|
594
865
|
|
|
595
866
|
/**
|
|
596
|
-
* 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
|
|
597
868
|
* of Rust's `quill.writer(&mut doc)`. Writes target the main card; use
|
|
598
869
|
* {@link card} for a composable card. Holds both handles by reference and owns
|
|
599
870
|
* neither, so there is nothing to `free()`.
|
|
@@ -606,10 +877,12 @@ export class DocumentWriter {
|
|
|
606
877
|
* @param {Document} doc the document to mutate, held by reference (not owned)
|
|
607
878
|
*/
|
|
608
879
|
constructor(quill, doc) {
|
|
880
|
+
requireLocalQuill(quill, 'quill.writer(doc)');
|
|
881
|
+
requireLocalDoc(doc, 'quill.writer(doc)');
|
|
609
882
|
this.#quill = quill;
|
|
610
883
|
this.#doc = doc;
|
|
611
884
|
}
|
|
612
|
-
/** The bound document
|
|
885
|
+
/** The bound document: the same instance passed in, mutated in place. */
|
|
613
886
|
get document() {
|
|
614
887
|
return this.#doc;
|
|
615
888
|
}
|
|
@@ -624,7 +897,7 @@ export class DocumentWriter {
|
|
|
624
897
|
return this.#doc._commitField(this.#quill, name, value);
|
|
625
898
|
}
|
|
626
899
|
/**
|
|
627
|
-
* Typed-commit several main-card fields atomically
|
|
900
|
+
* Typed-commit several main-card fields atomically: nothing is applied on
|
|
628
901
|
* error (throws a per-field diagnostic bundle, including an `UnknownField`
|
|
629
902
|
* for each undeclared name).
|
|
630
903
|
* @param {Record<string, unknown>} fields
|
|
@@ -635,7 +908,7 @@ export class DocumentWriter {
|
|
|
635
908
|
}
|
|
636
909
|
/**
|
|
637
910
|
* Set the main body from markdown (edit semantics: surviving anchors rebase),
|
|
638
|
-
* discarding the text delta
|
|
911
|
+
* discarding the text delta, the receipt-free body write. Call
|
|
639
912
|
* `doc.revise({}, md)` for the {@link Delta} receipt.
|
|
640
913
|
* @param {string} markdown
|
|
641
914
|
* @returns {void}
|
|
@@ -644,7 +917,7 @@ export class DocumentWriter {
|
|
|
644
917
|
this.#doc.revise({}, markdown);
|
|
645
918
|
}
|
|
646
919
|
/**
|
|
647
|
-
* Revise the richtext main-card field `name` from markdown
|
|
920
|
+
* Revise the richtext main-card field `name` from markdown: typed *and*
|
|
648
921
|
* anchor-preserving. Surviving anchors rebase, then the diffed result is
|
|
649
922
|
* schema-conformed (`richtext(inline)` rejects a multi-block result). Throws
|
|
650
923
|
* `UnknownField` for a name the schema does not declare. Returns the text
|
|
@@ -658,7 +931,7 @@ export class DocumentWriter {
|
|
|
658
931
|
}
|
|
659
932
|
/**
|
|
660
933
|
* Build a composable card of `kind`, typed-commit `fields` onto it, set its
|
|
661
|
-
* body from optional markdown, and place it
|
|
934
|
+
* body from optional markdown, and place it: the fused `makeCard` + typed
|
|
662
935
|
* commit + insertion. `at` picks the position: omitted appends, a number
|
|
663
936
|
* inserts at that index (`0..=cardCount`), so a positioned typed insert is one
|
|
664
937
|
* atomic call rather than `addCard` + `moveCard`. Transactional: the card is
|
|
@@ -676,7 +949,7 @@ export class DocumentWriter {
|
|
|
676
949
|
}
|
|
677
950
|
/**
|
|
678
951
|
* Remove the composable card at `index`, returning it (or `undefined` if the
|
|
679
|
-
* index is out of range)
|
|
952
|
+
* index is out of range): the writer spelling of `Document.removeCard`.
|
|
680
953
|
* @param {number} index
|
|
681
954
|
* @returns {import('../core/wasm.js').Card | undefined}
|
|
682
955
|
*/
|
|
@@ -686,9 +959,10 @@ export class DocumentWriter {
|
|
|
686
959
|
/**
|
|
687
960
|
* A {@link CardWriter} bound to the composable card at `index`. Index
|
|
688
961
|
* validity is checked lazily by the underlying write (it throws
|
|
689
|
-
* `IndexOutOfRange` at commit time), so
|
|
962
|
+
* `IndexOutOfRange` at commit time), so an out-of-range index does not throw
|
|
963
|
+
* here.
|
|
690
964
|
*
|
|
691
|
-
* The cursor is ephemeral
|
|
965
|
+
* The cursor is ephemeral: bind, write, discard. It holds `index`, not the
|
|
692
966
|
* card: a `removeCard`/`addCard` between binding and writing silently
|
|
693
967
|
* retargets it. For durable addressing stamp `$id` and re-resolve the index
|
|
694
968
|
* at write time.
|
|
@@ -715,6 +989,8 @@ export class CardWriter {
|
|
|
715
989
|
* @param {number} index the composable card's index
|
|
716
990
|
*/
|
|
717
991
|
constructor(quill, doc, index) {
|
|
992
|
+
requireLocalQuill(quill, 'writer.card(index)');
|
|
993
|
+
requireLocalDoc(doc, 'writer.card(index)');
|
|
718
994
|
this.#quill = quill;
|
|
719
995
|
this.#doc = doc;
|
|
720
996
|
this.#index = index;
|
|
@@ -725,7 +1001,7 @@ export class CardWriter {
|
|
|
725
1001
|
}
|
|
726
1002
|
/**
|
|
727
1003
|
* The bound card's `$kind` (empty string when it carries none), read through
|
|
728
|
-
* the document
|
|
1004
|
+
* the document: mirrors core `CardWriter::kind()`. Ephemeral like the cursor
|
|
729
1005
|
* itself: throws `IndexOutOfRange` if the bound index is out of range.
|
|
730
1006
|
* @returns {string}
|
|
731
1007
|
*/
|
|
@@ -754,7 +1030,7 @@ export class CardWriter {
|
|
|
754
1030
|
return this.#doc._commitFields(this.#quill, { card: this.#index }, fields);
|
|
755
1031
|
}
|
|
756
1032
|
/**
|
|
757
|
-
* Set this card's body from markdown (edit semantics), discarding the delta
|
|
1033
|
+
* Set this card's body from markdown (edit semantics), discarding the delta:
|
|
758
1034
|
* the card twin of {@link DocumentWriter.setBody}.
|
|
759
1035
|
* @param {string} markdown
|
|
760
1036
|
* @returns {void}
|
|
@@ -763,7 +1039,7 @@ export class CardWriter {
|
|
|
763
1039
|
this.#doc.revise({ card: this.#index }, markdown);
|
|
764
1040
|
}
|
|
765
1041
|
/**
|
|
766
|
-
* Revise the richtext field `name` on this card from markdown
|
|
1042
|
+
* Revise the richtext field `name` on this card from markdown: typed *and*
|
|
767
1043
|
* anchor-preserving; the card twin of {@link DocumentWriter.reviseField}.
|
|
768
1044
|
* Throws `UnknownField` for an undeclared name and `IndexOutOfRange` if the
|
|
769
1045
|
* bound index is out of range. Returns the text {@link Delta}.
|
|
@@ -776,16 +1052,16 @@ export class CardWriter {
|
|
|
776
1052
|
}
|
|
777
1053
|
}
|
|
778
1054
|
|
|
779
|
-
// ── `quill.writer(doc)
|
|
1055
|
+
// ── `quill.writer(doc)`, the typed front door ──────────────────────────────
|
|
780
1056
|
// The schema-bound writer: bind the quill's schema to a document and issue bare
|
|
781
|
-
// typed writes. Mirrors core's `quill.writer(&mut doc)
|
|
1057
|
+
// typed writes. Mirrors core's `quill.writer(&mut doc)`: the schema grants the
|
|
782
1058
|
// typing, so the quill (not the document) is the factory. Patched onto the
|
|
783
1059
|
// re-exported `Quill` prototype rather than wrapped: `Quill === CoreQuill`
|
|
784
1060
|
// stays true (the identity invariant above); this only adds a method that
|
|
785
1061
|
// constructs the pure-JS writer, which owns no WASM handle.
|
|
786
1062
|
/**
|
|
787
1063
|
* A {@link DocumentWriter} binding this quill's schema to `doc` for typed
|
|
788
|
-
* writes
|
|
1064
|
+
* writes: the documented front door. The returned writer holds both handles by
|
|
789
1065
|
* reference and owns neither, so there is nothing to `free()`. Ephemeral by
|
|
790
1066
|
* convention: bind, write, discard.
|
|
791
1067
|
* @this {Quill}
|
|
@@ -797,8 +1073,8 @@ Quill.prototype.writer = function writer(doc) {
|
|
|
797
1073
|
};
|
|
798
1074
|
|
|
799
1075
|
// ── Typed-reader sugar: the schema-plane read surface ──────────────────────────
|
|
800
|
-
// The read twin of the writer above. The transport `Document.getStored` is schema-free
|
|
801
|
-
//
|
|
1076
|
+
// The read twin of the writer above. The transport `Document.getStored` is schema-free:
|
|
1077
|
+
// a `Document` cannot say which fields are richtext, so an unknown field name
|
|
802
1078
|
// reads back `undefined` rather than as the typo it is. Binding the quill's
|
|
803
1079
|
// schema (`_readerGet` takes the handle, like the `commit*` verbs) lets one `get`
|
|
804
1080
|
// interpret by declared type: a richtext field to markdown, a plaintext field to
|
|
@@ -808,7 +1084,7 @@ Quill.prototype.writer = function writer(doc) {
|
|
|
808
1084
|
// reference, own no WASM object, and have nothing to `free()`.
|
|
809
1085
|
|
|
810
1086
|
/**
|
|
811
|
-
* A {@link Document} bound to its {@link Quill} for typed reads
|
|
1087
|
+
* A {@link Document} bound to its {@link Quill} for typed reads: the JS twin of
|
|
812
1088
|
* Rust's `quill.reader(&doc)` and the read counterpart of {@link DocumentWriter}.
|
|
813
1089
|
* Reads target the main card; use {@link card} for a composable card. Holds both
|
|
814
1090
|
* handles by reference and owns neither, so there is nothing to `free()`.
|
|
@@ -821,10 +1097,12 @@ export class DocumentReader {
|
|
|
821
1097
|
* @param {Document} doc the document to read, held by reference (not owned)
|
|
822
1098
|
*/
|
|
823
1099
|
constructor(quill, doc) {
|
|
1100
|
+
requireLocalQuill(quill, 'quill.reader(doc)');
|
|
1101
|
+
requireLocalDoc(doc, 'quill.reader(doc)');
|
|
824
1102
|
this.#quill = quill;
|
|
825
1103
|
this.#doc = doc;
|
|
826
1104
|
}
|
|
827
|
-
/** The bound document
|
|
1105
|
+
/** The bound document: the same instance passed in. */
|
|
828
1106
|
get document() {
|
|
829
1107
|
return this.#doc;
|
|
830
1108
|
}
|
|
@@ -842,7 +1120,7 @@ export class DocumentReader {
|
|
|
842
1120
|
return this.#doc._readerGet(this.#quill, addr);
|
|
843
1121
|
}
|
|
844
1122
|
/**
|
|
845
|
-
* The main body's markdown
|
|
1123
|
+
* The main body's markdown: the quill-free body read (a body's type is a
|
|
846
1124
|
* format fact, not a schema fact). Equivalent to `get({})`.
|
|
847
1125
|
* @returns {string}
|
|
848
1126
|
*/
|
|
@@ -852,9 +1130,9 @@ export class DocumentReader {
|
|
|
852
1130
|
/**
|
|
853
1131
|
* A {@link CardReader} bound to the composable card at `index`. Index validity
|
|
854
1132
|
* is checked lazily by the underlying read (it throws `IndexOutOfRange` at read
|
|
855
|
-
* time), so
|
|
856
|
-
* it holds `index`, not the card, so a `removeCard`/`addCard`
|
|
857
|
-
* and reading silently retargets it.
|
|
1133
|
+
* time), so an out-of-range index does not throw here. Ephemeral like the
|
|
1134
|
+
* writer cursor: it holds `index`, not the card, so a `removeCard`/`addCard`
|
|
1135
|
+
* between binding and reading silently retargets it.
|
|
858
1136
|
* @param {number} index
|
|
859
1137
|
* @returns {CardReader}
|
|
860
1138
|
*/
|
|
@@ -878,6 +1156,8 @@ export class CardReader {
|
|
|
878
1156
|
* @param {number} index the composable card's index
|
|
879
1157
|
*/
|
|
880
1158
|
constructor(quill, doc, index) {
|
|
1159
|
+
requireLocalQuill(quill, 'reader.card(index)');
|
|
1160
|
+
requireLocalDoc(doc, 'reader.card(index)');
|
|
881
1161
|
this.#quill = quill;
|
|
882
1162
|
this.#doc = doc;
|
|
883
1163
|
this.#index = index;
|
|
@@ -905,7 +1185,7 @@ export class CardReader {
|
|
|
905
1185
|
return this.#doc._readerGet(this.#quill, { card: this.#index, field: name });
|
|
906
1186
|
}
|
|
907
1187
|
/**
|
|
908
|
-
* This card's body markdown
|
|
1188
|
+
* This card's body markdown: the card twin of {@link DocumentReader.getBody}.
|
|
909
1189
|
* @returns {string}
|
|
910
1190
|
*/
|
|
911
1191
|
getBody() {
|
|
@@ -913,13 +1193,13 @@ export class CardReader {
|
|
|
913
1193
|
}
|
|
914
1194
|
}
|
|
915
1195
|
|
|
916
|
-
// ── `quill.reader(doc)
|
|
1196
|
+
// ── `quill.reader(doc)`: the schema-plane read front door ─────────────────────
|
|
917
1197
|
// The read twin of `quill.writer(doc)`, patched onto the same re-exported `Quill`
|
|
918
|
-
// prototype (the `Quill === CoreQuill` identity invariant holds
|
|
1198
|
+
// prototype (the `Quill === CoreQuill` identity invariant holds: this only adds
|
|
919
1199
|
// a method constructing the pure-JS reader, which owns no WASM handle).
|
|
920
1200
|
/**
|
|
921
1201
|
* A {@link DocumentReader} binding this quill's schema to `doc` for interpreted
|
|
922
|
-
* reads
|
|
1202
|
+
* reads: the read front door, mirroring core's `quill.reader(&doc)`. The returned
|
|
923
1203
|
* reader holds both handles by reference and owns neither, so there is nothing to
|
|
924
1204
|
* `free()`. Ephemeral by convention: bind, read, discard.
|
|
925
1205
|
* @this {Quill}
|