@quillmark/wasm 0.92.1 → 0.94.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 +536 -0
- package/README.md +147 -62
- package/backends/pdfform/wasm.d.ts +1099 -0
- package/backends/pdfform/wasm.js +9 -0
- package/backends/pdfform/wasm_bg.js +2670 -0
- package/backends/pdfform/wasm_bg.wasm +0 -0
- package/backends/pdfform/wasm_bg.wasm.d.ts +101 -0
- package/backends/typst/wasm.d.ts +544 -94
- package/backends/typst/wasm.js +1 -1
- package/backends/typst/wasm_bg.js +893 -227
- package/backends/typst/wasm_bg.wasm +0 -0
- package/backends/typst/wasm_bg.wasm.d.ts +34 -11
- package/core/wasm.d.ts +372 -37
- package/core/wasm.js +1 -1
- package/core/wasm_bg.js +562 -59
- package/core/wasm_bg.wasm +0 -0
- package/core/wasm_bg.wasm.d.ts +20 -3
- package/package.json +4 -3
- package/runtime/runtime.d.ts +327 -21
- package/runtime/runtime.js +355 -30
package/runtime/runtime.js
CHANGED
|
@@ -52,7 +52,17 @@
|
|
|
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 corpus codec — re-exported verbatim from the core build so
|
|
62
|
+
// the runtime subpath exposes `exportMarkdown(body)` (the on-demand markdown
|
|
63
|
+
// projection that replaces the eager `bodyMarkdown`), `importMarkdown`, and the
|
|
64
|
+
// position-mapping pair (`rebase`, `mapPos`).
|
|
65
|
+
export { importMarkdown, exportMarkdown, rebase, mapPos } from '../core/wasm.js';
|
|
56
66
|
|
|
57
67
|
/**
|
|
58
68
|
* Narrow an unknown caught value to a `QuillmarkError` — the error every
|
|
@@ -79,15 +89,22 @@ export function isQuillmarkError(e) {
|
|
|
79
89
|
// and `formats`/`canvas` are the REQUIRED static capability manifest so the
|
|
80
90
|
// cheap probes (`supportedFormats`/`supportsCanvas`) ALWAYS answer without
|
|
81
91
|
// loading the binary or cloning the quill. The manifest values are verified
|
|
82
|
-
// against
|
|
83
|
-
// `SUPPORTED_FORMATS`
|
|
84
|
-
//
|
|
85
|
-
//
|
|
92
|
+
// against each backend's Rust source (`crates/backends/<id>/src/lib.rs`
|
|
93
|
+
// `SUPPORTED_FORMATS`) and pinned by the `runtime.test.js` drift-guard test,
|
|
94
|
+
// which renders once and asserts the loaded backend reports the same list.
|
|
95
|
+
// `canvas` mirrors `quillmark_core::formats_support_canvas`: true iff the
|
|
96
|
+
// format list includes a visual-page format (`svg` or `png`).
|
|
86
97
|
const DEFAULT_BACKENDS = {
|
|
87
98
|
typst: {
|
|
88
99
|
load: () => import('../backends/typst/wasm.js'),
|
|
89
100
|
formats: ['pdf', 'svg', 'png'], // crates/backends/typst/src/lib.rs SUPPORTED_FORMATS
|
|
90
|
-
canvas: true //
|
|
101
|
+
canvas: true // has svg/png → formats_support_canvas == true
|
|
102
|
+
},
|
|
103
|
+
pdfform: {
|
|
104
|
+
load: () => import('../backends/pdfform/wasm.js'),
|
|
105
|
+
// crates/backends/pdfform/src/lib.rs SUPPORTED_FORMATS == [Pdf, Svg, Png]
|
|
106
|
+
formats: ['pdf', 'svg', 'png'],
|
|
107
|
+
canvas: true // has svg/png → formats_support_canvas == true
|
|
91
108
|
}
|
|
92
109
|
};
|
|
93
110
|
|
|
@@ -210,16 +227,18 @@ export class Engine {
|
|
|
210
227
|
|
|
211
228
|
/**
|
|
212
229
|
* 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
|
|
230
|
+
* `quill` under `backendId`. On a cache miss the clone is built from `tree`
|
|
231
|
+
* — the caller's pre-await `toTree()` snapshot; the canonical handle may be
|
|
232
|
+
* freed by now — and stored in the per-backend `WeakMap` keyed on the
|
|
215
233
|
* canonical `Quill` instance, so a later call with the same instance reuses
|
|
216
234
|
* it (the hot-path fix: no re-serialize / re-copy / re-validate per call).
|
|
217
235
|
* @param {any} mod the backend build module
|
|
218
236
|
* @param {string} backendId
|
|
219
|
-
* @param {
|
|
237
|
+
* @param {object} quill the canonical instance (cache key only)
|
|
238
|
+
* @param {Map<string, Uint8Array> | null} tree pre-await snapshot; `null` on a cache hit
|
|
220
239
|
* @returns {any} the backend-memory quill clone
|
|
221
240
|
*/
|
|
222
|
-
#cachedQuillClone(mod, backendId, quill) {
|
|
241
|
+
#cachedQuillClone(mod, backendId, quill, tree) {
|
|
223
242
|
let perQuill = this.#quillClones.get(backendId);
|
|
224
243
|
if (!perQuill) {
|
|
225
244
|
perQuill = new WeakMap();
|
|
@@ -227,7 +246,7 @@ export class Engine {
|
|
|
227
246
|
}
|
|
228
247
|
let backendQuill = perQuill.get(quill);
|
|
229
248
|
if (!backendQuill) {
|
|
230
|
-
backendQuill = mod.Quill.fromTree(
|
|
249
|
+
backendQuill = mod.Quill.fromTree(tree);
|
|
231
250
|
perQuill.set(quill, backendQuill);
|
|
232
251
|
}
|
|
233
252
|
return backendQuill;
|
|
@@ -238,6 +257,13 @@ export class Engine {
|
|
|
238
257
|
* memory and run `fn` against the backend engine. Only `render`/`open` call
|
|
239
258
|
* this, so `doc` is always present.
|
|
240
259
|
*
|
|
260
|
+
* OWNERSHIP WINDOW: both caller handles are snapshotted (`doc.toJson()`, and
|
|
261
|
+
* `quill.toTree()` on a clone-cache miss) BEFORE the first await. The backend
|
|
262
|
+
* load below is a real suspension point — a multi-MB `import()` on first
|
|
263
|
+
* render — so reading the handles after it would race a caller that
|
|
264
|
+
* `free()`s them as soon as this call returns its promise ("null pointer
|
|
265
|
+
* passed to rust"). The snapshot makes that natural calling pattern correct.
|
|
266
|
+
*
|
|
241
267
|
* Clone lifetimes differ by design: the `doc` clone is TRANSIENT — freed in
|
|
242
268
|
* the `finally` of every call. The `quill` clone is CACHED per (engine,
|
|
243
269
|
* backend, canonical quill instance) and is NOT freed here; a `Quill`
|
|
@@ -251,6 +277,8 @@ export class Engine {
|
|
|
251
277
|
* @param {(ctx: { mod: any, engine: any, quill: any, doc: any }) => any} fn
|
|
252
278
|
*/
|
|
253
279
|
async #withClones(backendId, quill, doc, fn) {
|
|
280
|
+
const docJson = doc.toJson();
|
|
281
|
+
const quillTree = this.#quillClones.get(backendId)?.has(quill) ? null : quill.toTree();
|
|
254
282
|
const { mod, engine } = await this.#resolveBackend(backendId);
|
|
255
283
|
// The quill clone is cached (see #cachedQuillClone); only the per-call doc
|
|
256
284
|
// clone is transient. Bring the doc clone + `fn` under one try so the doc
|
|
@@ -258,10 +286,10 @@ export class Engine {
|
|
|
258
286
|
// rejecting a cross-version DTO). The cached quill clone is intentionally
|
|
259
287
|
// NOT freed here. `fn` MUST be synchronous — the doc clone is freed as soon
|
|
260
288
|
// as it returns, so an async `fn` would have it freed mid-flight.
|
|
261
|
-
const backendQuill = this.#cachedQuillClone(mod, backendId, quill);
|
|
289
|
+
const backendQuill = this.#cachedQuillClone(mod, backendId, quill, quillTree);
|
|
262
290
|
let backendDoc = null;
|
|
263
291
|
try {
|
|
264
|
-
backendDoc = mod.Document.fromJson(
|
|
292
|
+
backendDoc = mod.Document.fromJson(docJson);
|
|
265
293
|
return fn({ mod, engine, quill: backendQuill, doc: backendDoc });
|
|
266
294
|
} finally {
|
|
267
295
|
backendDoc?.free();
|
|
@@ -270,6 +298,8 @@ export class Engine {
|
|
|
270
298
|
|
|
271
299
|
/**
|
|
272
300
|
* Render `doc` against `quill` in one shot, returning a `RenderResult`.
|
|
301
|
+
* Both handles are read synchronously before the first await, so the caller
|
|
302
|
+
* may `free()` them as soon as this call returns.
|
|
273
303
|
* @param {Quill} quill
|
|
274
304
|
* @param {Document} doc
|
|
275
305
|
* @param {object} [options] render options (`{ format, ppi, pages, producer }`)
|
|
@@ -282,23 +312,22 @@ export class Engine {
|
|
|
282
312
|
}
|
|
283
313
|
|
|
284
314
|
/**
|
|
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.
|
|
315
|
+
* Open a live render session (canvas preview / per-page paint / `apply`).
|
|
316
|
+
* The session is self-contained (it retains what it needs for `apply`), so
|
|
317
|
+
* the transient quill and document clones are freed before this returns;
|
|
318
|
+
* the caller owns the returned session and must `.free()` it. The `quill`
|
|
319
|
+
* and `doc` handles are read synchronously before the first await, so the
|
|
320
|
+
* caller may `free()` them as soon as this call returns.
|
|
292
321
|
* @param {Quill} quill
|
|
293
322
|
* @param {Document} doc
|
|
294
|
-
* @returns {Promise<
|
|
323
|
+
* @returns {Promise<LiveSession>}
|
|
295
324
|
*/
|
|
296
325
|
async open(quill, doc) {
|
|
297
326
|
return this.#withClones(
|
|
298
327
|
quill.backendId,
|
|
299
328
|
quill,
|
|
300
329
|
doc,
|
|
301
|
-
({ engine, quill: q, doc: d }) => new
|
|
330
|
+
({ mod, engine, quill: q, doc: d }) => new LiveSession(engine.open(q, d), mod)
|
|
302
331
|
);
|
|
303
332
|
}
|
|
304
333
|
|
|
@@ -317,9 +346,13 @@ export class Engine {
|
|
|
317
346
|
}
|
|
318
347
|
|
|
319
348
|
/**
|
|
320
|
-
* Whether `quill`'s
|
|
321
|
-
*
|
|
322
|
-
* `
|
|
349
|
+
* Whether `quill`'s BACKEND can paint sessions to a canvas — a pre-session
|
|
350
|
+
* ESTIMATE, not a fact about any particular compile. Same ALWAYS-free probe
|
|
351
|
+
* as `supportedFormats`: answered from the descriptor's required `canvas`
|
|
352
|
+
* manifest, no load and no clone. A specific compile can still refuse to
|
|
353
|
+
* paint (e.g. a 0-page document), so this can answer `true` while the
|
|
354
|
+
* resulting `LiveSession.supportsCanvas` answers `false` — gate mounting a
|
|
355
|
+
* canvas UI on this, gate the actual `paint` call on the session's getter.
|
|
323
356
|
* @param {Quill} quill
|
|
324
357
|
* @returns {Promise<boolean>}
|
|
325
358
|
*/
|
|
@@ -330,15 +363,49 @@ export class Engine {
|
|
|
330
363
|
}
|
|
331
364
|
|
|
332
365
|
/**
|
|
333
|
-
* Thin wrapper over a backend's
|
|
334
|
-
*
|
|
366
|
+
* Thin wrapper over a backend's live render session. Reads serve the current
|
|
367
|
+
* compile; `apply(doc)` recompiles in place (transactional: on throw, reads
|
|
368
|
+
* keep serving the last-good compile). The quill/document clones it was
|
|
369
|
+
* opened from have already been freed — the session retains what `apply`
|
|
370
|
+
* needs.
|
|
371
|
+
*
|
|
372
|
+
* Geometry reads (`regions`, `positionAt`, `locate`) resolve against the
|
|
373
|
+
* current compile; anchoring a caret or selection across edits is the editor's
|
|
374
|
+
* job (its own transaction mapping) — re-read geometry after each committed
|
|
375
|
+
* `apply`.
|
|
376
|
+
*
|
|
377
|
+
* `paint` writes a COMPLETE page raster — all content visible, no caller-side
|
|
378
|
+
* compositing — for every backend that supports canvas (Typst rasterizes
|
|
379
|
+
* natively; pdfform rasterizes its pre-flattened page). See `runtime.d.ts`.
|
|
335
380
|
*/
|
|
336
|
-
export class
|
|
337
|
-
/**
|
|
338
|
-
|
|
381
|
+
export class LiveSession {
|
|
382
|
+
/**
|
|
383
|
+
* @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)
|
|
384
|
+
* @param {{ Document: { fromJson(json: string): any } }} mod the session's backend build, used to materialize `apply` documents in its linear memory
|
|
385
|
+
*/
|
|
386
|
+
constructor(inner, mod) {
|
|
339
387
|
this.#inner = inner;
|
|
388
|
+
this.#mod = mod;
|
|
340
389
|
}
|
|
341
390
|
#inner;
|
|
391
|
+
#mod;
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Recompile the session against `doc` — the edit verb of a live preview.
|
|
395
|
+
* Transactional: on throw every read keeps serving the last-good compile.
|
|
396
|
+
* On success reads serve the new compile; repaint `dirtyPages ∩ visible`.
|
|
397
|
+
* @param {Document} doc
|
|
398
|
+
* @returns {import('./runtime.d.ts').ChangeSet}
|
|
399
|
+
*/
|
|
400
|
+
apply(doc) {
|
|
401
|
+
let backendDoc = null;
|
|
402
|
+
try {
|
|
403
|
+
backendDoc = this.#mod.Document.fromJson(doc.toJson());
|
|
404
|
+
return this.#inner.apply(backendDoc);
|
|
405
|
+
} finally {
|
|
406
|
+
backendDoc?.free();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
342
409
|
|
|
343
410
|
get pageCount() {
|
|
344
411
|
return this.#inner.pageCount;
|
|
@@ -346,6 +413,16 @@ export class RenderSession {
|
|
|
346
413
|
get backendId() {
|
|
347
414
|
return this.#inner.backendId;
|
|
348
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* `true` iff `paint`/`pageSize` will succeed for THIS compile — the
|
|
418
|
+
* authoritative answer, derived from the session's canvas seam, so it can
|
|
419
|
+
* never disagree with what `paint` actually does. This can be `false` even
|
|
420
|
+
* when `Engine.supportsCanvas` answered `true` for the same `quill` (that
|
|
421
|
+
* probe is a pre-session backend estimate; e.g. a canvas-capable backend
|
|
422
|
+
* compiled to a 0-page document has nothing to paint). Re-check this getter
|
|
423
|
+
* after `open()` rather than relying on the engine hint alone.
|
|
424
|
+
* @returns {boolean}
|
|
425
|
+
*/
|
|
349
426
|
get supportsCanvas() {
|
|
350
427
|
return this.#inner.supportsCanvas;
|
|
351
428
|
}
|
|
@@ -358,12 +435,75 @@ export class RenderSession {
|
|
|
358
435
|
return this.#inner.render(options ?? undefined);
|
|
359
436
|
}
|
|
360
437
|
|
|
438
|
+
/**
|
|
439
|
+
* Schema-field geometry for this compiled session — one region per
|
|
440
|
+
* schema-bound field, keyed on its quill schema field path. A session-level
|
|
441
|
+
* query (no render); read it to place field overlays / cross-navigation over
|
|
442
|
+
* a `paint`-ed canvas.
|
|
443
|
+
* @returns {import('./runtime.d.ts').FieldRegion[]}
|
|
444
|
+
*/
|
|
445
|
+
regions() {
|
|
446
|
+
return this.#inner.regions();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* The whole-field highlight boxes for `field` — one union rect per page,
|
|
451
|
+
* over the field's `span`-bearing content segments. Owns the union
|
|
452
|
+
* `regions()` leaves derived (span-filter + per-page union), so a "highlight
|
|
453
|
+
* the focused field" consumer stops reimplementing it. Content only: a field
|
|
454
|
+
* placed solely as a scalar reference or a bound widget returns `[]` — its
|
|
455
|
+
* box is a single `regions()` rect.
|
|
456
|
+
* @param {string} field
|
|
457
|
+
* @returns {import('./runtime.d.ts').FieldRegion[]}
|
|
458
|
+
*/
|
|
459
|
+
fieldBoxes(field) {
|
|
460
|
+
return this.#inner.fieldBoxes(field);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* The schema field whose content is under a point on `page` — the forward
|
|
465
|
+
* (click → field) direction, resolving *every* placement, not just the first
|
|
466
|
+
* that `regions` enumerates. `x`/`y` are PDF points with a bottom-left origin
|
|
467
|
+
* (the `FieldRegion.rect` space). See `runtime.d.ts` for the click-to-point
|
|
468
|
+
* inverse transform.
|
|
469
|
+
* @param {number} page
|
|
470
|
+
* @param {number} x
|
|
471
|
+
* @param {number} y
|
|
472
|
+
* @returns {string | undefined}
|
|
473
|
+
*/
|
|
474
|
+
fieldAt(page, x, y) {
|
|
475
|
+
return this.#inner.fieldAt(page, x, y);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* @param {number} page
|
|
480
|
+
* @param {number} x
|
|
481
|
+
* @param {number} y
|
|
482
|
+
* @returns {import('./runtime.d.ts').CorpusHit | undefined}
|
|
483
|
+
*/
|
|
484
|
+
positionAt(page, x, y) {
|
|
485
|
+
return this.#inner.positionAt(page, x, y);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* @param {string} field
|
|
490
|
+
* @param {number} pos
|
|
491
|
+
* @returns {import('./runtime.d.ts').FieldRegion | undefined}
|
|
492
|
+
*/
|
|
493
|
+
locate(field, pos) {
|
|
494
|
+
return this.#inner.locate(field, pos);
|
|
495
|
+
}
|
|
496
|
+
|
|
361
497
|
/** @param {number} page */
|
|
362
498
|
pageSize(page) {
|
|
363
499
|
return this.#inner.pageSize(page);
|
|
364
500
|
}
|
|
365
501
|
|
|
366
502
|
/**
|
|
503
|
+
* Paint `page` into a 2D canvas context. The painted raster is COMPLETE —
|
|
504
|
+
* all page content visible, no caller-side compositing — for both the Typst
|
|
505
|
+
* and pdfform backends. See `runtime.d.ts` for the DPR/clamp math and the
|
|
506
|
+
* region-overlay coordinate transform.
|
|
367
507
|
* @param {CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D} ctx
|
|
368
508
|
* @param {number} page
|
|
369
509
|
* @param {object} [options]
|
|
@@ -376,3 +516,188 @@ export class RenderSession {
|
|
|
376
516
|
this.#inner.free();
|
|
377
517
|
}
|
|
378
518
|
}
|
|
519
|
+
|
|
520
|
+
// ── Typed-writer sugar: bind the quill once ─────────────────────────────────
|
|
521
|
+
// Rust exposes `quill.writer(&mut doc)` so a caller issues bare `set` / `set_all`
|
|
522
|
+
// without threading the schema per write. The WASM `commit*` verbs can't borrow
|
|
523
|
+
// like that — a `Document` carries only a `$quill` REFERENCE, not the resolved
|
|
524
|
+
// schema, so each `commit*` method takes the `quill` handle as its first
|
|
525
|
+
// argument. These pure-JS classes restore the Rust ergonomics: bind `quill` +
|
|
526
|
+
// `doc` once, then issue `set` / `setAll` / `card(i).set`.
|
|
527
|
+
//
|
|
528
|
+
// They hold JS references to the caller's EXISTING handles — no WASM object of
|
|
529
|
+
// their own, no `free()` burden, no second owner of either handle — and every
|
|
530
|
+
// write delegates straight to the underlying `commit*` verb: a schema field is
|
|
531
|
+
// typed-committed (coerced to canonical form, mismatch throws now), and a name
|
|
532
|
+
// the schema does not declare throws `UnknownField` rather than falling to the
|
|
533
|
+
// opaque store — on the typed path an undeclared name is a typo. Opaque storage
|
|
534
|
+
// stays available through the raw `Document.setField` / `setCardField` verbs.
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* A {@link Document} bound to its {@link Quill} for typed writes — the JS twin
|
|
538
|
+
* of Rust's `quill.writer(&mut doc)`. Writes target the main card; use
|
|
539
|
+
* {@link card} for a composable card. Holds both handles by reference and owns
|
|
540
|
+
* neither, so there is nothing to `free()`.
|
|
541
|
+
*/
|
|
542
|
+
export class DocumentWriter {
|
|
543
|
+
#quill;
|
|
544
|
+
#doc;
|
|
545
|
+
/**
|
|
546
|
+
* @param {Quill} quill the schema source for typed commits
|
|
547
|
+
* @param {Document} doc the document to mutate, held by reference (not owned)
|
|
548
|
+
*/
|
|
549
|
+
constructor(quill, doc) {
|
|
550
|
+
this.#quill = quill;
|
|
551
|
+
this.#doc = doc;
|
|
552
|
+
}
|
|
553
|
+
/** The bound document — the same instance passed in, mutated in place. */
|
|
554
|
+
get document() {
|
|
555
|
+
return this.#doc;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Typed-commit one main-card field (strict coerce, mismatch throws now).
|
|
559
|
+
* Throws `UnknownField` for a name the schema does not declare. See
|
|
560
|
+
* `Document.commitField`.
|
|
561
|
+
* @param {string} name
|
|
562
|
+
* @param {unknown} value
|
|
563
|
+
* @returns {void}
|
|
564
|
+
*/
|
|
565
|
+
set(name, value) {
|
|
566
|
+
return this.#doc.commitField(this.#quill, name, value);
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Typed-commit several main-card fields atomically — nothing is applied on
|
|
570
|
+
* error (throws a per-field diagnostic bundle, including an `UnknownField`
|
|
571
|
+
* for each undeclared name). See `Document.commitFields`.
|
|
572
|
+
* @param {Record<string, unknown>} fields
|
|
573
|
+
* @returns {void}
|
|
574
|
+
*/
|
|
575
|
+
setAll(fields) {
|
|
576
|
+
return this.#doc.commitFields(this.#quill, fields);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Set the main body from markdown (edit semantics: surviving anchors rebase),
|
|
580
|
+
* discarding the text delta — the receipt-free body write. Call
|
|
581
|
+
* `doc.revise({}, md)` for the {@link Delta} receipt (the corpus-lane
|
|
582
|
+
* spelling). Markdown in, no corpus or receipt in sight.
|
|
583
|
+
* @param {string} markdown
|
|
584
|
+
* @returns {void}
|
|
585
|
+
*/
|
|
586
|
+
setBody(markdown) {
|
|
587
|
+
this.#doc.revise({}, markdown);
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Build a composable card of `kind`, typed-commit `fields` onto it, set its
|
|
591
|
+
* body from optional markdown, and append it — the fused `makeCard` + typed
|
|
592
|
+
* commit + `pushCard`. Transactional: the card is committed in full before it
|
|
593
|
+
* joins the document, so a rejected field (throws a per-field diagnostic
|
|
594
|
+
* bundle, `UnknownField` per undeclared name) or an invalid kind/body leaves
|
|
595
|
+
* the document untouched. See `Document.addCard`.
|
|
596
|
+
* @param {string} kind
|
|
597
|
+
* @param {Record<string, unknown>} [fields]
|
|
598
|
+
* @param {string} [body]
|
|
599
|
+
* @returns {void}
|
|
600
|
+
*/
|
|
601
|
+
addCard(kind, fields, body) {
|
|
602
|
+
return this.#doc.addCard(this.#quill, kind, fields, body);
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Remove the composable card at `index`, returning it (or `undefined` if the
|
|
606
|
+
* index is out of range) — the tier-1 spelling of `Document.removeCard`.
|
|
607
|
+
* @param {number} index
|
|
608
|
+
* @returns {import('../core/wasm.js').Card | undefined}
|
|
609
|
+
*/
|
|
610
|
+
removeCard(index) {
|
|
611
|
+
return this.#doc.removeCard(index);
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* A {@link CardWriter} bound to the composable card at `index`. Index
|
|
615
|
+
* validity is checked lazily by the underlying write (it throws
|
|
616
|
+
* `IndexOutOfRange` at commit time), so constructing one never throws.
|
|
617
|
+
*
|
|
618
|
+
* The cursor is ephemeral — bind, write, discard. It holds `index`, not the
|
|
619
|
+
* card: a `removeCard`/`addCard` between binding and writing silently
|
|
620
|
+
* retargets it. For durable addressing stamp `$id` and re-resolve the index
|
|
621
|
+
* at write time.
|
|
622
|
+
* @param {number} index
|
|
623
|
+
* @returns {CardWriter}
|
|
624
|
+
*/
|
|
625
|
+
card(index) {
|
|
626
|
+
return new CardWriter(this.#quill, this.#doc, index);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* A single composable card bound to its {@link Quill} for typed writes, from
|
|
632
|
+
* {@link DocumentWriter.card}. Same `set` / `setAll` verbs as
|
|
633
|
+
* {@link DocumentWriter}, targeting the card at its bound index.
|
|
634
|
+
*/
|
|
635
|
+
export class CardWriter {
|
|
636
|
+
#quill;
|
|
637
|
+
#doc;
|
|
638
|
+
#index;
|
|
639
|
+
/**
|
|
640
|
+
* @param {Quill} quill the schema source
|
|
641
|
+
* @param {Document} doc the document to mutate, held by reference (not owned)
|
|
642
|
+
* @param {number} index the composable card's index
|
|
643
|
+
*/
|
|
644
|
+
constructor(quill, doc, index) {
|
|
645
|
+
this.#quill = quill;
|
|
646
|
+
this.#doc = doc;
|
|
647
|
+
this.#index = index;
|
|
648
|
+
}
|
|
649
|
+
/** The bound card index. */
|
|
650
|
+
get index() {
|
|
651
|
+
return this.#index;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Typed-commit one field on this card, per `Document.commitCardField`.
|
|
655
|
+
* Throws `UnknownField` for an undeclared name and `IndexOutOfRange` if the
|
|
656
|
+
* bound index is out of range.
|
|
657
|
+
* @param {string} name
|
|
658
|
+
* @param {unknown} value
|
|
659
|
+
* @returns {void}
|
|
660
|
+
*/
|
|
661
|
+
set(name, value) {
|
|
662
|
+
return this.#doc.commitCardField(this.#quill, this.#index, name, value);
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Typed-commit several fields on this card atomically, per
|
|
666
|
+
* `Document.commitCardFields`. Throws a per-field diagnostic bundle on error
|
|
667
|
+
* and `IndexOutOfRange` if the bound index is out of range.
|
|
668
|
+
* @param {Record<string, unknown>} fields
|
|
669
|
+
* @returns {void}
|
|
670
|
+
*/
|
|
671
|
+
setAll(fields) {
|
|
672
|
+
return this.#doc.commitCardFields(this.#quill, this.#index, fields);
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Set this card's body from markdown (edit semantics), discarding the delta —
|
|
676
|
+
* the card twin of {@link DocumentWriter.setBody}.
|
|
677
|
+
* @param {string} markdown
|
|
678
|
+
* @returns {void}
|
|
679
|
+
*/
|
|
680
|
+
setBody(markdown) {
|
|
681
|
+
this.#doc.revise({ card: this.#index }, markdown);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// ── `quill.writer(doc)` — the typed front door ──────────────────────────────
|
|
686
|
+
// The tier-1 default: bind the quill's schema to a document and issue bare
|
|
687
|
+
// typed writes. Mirrors core's `quill.writer(&mut doc)` — the schema grants the
|
|
688
|
+
// typing, so the quill (not the document) is the factory. Patched onto the
|
|
689
|
+
// re-exported `Quill` prototype rather than wrapped: `Quill === CoreQuill`
|
|
690
|
+
// stays true (the identity invariant above); this only adds a method that
|
|
691
|
+
// constructs the pure-JS writer, which owns no WASM handle.
|
|
692
|
+
/**
|
|
693
|
+
* A {@link DocumentWriter} binding this quill's schema to `doc` for typed
|
|
694
|
+
* writes — the documented front door. The returned writer holds both handles by
|
|
695
|
+
* reference and owns neither, so there is nothing to `free()`. Ephemeral by
|
|
696
|
+
* convention: bind, write, discard.
|
|
697
|
+
* @this {Quill}
|
|
698
|
+
* @param {Document} doc the document to mutate, held by reference (not owned)
|
|
699
|
+
* @returns {DocumentWriter}
|
|
700
|
+
*/
|
|
701
|
+
Quill.prototype.writer = function writer(doc) {
|
|
702
|
+
return new DocumentWriter(this, doc);
|
|
703
|
+
};
|