@quillmark/wasm 0.92.1 → 0.95.1
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 +301 -4
- package/README.md +167 -66
- package/backends/pdfform/wasm.d.ts +1066 -0
- package/backends/pdfform/wasm.js +9 -0
- package/backends/pdfform/wasm_bg.js +2654 -0
- package/backends/pdfform/wasm_bg.wasm +0 -0
- package/backends/pdfform/wasm_bg.wasm.d.ts +98 -0
- package/backends/typst/wasm.d.ts +584 -167
- package/backends/typst/wasm.js +1 -1
- package/backends/typst/wasm_bg.js +1047 -397
- package/backends/typst/wasm_bg.wasm +0 -0
- package/backends/typst/wasm_bg.wasm.d.ts +45 -25
- package/core/wasm.d.ts +412 -110
- package/core/wasm.js +1 -1
- package/core/wasm_bg.js +713 -226
- package/core/wasm_bg.wasm +0 -0
- package/core/wasm_bg.wasm.d.ts +31 -17
- package/package.json +4 -3
- package/runtime/runtime.d.ts +459 -21
- package/runtime/runtime.js +536 -30
package/runtime/runtime.js
CHANGED
|
@@ -52,7 +52,29 @@
|
|
|
52
52
|
// (it is) so it tolerates handles from any core instance. The `runtime.test.js`
|
|
53
53
|
// "re-exports the internal core build classes verbatim" case
|
|
54
54
|
// (`Quill === CoreQuill`) is the executable guard for this invariant.
|
|
55
|
-
|
|
55
|
+
//
|
|
56
|
+
// Imported (not bare re-exported) so `Quill` is a local binding this module can
|
|
57
|
+
// augment — `quill.writer(doc)` is patched onto its prototype below. The
|
|
58
|
+
// re-export keeps the identity: the exported `Quill` IS the core class.
|
|
59
|
+
import { Quill, Document, init } from '../core/wasm.js';
|
|
60
|
+
export { Quill, Document, init };
|
|
61
|
+
// The document-free content codec — re-exported verbatim from the core build so
|
|
62
|
+
// the runtime subpath exposes `exportMarkdown(body)` (the on-demand markdown
|
|
63
|
+
// projection), `importMarkdown`, and the position-mapping pair (`rebase`,
|
|
64
|
+
// `mapPos`).
|
|
65
|
+
export { importMarkdown, exportMarkdown, rebase, mapPos } from '../core/wasm.js';
|
|
66
|
+
|
|
67
|
+
// ── The main-card address ───────────────────────────────────────────────────
|
|
68
|
+
/**
|
|
69
|
+
* The main card's address — the default target of the card-scoped verbs
|
|
70
|
+
* (`storeFields` / `storeExt` / `commitFields` / …). A named, `CardAddr`-typed
|
|
71
|
+
* alias for the empty address `{}`, so a main-card write names its target:
|
|
72
|
+
* `doc.storeFields(MAIN_CARD_ADDR, fields)`. It IS `{}` (frozen), a pure alias —
|
|
73
|
+
* `{}` and `undefined` stay equally valid. Card axis only: a card selector,
|
|
74
|
+
* never a field address.
|
|
75
|
+
* @type {import('../core/wasm.js').CardAddr}
|
|
76
|
+
*/
|
|
77
|
+
export const MAIN_CARD_ADDR = Object.freeze({});
|
|
56
78
|
|
|
57
79
|
/**
|
|
58
80
|
* Narrow an unknown caught value to a `QuillmarkError` — the error every
|
|
@@ -79,15 +101,22 @@ export function isQuillmarkError(e) {
|
|
|
79
101
|
// and `formats`/`canvas` are the REQUIRED static capability manifest so the
|
|
80
102
|
// cheap probes (`supportedFormats`/`supportsCanvas`) ALWAYS answer without
|
|
81
103
|
// loading the binary or cloning the quill. The manifest values are verified
|
|
82
|
-
// against
|
|
83
|
-
// `SUPPORTED_FORMATS`
|
|
84
|
-
//
|
|
85
|
-
//
|
|
104
|
+
// against each backend's Rust source (`crates/backends/<id>/src/lib.rs`
|
|
105
|
+
// `SUPPORTED_FORMATS`) and pinned by the `runtime.test.js` drift-guard test,
|
|
106
|
+
// which renders once and asserts the loaded backend reports the same list.
|
|
107
|
+
// `canvas` mirrors `quillmark_core::formats_support_canvas`: true iff the
|
|
108
|
+
// format list includes a visual-page format (`svg` or `png`).
|
|
86
109
|
const DEFAULT_BACKENDS = {
|
|
87
110
|
typst: {
|
|
88
111
|
load: () => import('../backends/typst/wasm.js'),
|
|
89
112
|
formats: ['pdf', 'svg', 'png'], // crates/backends/typst/src/lib.rs SUPPORTED_FORMATS
|
|
90
|
-
canvas: true //
|
|
113
|
+
canvas: true // has svg/png → formats_support_canvas == true
|
|
114
|
+
},
|
|
115
|
+
pdfform: {
|
|
116
|
+
load: () => import('../backends/pdfform/wasm.js'),
|
|
117
|
+
// crates/backends/pdfform/src/lib.rs SUPPORTED_FORMATS == [Pdf, Svg, Png]
|
|
118
|
+
formats: ['pdf', 'svg', 'png'],
|
|
119
|
+
canvas: true // has svg/png → formats_support_canvas == true
|
|
91
120
|
}
|
|
92
121
|
};
|
|
93
122
|
|
|
@@ -210,16 +239,18 @@ export class Engine {
|
|
|
210
239
|
|
|
211
240
|
/**
|
|
212
241
|
* Get (or materialize-and-cache) the backend-memory `Quill` clone for
|
|
213
|
-
* `quill` under `backendId`. On a cache miss the clone is built
|
|
214
|
-
* `toTree
|
|
242
|
+
* `quill` under `backendId`. On a cache miss the clone is built from `tree`
|
|
243
|
+
* — the caller's pre-await `toTree()` snapshot; the canonical handle may be
|
|
244
|
+
* freed by now — and stored in the per-backend `WeakMap` keyed on the
|
|
215
245
|
* canonical `Quill` instance, so a later call with the same instance reuses
|
|
216
246
|
* it (the hot-path fix: no re-serialize / re-copy / re-validate per call).
|
|
217
247
|
* @param {any} mod the backend build module
|
|
218
248
|
* @param {string} backendId
|
|
219
|
-
* @param {
|
|
249
|
+
* @param {object} quill the canonical instance (cache key only)
|
|
250
|
+
* @param {Map<string, Uint8Array> | null} tree pre-await snapshot; `null` on a cache hit
|
|
220
251
|
* @returns {any} the backend-memory quill clone
|
|
221
252
|
*/
|
|
222
|
-
#cachedQuillClone(mod, backendId, quill) {
|
|
253
|
+
#cachedQuillClone(mod, backendId, quill, tree) {
|
|
223
254
|
let perQuill = this.#quillClones.get(backendId);
|
|
224
255
|
if (!perQuill) {
|
|
225
256
|
perQuill = new WeakMap();
|
|
@@ -227,7 +258,7 @@ export class Engine {
|
|
|
227
258
|
}
|
|
228
259
|
let backendQuill = perQuill.get(quill);
|
|
229
260
|
if (!backendQuill) {
|
|
230
|
-
backendQuill = mod.Quill.fromTree(
|
|
261
|
+
backendQuill = mod.Quill.fromTree(tree);
|
|
231
262
|
perQuill.set(quill, backendQuill);
|
|
232
263
|
}
|
|
233
264
|
return backendQuill;
|
|
@@ -238,6 +269,13 @@ export class Engine {
|
|
|
238
269
|
* memory and run `fn` against the backend engine. Only `render`/`open` call
|
|
239
270
|
* this, so `doc` is always present.
|
|
240
271
|
*
|
|
272
|
+
* OWNERSHIP WINDOW: both caller handles are snapshotted (`doc.toJson()`, and
|
|
273
|
+
* `quill.toTree()` on a clone-cache miss) BEFORE the first await. The backend
|
|
274
|
+
* load below is a real suspension point — a multi-MB `import()` on first
|
|
275
|
+
* render — so reading the handles after it would race a caller that
|
|
276
|
+
* `free()`s them as soon as this call returns its promise ("null pointer
|
|
277
|
+
* passed to rust"). The snapshot makes that natural calling pattern correct.
|
|
278
|
+
*
|
|
241
279
|
* Clone lifetimes differ by design: the `doc` clone is TRANSIENT — freed in
|
|
242
280
|
* the `finally` of every call. The `quill` clone is CACHED per (engine,
|
|
243
281
|
* backend, canonical quill instance) and is NOT freed here; a `Quill`
|
|
@@ -251,6 +289,8 @@ export class Engine {
|
|
|
251
289
|
* @param {(ctx: { mod: any, engine: any, quill: any, doc: any }) => any} fn
|
|
252
290
|
*/
|
|
253
291
|
async #withClones(backendId, quill, doc, fn) {
|
|
292
|
+
const docJson = doc.toJson();
|
|
293
|
+
const quillTree = this.#quillClones.get(backendId)?.has(quill) ? null : quill.toTree();
|
|
254
294
|
const { mod, engine } = await this.#resolveBackend(backendId);
|
|
255
295
|
// The quill clone is cached (see #cachedQuillClone); only the per-call doc
|
|
256
296
|
// clone is transient. Bring the doc clone + `fn` under one try so the doc
|
|
@@ -258,10 +298,10 @@ export class Engine {
|
|
|
258
298
|
// rejecting a cross-version DTO). The cached quill clone is intentionally
|
|
259
299
|
// NOT freed here. `fn` MUST be synchronous — the doc clone is freed as soon
|
|
260
300
|
// as it returns, so an async `fn` would have it freed mid-flight.
|
|
261
|
-
const backendQuill = this.#cachedQuillClone(mod, backendId, quill);
|
|
301
|
+
const backendQuill = this.#cachedQuillClone(mod, backendId, quill, quillTree);
|
|
262
302
|
let backendDoc = null;
|
|
263
303
|
try {
|
|
264
|
-
backendDoc = mod.Document.fromJson(
|
|
304
|
+
backendDoc = mod.Document.fromJson(docJson);
|
|
265
305
|
return fn({ mod, engine, quill: backendQuill, doc: backendDoc });
|
|
266
306
|
} finally {
|
|
267
307
|
backendDoc?.free();
|
|
@@ -270,6 +310,8 @@ export class Engine {
|
|
|
270
310
|
|
|
271
311
|
/**
|
|
272
312
|
* Render `doc` against `quill` in one shot, returning a `RenderResult`.
|
|
313
|
+
* Both handles are read synchronously before the first await, so the caller
|
|
314
|
+
* may `free()` them as soon as this call returns.
|
|
273
315
|
* @param {Quill} quill
|
|
274
316
|
* @param {Document} doc
|
|
275
317
|
* @param {object} [options] render options (`{ format, ppi, pages, producer }`)
|
|
@@ -282,23 +324,22 @@ export class Engine {
|
|
|
282
324
|
}
|
|
283
325
|
|
|
284
326
|
/**
|
|
285
|
-
* Open
|
|
286
|
-
* The session is
|
|
287
|
-
* and document clones are freed before this returns;
|
|
288
|
-
* returned session and must `.free()` it.
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
* 0.x release. `render()` is the stable path.
|
|
327
|
+
* Open a live render session (canvas preview / per-page paint / `apply`).
|
|
328
|
+
* The session is self-contained (it retains what it needs for `apply`), so
|
|
329
|
+
* the transient quill and document clones are freed before this returns;
|
|
330
|
+
* the caller owns the returned session and must `.free()` it. The `quill`
|
|
331
|
+
* and `doc` handles are read synchronously before the first await, so the
|
|
332
|
+
* caller may `free()` them as soon as this call returns.
|
|
292
333
|
* @param {Quill} quill
|
|
293
334
|
* @param {Document} doc
|
|
294
|
-
* @returns {Promise<
|
|
335
|
+
* @returns {Promise<LiveSession>}
|
|
295
336
|
*/
|
|
296
337
|
async open(quill, doc) {
|
|
297
338
|
return this.#withClones(
|
|
298
339
|
quill.backendId,
|
|
299
340
|
quill,
|
|
300
341
|
doc,
|
|
301
|
-
({ engine, quill: q, doc: d }) => new
|
|
342
|
+
({ mod, engine, quill: q, doc: d }) => new LiveSession(engine.open(q, d), mod)
|
|
302
343
|
);
|
|
303
344
|
}
|
|
304
345
|
|
|
@@ -317,9 +358,13 @@ export class Engine {
|
|
|
317
358
|
}
|
|
318
359
|
|
|
319
360
|
/**
|
|
320
|
-
* Whether `quill`'s
|
|
321
|
-
*
|
|
322
|
-
* `
|
|
361
|
+
* Whether `quill`'s BACKEND can paint sessions to a canvas — a pre-session
|
|
362
|
+
* ESTIMATE, not a fact about any particular compile. Same ALWAYS-free probe
|
|
363
|
+
* as `supportedFormats`: answered from the descriptor's required `canvas`
|
|
364
|
+
* manifest, no load and no clone. A specific compile can still refuse to
|
|
365
|
+
* paint (e.g. a 0-page document), so this can answer `true` while the
|
|
366
|
+
* resulting `LiveSession.supportsCanvas` answers `false` — gate mounting a
|
|
367
|
+
* canvas UI on this, gate the actual `paint` call on the session's getter.
|
|
323
368
|
* @param {Quill} quill
|
|
324
369
|
* @returns {Promise<boolean>}
|
|
325
370
|
*/
|
|
@@ -330,15 +375,49 @@ export class Engine {
|
|
|
330
375
|
}
|
|
331
376
|
|
|
332
377
|
/**
|
|
333
|
-
* Thin wrapper over a backend's
|
|
334
|
-
*
|
|
378
|
+
* Thin wrapper over a backend's live render session. Reads serve the current
|
|
379
|
+
* compile; `apply(doc)` recompiles in place (transactional: on throw, reads
|
|
380
|
+
* keep serving the last-good compile). The quill/document clones it was
|
|
381
|
+
* opened from have already been freed — the session retains what `apply`
|
|
382
|
+
* needs.
|
|
383
|
+
*
|
|
384
|
+
* Geometry reads (`regions`, `positionAt`, `locate`) resolve against the
|
|
385
|
+
* current compile; anchoring a caret or selection across edits is the editor's
|
|
386
|
+
* job (its own transaction mapping) — re-read geometry after each committed
|
|
387
|
+
* `apply`.
|
|
388
|
+
*
|
|
389
|
+
* `paint` writes a COMPLETE page raster — all content visible, no caller-side
|
|
390
|
+
* compositing — for every backend that supports canvas (Typst rasterizes
|
|
391
|
+
* natively; pdfform rasterizes its pre-flattened page). See `runtime.d.ts`.
|
|
335
392
|
*/
|
|
336
|
-
export class
|
|
337
|
-
/**
|
|
338
|
-
|
|
393
|
+
export class LiveSession {
|
|
394
|
+
/**
|
|
395
|
+
* @param {{ pageCount: number, backendId: string, supportsCanvas: boolean, warnings: any[], apply: Function, render: Function, regions: Function, pageSize: Function, paint: Function, free: Function }} inner backend-build LiveSession (typst or pdfform)
|
|
396
|
+
* @param {{ Document: { fromJson(json: string): any } }} mod the session's backend build, used to materialize `apply` documents in its linear memory
|
|
397
|
+
*/
|
|
398
|
+
constructor(inner, mod) {
|
|
339
399
|
this.#inner = inner;
|
|
400
|
+
this.#mod = mod;
|
|
340
401
|
}
|
|
341
402
|
#inner;
|
|
403
|
+
#mod;
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Recompile the session against `doc` — the edit verb of a live preview.
|
|
407
|
+
* Transactional: on throw every read keeps serving the last-good compile.
|
|
408
|
+
* On success reads serve the new compile; repaint `dirtyPages ∩ visible`.
|
|
409
|
+
* @param {Document} doc
|
|
410
|
+
* @returns {import('./runtime.d.ts').ChangeSet}
|
|
411
|
+
*/
|
|
412
|
+
apply(doc) {
|
|
413
|
+
let backendDoc = null;
|
|
414
|
+
try {
|
|
415
|
+
backendDoc = this.#mod.Document.fromJson(doc.toJson());
|
|
416
|
+
return this.#inner.apply(backendDoc);
|
|
417
|
+
} finally {
|
|
418
|
+
backendDoc?.free();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
342
421
|
|
|
343
422
|
get pageCount() {
|
|
344
423
|
return this.#inner.pageCount;
|
|
@@ -346,6 +425,16 @@ export class RenderSession {
|
|
|
346
425
|
get backendId() {
|
|
347
426
|
return this.#inner.backendId;
|
|
348
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* `true` iff `paint`/`pageSize` will succeed for THIS compile — the
|
|
430
|
+
* authoritative answer, derived from the session's canvas seam, so it can
|
|
431
|
+
* never disagree with what `paint` actually does. This can be `false` even
|
|
432
|
+
* when `Engine.supportsCanvas` answered `true` for the same `quill` (that
|
|
433
|
+
* probe is a pre-session backend estimate; e.g. a canvas-capable backend
|
|
434
|
+
* compiled to a 0-page document has nothing to paint). Re-check this getter
|
|
435
|
+
* after `open()` rather than relying on the engine hint alone.
|
|
436
|
+
* @returns {boolean}
|
|
437
|
+
*/
|
|
349
438
|
get supportsCanvas() {
|
|
350
439
|
return this.#inner.supportsCanvas;
|
|
351
440
|
}
|
|
@@ -358,12 +447,75 @@ export class RenderSession {
|
|
|
358
447
|
return this.#inner.render(options ?? undefined);
|
|
359
448
|
}
|
|
360
449
|
|
|
450
|
+
/**
|
|
451
|
+
* Schema-field geometry for this compiled session — one region per
|
|
452
|
+
* schema-bound field, keyed on its quill schema field path. A session-level
|
|
453
|
+
* query (no render); read it to place field overlays / cross-navigation over
|
|
454
|
+
* a `paint`-ed canvas.
|
|
455
|
+
* @returns {import('./runtime.d.ts').FieldRegion[]}
|
|
456
|
+
*/
|
|
457
|
+
regions() {
|
|
458
|
+
return this.#inner.regions();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* The whole-field highlight boxes for `field` — one union rect per page,
|
|
463
|
+
* over the field's `span`-bearing content segments. Owns the union
|
|
464
|
+
* `regions()` leaves derived (span-filter + per-page union), so a "highlight
|
|
465
|
+
* the focused field" consumer stops reimplementing it. Content only: a field
|
|
466
|
+
* placed solely as a scalar reference or a bound widget returns `[]` — its
|
|
467
|
+
* box is a single `regions()` rect.
|
|
468
|
+
* @param {string} field
|
|
469
|
+
* @returns {import('./runtime.d.ts').FieldRegion[]}
|
|
470
|
+
*/
|
|
471
|
+
fieldBoxes(field) {
|
|
472
|
+
return this.#inner.fieldBoxes(field);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* The schema field whose content is under a point on `page` — the forward
|
|
477
|
+
* (click → field) direction, resolving *every* placement, not just the first
|
|
478
|
+
* that `regions` enumerates. `x`/`y` are PDF points with a bottom-left origin
|
|
479
|
+
* (the `FieldRegion.rect` space). See `runtime.d.ts` for the click-to-point
|
|
480
|
+
* inverse transform.
|
|
481
|
+
* @param {number} page
|
|
482
|
+
* @param {number} x
|
|
483
|
+
* @param {number} y
|
|
484
|
+
* @returns {string | undefined}
|
|
485
|
+
*/
|
|
486
|
+
fieldAt(page, x, y) {
|
|
487
|
+
return this.#inner.fieldAt(page, x, y);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* @param {number} page
|
|
492
|
+
* @param {number} x
|
|
493
|
+
* @param {number} y
|
|
494
|
+
* @returns {import('./runtime.d.ts').ContentHit | undefined}
|
|
495
|
+
*/
|
|
496
|
+
positionAt(page, x, y) {
|
|
497
|
+
return this.#inner.positionAt(page, x, y);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* @param {string} field
|
|
502
|
+
* @param {number} pos
|
|
503
|
+
* @returns {import('./runtime.d.ts').FieldRegion | undefined}
|
|
504
|
+
*/
|
|
505
|
+
locate(field, pos) {
|
|
506
|
+
return this.#inner.locate(field, pos);
|
|
507
|
+
}
|
|
508
|
+
|
|
361
509
|
/** @param {number} page */
|
|
362
510
|
pageSize(page) {
|
|
363
511
|
return this.#inner.pageSize(page);
|
|
364
512
|
}
|
|
365
513
|
|
|
366
514
|
/**
|
|
515
|
+
* Paint `page` into a 2D canvas context. The painted raster is COMPLETE —
|
|
516
|
+
* all page content visible, no caller-side compositing — for both the Typst
|
|
517
|
+
* and pdfform backends. See `runtime.d.ts` for the DPR/clamp math and the
|
|
518
|
+
* region-overlay coordinate transform.
|
|
367
519
|
* @param {CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D} ctx
|
|
368
520
|
* @param {number} page
|
|
369
521
|
* @param {object} [options]
|
|
@@ -376,3 +528,357 @@ export class RenderSession {
|
|
|
376
528
|
this.#inner.free();
|
|
377
529
|
}
|
|
378
530
|
}
|
|
531
|
+
|
|
532
|
+
// ── Typed-writer sugar: bind the quill once ─────────────────────────────────
|
|
533
|
+
// Rust exposes `quill.writer(&mut doc)` so a caller issues bare `set` / `set_all`
|
|
534
|
+
// without threading the schema per write. The WASM `commit*` verbs can't borrow
|
|
535
|
+
// like that — a `Document` carries only a `$quill` REFERENCE, not the resolved
|
|
536
|
+
// schema, so each `commit*` method takes the `quill` handle as its first
|
|
537
|
+
// argument. These pure-JS classes restore the Rust ergonomics: bind `quill` +
|
|
538
|
+
// `doc` once, then issue `set` / `setAll` / `card(i).set`.
|
|
539
|
+
//
|
|
540
|
+
// They hold JS references to the caller's EXISTING handles — no WASM object of
|
|
541
|
+
// their own, no `free()` burden, no second owner of either handle — and every
|
|
542
|
+
// write delegates straight to the underlying `commit*` verb: a schema field is
|
|
543
|
+
// typed-committed (coerced to canonical form, mismatch throws now), and a name
|
|
544
|
+
// the schema does not declare throws `UnknownField` rather than falling to the
|
|
545
|
+
// opaque store — on the typed path an undeclared name is a typo. Opaque storage
|
|
546
|
+
// stays available through the raw addressed `Document.storeField` verb.
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* A {@link Document} bound to its {@link Quill} for typed writes — the JS twin
|
|
550
|
+
* of Rust's `quill.writer(&mut doc)`. Writes target the main card; use
|
|
551
|
+
* {@link card} for a composable card. Holds both handles by reference and owns
|
|
552
|
+
* neither, so there is nothing to `free()`.
|
|
553
|
+
*/
|
|
554
|
+
export class DocumentWriter {
|
|
555
|
+
#quill;
|
|
556
|
+
#doc;
|
|
557
|
+
/**
|
|
558
|
+
* @param {Quill} quill the schema source for typed commits
|
|
559
|
+
* @param {Document} doc the document to mutate, held by reference (not owned)
|
|
560
|
+
*/
|
|
561
|
+
constructor(quill, doc) {
|
|
562
|
+
this.#quill = quill;
|
|
563
|
+
this.#doc = doc;
|
|
564
|
+
}
|
|
565
|
+
/** The bound document — the same instance passed in, mutated in place. */
|
|
566
|
+
get document() {
|
|
567
|
+
return this.#doc;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Typed-commit one main-card field (strict coerce, mismatch throws now).
|
|
571
|
+
* Throws `UnknownField` for a name the schema does not declare.
|
|
572
|
+
* @param {string} name
|
|
573
|
+
* @param {unknown} value
|
|
574
|
+
* @returns {void}
|
|
575
|
+
*/
|
|
576
|
+
set(name, value) {
|
|
577
|
+
return this.#doc._commitField(this.#quill, name, value);
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Typed-commit several main-card fields atomically — nothing is applied on
|
|
581
|
+
* error (throws a per-field diagnostic bundle, including an `UnknownField`
|
|
582
|
+
* for each undeclared name).
|
|
583
|
+
* @param {Record<string, unknown>} fields
|
|
584
|
+
* @returns {void}
|
|
585
|
+
*/
|
|
586
|
+
setAll(fields) {
|
|
587
|
+
return this.#doc._commitFields(this.#quill, MAIN_CARD_ADDR, fields);
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Set the main body from markdown (edit semantics: surviving anchors rebase),
|
|
591
|
+
* discarding the text delta — the receipt-free body write. Call
|
|
592
|
+
* `doc.revise({}, md)` for the {@link Delta} receipt.
|
|
593
|
+
* @param {string} markdown
|
|
594
|
+
* @returns {void}
|
|
595
|
+
*/
|
|
596
|
+
setBody(markdown) {
|
|
597
|
+
this.#doc.revise({}, markdown);
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Revise the richtext main-card field `name` from markdown — typed *and*
|
|
601
|
+
* anchor-preserving. Surviving anchors rebase, then the diffed result is
|
|
602
|
+
* schema-conformed (`richtext(inline)` rejects a multi-block result). Throws
|
|
603
|
+
* `UnknownField` for a name the schema does not declare. Returns the text
|
|
604
|
+
* {@link Delta}.
|
|
605
|
+
* @param {string} name
|
|
606
|
+
* @param {string} markdown
|
|
607
|
+
* @returns {import('../core/wasm.js').Delta}
|
|
608
|
+
*/
|
|
609
|
+
reviseField(name, markdown) {
|
|
610
|
+
return this.#doc._reviseField(this.#quill, name, markdown);
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Build a composable card of `kind`, typed-commit `fields` onto it, set its
|
|
614
|
+
* body from optional markdown, and place it — the fused `makeCard` + typed
|
|
615
|
+
* commit + insertion. `at` picks the position: omitted appends, a number
|
|
616
|
+
* inserts at that index (`0..=cardCount`), so a positioned typed insert is one
|
|
617
|
+
* atomic call rather than `addCard` + `moveCard`. Transactional: the card is
|
|
618
|
+
* committed in full before it joins the document, so a rejected field (throws
|
|
619
|
+
* a per-field diagnostic bundle, `UnknownField` per undeclared name) or an
|
|
620
|
+
* invalid kind/body/position leaves the document untouched.
|
|
621
|
+
* @param {string} kind
|
|
622
|
+
* @param {Record<string, unknown>} [fields]
|
|
623
|
+
* @param {string} [body]
|
|
624
|
+
* @param {number} [at] insertion index; appends when omitted
|
|
625
|
+
* @returns {void}
|
|
626
|
+
*/
|
|
627
|
+
addCard(kind, fields, body, at) {
|
|
628
|
+
return this.#doc._addCard(this.#quill, kind, fields, body, at);
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Remove the composable card at `index`, returning it (or `undefined` if the
|
|
632
|
+
* index is out of range) — the writer spelling of `Document.removeCard`.
|
|
633
|
+
* @param {number} index
|
|
634
|
+
* @returns {import('../core/wasm.js').Card | undefined}
|
|
635
|
+
*/
|
|
636
|
+
removeCard(index) {
|
|
637
|
+
return this.#doc.removeCard(index);
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* A {@link CardWriter} bound to the composable card at `index`. Index
|
|
641
|
+
* validity is checked lazily by the underlying write (it throws
|
|
642
|
+
* `IndexOutOfRange` at commit time), so constructing one never throws.
|
|
643
|
+
*
|
|
644
|
+
* The cursor is ephemeral — bind, write, discard. It holds `index`, not the
|
|
645
|
+
* card: a `removeCard`/`addCard` between binding and writing silently
|
|
646
|
+
* retargets it. For durable addressing stamp `$id` and re-resolve the index
|
|
647
|
+
* at write time.
|
|
648
|
+
* @param {number} index
|
|
649
|
+
* @returns {CardWriter}
|
|
650
|
+
*/
|
|
651
|
+
card(index) {
|
|
652
|
+
return new CardWriter(this.#quill, this.#doc, index);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* A single composable card bound to its {@link Quill} for typed writes, from
|
|
658
|
+
* {@link DocumentWriter.card}. Same `set` / `setAll` verbs as
|
|
659
|
+
* {@link DocumentWriter}, targeting the card at its bound index.
|
|
660
|
+
*/
|
|
661
|
+
export class CardWriter {
|
|
662
|
+
#quill;
|
|
663
|
+
#doc;
|
|
664
|
+
#index;
|
|
665
|
+
/**
|
|
666
|
+
* @param {Quill} quill the schema source
|
|
667
|
+
* @param {Document} doc the document to mutate, held by reference (not owned)
|
|
668
|
+
* @param {number} index the composable card's index
|
|
669
|
+
*/
|
|
670
|
+
constructor(quill, doc, index) {
|
|
671
|
+
this.#quill = quill;
|
|
672
|
+
this.#doc = doc;
|
|
673
|
+
this.#index = index;
|
|
674
|
+
}
|
|
675
|
+
/** The bound card index. */
|
|
676
|
+
get index() {
|
|
677
|
+
return this.#index;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* The bound card's `$kind` (empty string when it carries none), read through
|
|
681
|
+
* the document — mirrors core `CardWriter::kind()`. Ephemeral like the cursor
|
|
682
|
+
* itself: throws `IndexOutOfRange` if the bound index is out of range.
|
|
683
|
+
* @returns {string}
|
|
684
|
+
*/
|
|
685
|
+
get kind() {
|
|
686
|
+
return this.#doc.card(this.#index).kind;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Typed-commit one field on this card, addressed at `{ card, field }`. Throws
|
|
690
|
+
* `UnknownField` for an undeclared name and `IndexOutOfRange` if the bound
|
|
691
|
+
* index is out of range.
|
|
692
|
+
* @param {string} name
|
|
693
|
+
* @param {unknown} value
|
|
694
|
+
* @returns {void}
|
|
695
|
+
*/
|
|
696
|
+
set(name, value) {
|
|
697
|
+
return this.#doc._commitField(this.#quill, { card: this.#index, field: name }, value);
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Typed-commit several fields on this card atomically, addressed at
|
|
701
|
+
* `{ card }`. Throws a per-field diagnostic bundle on error and
|
|
702
|
+
* `IndexOutOfRange` if the bound index is out of range.
|
|
703
|
+
* @param {Record<string, unknown>} fields
|
|
704
|
+
* @returns {void}
|
|
705
|
+
*/
|
|
706
|
+
setAll(fields) {
|
|
707
|
+
return this.#doc._commitFields(this.#quill, { card: this.#index }, fields);
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Set this card's body from markdown (edit semantics), discarding the delta —
|
|
711
|
+
* the card twin of {@link DocumentWriter.setBody}.
|
|
712
|
+
* @param {string} markdown
|
|
713
|
+
* @returns {void}
|
|
714
|
+
*/
|
|
715
|
+
setBody(markdown) {
|
|
716
|
+
this.#doc.revise({ card: this.#index }, markdown);
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Revise the richtext field `name` on this card from markdown — typed *and*
|
|
720
|
+
* anchor-preserving; the card twin of {@link DocumentWriter.reviseField}.
|
|
721
|
+
* Throws `UnknownField` for an undeclared name and `IndexOutOfRange` if the
|
|
722
|
+
* bound index is out of range. Returns the text {@link Delta}.
|
|
723
|
+
* @param {string} name
|
|
724
|
+
* @param {string} markdown
|
|
725
|
+
* @returns {import('../core/wasm.js').Delta}
|
|
726
|
+
*/
|
|
727
|
+
reviseField(name, markdown) {
|
|
728
|
+
return this.#doc._reviseField(this.#quill, { card: this.#index, field: name }, markdown);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// ── `quill.writer(doc)` — the typed front door ──────────────────────────────
|
|
733
|
+
// The schema-bound writer: bind the quill's schema to a document and issue bare
|
|
734
|
+
// typed writes. Mirrors core's `quill.writer(&mut doc)` — the schema grants the
|
|
735
|
+
// typing, so the quill (not the document) is the factory. Patched onto the
|
|
736
|
+
// re-exported `Quill` prototype rather than wrapped: `Quill === CoreQuill`
|
|
737
|
+
// stays true (the identity invariant above); this only adds a method that
|
|
738
|
+
// constructs the pure-JS writer, which owns no WASM handle.
|
|
739
|
+
/**
|
|
740
|
+
* A {@link DocumentWriter} binding this quill's schema to `doc` for typed
|
|
741
|
+
* writes — the documented front door. The returned writer holds both handles by
|
|
742
|
+
* reference and owns neither, so there is nothing to `free()`. Ephemeral by
|
|
743
|
+
* convention: bind, write, discard.
|
|
744
|
+
* @this {Quill}
|
|
745
|
+
* @param {Document} doc the document to mutate, held by reference (not owned)
|
|
746
|
+
* @returns {DocumentWriter}
|
|
747
|
+
*/
|
|
748
|
+
Quill.prototype.writer = function writer(doc) {
|
|
749
|
+
return new DocumentWriter(this, doc);
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
// ── Typed-reader sugar: the schema-plane read view ──────────────────────────
|
|
753
|
+
// The read twin of the writer above. The transport `Document.get` is schema-free
|
|
754
|
+
// — a `Document` cannot say which fields are richtext, so an unknown field name
|
|
755
|
+
// reads back `undefined` rather than as the typo it is. Binding the quill's
|
|
756
|
+
// schema (`_viewGet` takes the handle, like the `commit*` verbs) lets one `get`
|
|
757
|
+
// interpret by declared type: a richtext field to markdown, a plaintext field to
|
|
758
|
+
// its literal text, every other type verbatim, and an unknown name throws
|
|
759
|
+
// `UnknownField`. A field's markdown lives here, not on the body-only
|
|
760
|
+
// `getMarkdown`. Like the writer classes these hold the caller's handles by
|
|
761
|
+
// reference, own no WASM object, and have nothing to `free()`.
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* A {@link Document} bound to its {@link Quill} for typed reads — the JS twin of
|
|
765
|
+
* Rust's `quill.view(&doc)` and the read counterpart of {@link DocumentWriter}.
|
|
766
|
+
* Reads target the main card; use {@link card} for a composable card. Holds both
|
|
767
|
+
* handles by reference and owns neither, so there is nothing to `free()`.
|
|
768
|
+
*/
|
|
769
|
+
export class DocumentView {
|
|
770
|
+
#quill;
|
|
771
|
+
#doc;
|
|
772
|
+
/**
|
|
773
|
+
* @param {Quill} quill the schema source for interpreted reads
|
|
774
|
+
* @param {Document} doc the document to read, held by reference (not owned)
|
|
775
|
+
*/
|
|
776
|
+
constructor(quill, doc) {
|
|
777
|
+
this.#quill = quill;
|
|
778
|
+
this.#doc = doc;
|
|
779
|
+
}
|
|
780
|
+
/** The bound document — the same instance passed in. */
|
|
781
|
+
get document() {
|
|
782
|
+
return this.#doc;
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Read the value at `addr`, interpreted by its declared type: a richtext field
|
|
786
|
+
* to markdown, every other type verbatim. A bare string is `Addr` shorthand
|
|
787
|
+
* for `{ field }`; an absent `addr.field` reads the body markdown. `undefined`
|
|
788
|
+
* for an absent field; throws `UnknownField` for a name the schema does not
|
|
789
|
+
* declare, `FieldRichtextDecode` for a richtext field holding an undecodable
|
|
790
|
+
* value, and `IndexOutOfRange` for a bad `addr.card`.
|
|
791
|
+
* @param {import('../core/wasm.js').Addr | string} addr
|
|
792
|
+
* @returns {unknown}
|
|
793
|
+
*/
|
|
794
|
+
get(addr) {
|
|
795
|
+
return this.#doc._viewGet(this.#quill, addr);
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* The main body's markdown — the quill-free body read (a body's type is a
|
|
799
|
+
* format fact, not a schema fact). Equivalent to `get({})`.
|
|
800
|
+
* @returns {string}
|
|
801
|
+
*/
|
|
802
|
+
getBody() {
|
|
803
|
+
return this.#doc._viewGet(this.#quill, {});
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* A {@link CardView} bound to the composable card at `index`. Index validity
|
|
807
|
+
* is checked lazily by the underlying read (it throws `IndexOutOfRange` at read
|
|
808
|
+
* time), so constructing one never throws. Ephemeral like the writer cursor —
|
|
809
|
+
* it holds `index`, not the card, so a `removeCard`/`addCard` between binding
|
|
810
|
+
* and reading silently retargets it.
|
|
811
|
+
* @param {number} index
|
|
812
|
+
* @returns {CardView}
|
|
813
|
+
*/
|
|
814
|
+
card(index) {
|
|
815
|
+
return new CardView(this.#quill, this.#doc, index);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* A single composable card bound to its {@link Quill} for typed reads, from
|
|
821
|
+
* {@link DocumentView.card}. Same `get` / `getBody` verbs as
|
|
822
|
+
* {@link DocumentView}, reading the card at its bound index.
|
|
823
|
+
*/
|
|
824
|
+
export class CardView {
|
|
825
|
+
#quill;
|
|
826
|
+
#doc;
|
|
827
|
+
#index;
|
|
828
|
+
/**
|
|
829
|
+
* @param {Quill} quill the schema source
|
|
830
|
+
* @param {Document} doc the document to read, held by reference (not owned)
|
|
831
|
+
* @param {number} index the composable card's index
|
|
832
|
+
*/
|
|
833
|
+
constructor(quill, doc, index) {
|
|
834
|
+
this.#quill = quill;
|
|
835
|
+
this.#doc = doc;
|
|
836
|
+
this.#index = index;
|
|
837
|
+
}
|
|
838
|
+
/** The bound card index. */
|
|
839
|
+
get index() {
|
|
840
|
+
return this.#index;
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* The bound card's `$kind` (empty string when it carries none), read through
|
|
844
|
+
* the document. Throws `IndexOutOfRange` if the bound index is out of range.
|
|
845
|
+
* @returns {string}
|
|
846
|
+
*/
|
|
847
|
+
get kind() {
|
|
848
|
+
return this.#doc.card(this.#index).kind;
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Read the field `name` on this card, interpreted by its declared type,
|
|
852
|
+
* addressed at `{ card, field }`. `undefined` when absent; throws
|
|
853
|
+
* `UnknownField` for an undeclared name and `IndexOutOfRange` for a bad index.
|
|
854
|
+
* @param {string} name
|
|
855
|
+
* @returns {unknown}
|
|
856
|
+
*/
|
|
857
|
+
get(name) {
|
|
858
|
+
return this.#doc._viewGet(this.#quill, { card: this.#index, field: name });
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* This card's body markdown — the card twin of {@link DocumentView.getBody}.
|
|
862
|
+
* @returns {string}
|
|
863
|
+
*/
|
|
864
|
+
getBody() {
|
|
865
|
+
return this.#doc._viewGet(this.#quill, { card: this.#index });
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// ── `quill.view(doc)` — the schema-plane read front door ─────────────────────
|
|
870
|
+
// The read twin of `quill.writer(doc)`, patched onto the same re-exported `Quill`
|
|
871
|
+
// prototype (the `Quill === CoreQuill` identity invariant holds — this only adds
|
|
872
|
+
// a method constructing the pure-JS view, which owns no WASM handle).
|
|
873
|
+
/**
|
|
874
|
+
* A {@link DocumentView} binding this quill's schema to `doc` for interpreted
|
|
875
|
+
* reads — the read front door, mirroring core's `quill.view(&doc)`. The returned
|
|
876
|
+
* view holds both handles by reference and owns neither, so there is nothing to
|
|
877
|
+
* `free()`. Ephemeral by convention: bind, read, discard.
|
|
878
|
+
* @this {Quill}
|
|
879
|
+
* @param {Document} doc the document to read, held by reference (not owned)
|
|
880
|
+
* @returns {DocumentView}
|
|
881
|
+
*/
|
|
882
|
+
Quill.prototype.view = function view(doc) {
|
|
883
|
+
return new DocumentView(this, doc);
|
|
884
|
+
};
|