@quillmark/wasm 0.124.0 → 0.126.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.
@@ -142,6 +142,7 @@ export type {
142
142
  Revised,
143
143
  DocumentRevised,
144
144
  AnnotatedMarkdown,
145
+ DocumentAnchor,
145
146
  Imported,
146
147
  Assoc,
147
148
  IslandOp,
@@ -332,14 +333,30 @@ export interface FieldRegion {
332
333
  rect: [number, number, number, number];
333
334
  /**
334
335
  * The content slice this box covers: USV `[start, end)` into the field's
335
- * `Content` for content ink (one segment), absent for a scalar reference
336
- * site or widget. Consumers key segment highlights on it;
336
+ * `Content` for content ink (one segment), absent for a scalar's or a
337
+ * date's ink, a claim or a widget. Consumers key segment highlights on it;
337
338
  * {@link LiveSession.fieldBoxes} unions same-page segments for the
338
339
  * whole-field box.
339
340
  */
340
341
  span?: [number, number];
341
342
  }
342
343
 
344
+ /**
345
+ * One address a compiled document fills, and the region address a click
346
+ * reaches it through. A report, not a gate: a field a quill never prints is
347
+ * legitimately unreachable.
348
+ */
349
+ export interface FieldCoverage {
350
+ /** The field's canonical `DocPath` address, as {@link FieldRegion.field}. */
351
+ field: string;
352
+ /**
353
+ * The `DocPath` of the region reaching `field`: `field` itself, or the
354
+ * nearest enclosing address a region carries (a row claim reaching its
355
+ * cells, a whole array its elements). Absent where no click reaches it.
356
+ */
357
+ coveredBy?: string;
358
+ }
359
+
343
360
  /** Result of one render. */
344
361
  export interface RenderResult {
345
362
  artifacts: Artifact[];
@@ -415,14 +432,21 @@ export declare class Engine {
415
432
  * soon as this call returns.
416
433
  *
417
434
  * {@link RenderResult.warnings} carries every `quill.validate(doc)` warning
418
- * but `validation::declined_construct`, which the compile raises as
419
- * `backend::declined_construct`, then the compile's own. The load's (parse
435
+ * but `backend::declined_construct`, then the compile's own, a decline among
436
+ * them. The load's (parse
420
437
  * and `conform::*`) stay on `doc.warnings`, and a revise's on its receipt.
421
438
  * A {@link LiveSession} carries the compile's alone: read
422
439
  * `quill.validate(doc)` beside it.
423
440
  *
424
441
  * `today` reads as on {@link open}. An `options` key outside
425
442
  * {@link RenderOptions} rejects, `today` among them.
443
+ *
444
+ * A compile that traps (out of memory, a panic) rejects as
445
+ * `runtime::backend_trapped`, the `WebAssembly.RuntimeError` on `cause`, as
446
+ * does a trap in {@link LiveSession}'s `update` / `render` / `paint`, and a
447
+ * `RangeError` or Firefox's `InternalError` for stack or memory exhaustion,
448
+ * which engines raise for a stack overflow inside wasm. The build's state is then undefined: discard the
449
+ * realm.
426
450
  */
427
451
  render(quill: Quill, doc: Document, options?: RenderOptions, today?: string): Promise<RenderResult>;
428
452
 
@@ -494,22 +518,27 @@ export declare class LiveSession {
494
518
  * Empty for backends that place no schema fields.
495
519
  *
496
520
  * `field` is **not** unique: a content field surfaces its **first placement**
497
- * as one {@link FieldRegion} per page that placement touches, a scalar
498
- * referenced at several plate sites surfaces each site, and tracked content
499
- * plus a `field:`-bound widget yields both, widget first. Group by `field`.
500
- * Later placements of one content value are not enumerated; {@link fieldAt}
501
- * still resolves clicks on them.
521
+ * as one {@link FieldRegion} per page that placement touches, each claim on
522
+ * a field surfaces its own, and tracked content plus a `field:`-bound widget
523
+ * yields both, widget first. Group by `field`. Later placements of one value
524
+ * are not enumerated; {@link fieldAt} still resolves clicks on them.
502
525
  */
503
526
  regions(): FieldRegion[];
504
527
  /**
505
528
  * The whole-field highlight boxes for `field` (a canonical `DocPath` address,
506
529
  * as {@link regions} keys): one union rect per page over the field's
507
530
  * `span`-bearing content segments, the union {@link regions} leaves derived.
508
- * **Content only**: a field placed solely as a scalar reference or a bound
509
- * widget carries no `span` and returns `[]`, its box being a single
531
+ * **Content only**: a field placed solely as a scalar's ink, a claim or a
532
+ * bound widget carries no `span` and returns `[]`, its box being a single
510
533
  * {@link regions} rect. Reflects the current compile.
511
534
  */
512
535
  fieldBoxes(field: string): FieldRegion[];
536
+ /**
537
+ * Every address the current compile fills that a reader could want to
538
+ * click — each non-blank declared leaf, every roster member held or not —
539
+ * with the region reaching it. Reflects the current compile.
540
+ */
541
+ coverage(): FieldCoverage[];
513
542
  /**
514
543
  * The schema field whose content is under a point on `page`: the canonical
515
544
  * `DocPath` address to focus in the editor, or `undefined` off any field's
@@ -616,15 +645,18 @@ export declare class DocumentWriter {
616
645
  readonly document: Document;
617
646
  /**
618
647
  * Typed-commit one main-card field (strict coerce, mismatch throws now).
619
- * Throws `UnknownField` for a name the schema does not declare.
648
+ * Throws `UnknownField` for a name the schema does not declare. Returns a
649
+ * `parse::dropped_construct` warning per construct a `richtext` markdown
650
+ * string in the value dropped, at the string's path.
620
651
  */
621
- set(name: string, value: unknown): void;
652
+ set(name: string, value: unknown): Diagnostic[];
622
653
  /**
623
654
  * Typed-commit several main-card fields atomically: nothing is applied on
624
655
  * error (throws a {@link QuillmarkError} carrying one diagnostic per
625
656
  * offending field, including an `UnknownField` for each undeclared name).
657
+ * Returns `set`'s warnings for every field, in batch order.
626
658
  */
627
- setAll(fields: Record<string, unknown>): void;
659
+ setAll(fields: Record<string, unknown>): Diagnostic[];
628
660
  /**
629
661
  * Revise the main body from markdown; anchors rebase. Returns the
630
662
  * `Revised` receipt, its warnings at `main.body`: a body carries no field
@@ -635,12 +667,12 @@ export declare class DocumentWriter {
635
667
  /**
636
668
  * Revise the content main-card field `name` from authored text: typed *and*
637
669
  * anchor-preserving. Surviving anchors rebase, then the diffed result is
638
- * schema-conformed (`richtext(inline)` rejects a multi-block result). Throws
670
+ * schema-conformed (a one-paragraph `richtext` rejects a multi-block result). Throws
639
671
  * `UnknownField` for a name the schema does not declare. Returns the
640
672
  * `Revised` receipt, its warnings at the field.
641
673
  *
642
674
  * The codec comes from the declared type: `richtext` diffs markdown, while
643
- * `plaintext` diffs the literal text and never imports markdown, so a
675
+ * `text` diffs the literal text and never imports markdown, so a
644
676
  * byte-identical revise of a value carrying escapes is a byte no-op.
645
677
  */
646
678
  reviseField(name: string, text: string): Revised;
@@ -660,8 +692,8 @@ export declare class DocumentWriter {
660
692
  * body from optional markdown, and place it. `at` omitted appends, a number
661
693
  * inserts at that index. Transactional: a rejected field (throwing a per-field
662
694
  * diagnostic bundle) or an invalid kind, body, or position leaves the document
663
- * untouched. Returns the body import's `parse::dropped_construct` warnings,
664
- * anchored at the placed card's body.
695
+ * untouched. Returns the fields' `parse::dropped_construct` warnings, then
696
+ * the body import's, anchored under the placed card.
665
697
  */
666
698
  addCard(kind: string, fields?: Record<string, unknown>, body?: string, at?: number): Diagnostic[];
667
699
  /** Remove the composable card at `index`, returning it (or `undefined`). */
@@ -691,8 +723,8 @@ export declare class CardWriter {
691
723
  * index.
692
724
  */
693
725
  readonly kind: string;
694
- set(name: string, value: unknown): void;
695
- setAll(fields: Record<string, unknown>): void;
726
+ set(name: string, value: unknown): Diagnostic[];
727
+ setAll(fields: Record<string, unknown>): Diagnostic[];
696
728
  /**
697
729
  * Revise this card's body from markdown (edit semantics), returning the
698
730
  * `Revised` receipt, its warnings at the card's body.
@@ -709,7 +741,7 @@ export declare class CardWriter {
709
741
  * A `Document` bound to its `Quill` for interpreted reads, from
710
742
  * {@link Quill.reader}: the read twin of {@link DocumentWriter}. One `get` reads
711
743
  * each field by its declared type — a richtext field to its markdown projection,
712
- * a plaintext field to its literal text, every other type verbatim.
744
+ * a text field to its literal text, every other type verbatim.
713
745
  *
714
746
  * The schema authority is the point: unlike the quill-free `Document.getStored`,
715
747
  * an undeclared name throws `UnknownField` rather than reading back `undefined`,
@@ -161,6 +161,53 @@ function quillmarkError(code, message, hint) {
161
161
  return err;
162
162
  }
163
163
 
164
+ /**
165
+ * `err` as `runtime::backend_trapped` when it is a `WebAssembly.RuntimeError`,
166
+ * or a `RangeError` (V8, JavaScriptCore) or `InternalError` (SpiderMonkey) for
167
+ * stack or memory exhaustion, which those engines raise for native stack
168
+ * overflow inside wasm; else unchanged.
169
+ * @param {unknown} err
170
+ * @returns {unknown}
171
+ */
172
+ function backendTrap(err) {
173
+ const exhausted =
174
+ (err instanceof RangeError || (err instanceof Error && err.name === 'InternalError')) &&
175
+ /Maximum call stack|stack overflow|too much recursion|out of memory/i.test(err.message);
176
+ if (!(err instanceof WebAssembly.RuntimeError) && !exhausted) return err;
177
+ return Object.assign(
178
+ quillmarkError(
179
+ 'runtime::backend_trapped',
180
+ `The backend build trapped (${err.message}); its state is now undefined.`,
181
+ 'A render ran out of memory or stack, or panicked. Discard the realm before rendering again: terminate the Worker or restart the process. For untrusted input, cap the document size.'
182
+ ),
183
+ { cause: err }
184
+ );
185
+ }
186
+
187
+ /**
188
+ * Run `fn` on a transient clone of `docJson` in `mod`'s memory, then free it. A
189
+ * call that throws can leave the clone borrowed, so its `free()` throws too, and
190
+ * the error in flight is the one that propagates.
191
+ * @param {any} mod
192
+ * @param {string} docJson
193
+ * @param {(doc: any) => any} fn
194
+ * @returns {any}
195
+ */
196
+ function withDocClone(mod, docJson, fn) {
197
+ const backendDoc = mod.Document.fromStored(docJson);
198
+ let result;
199
+ try {
200
+ result = fn(backendDoc);
201
+ } catch (err) {
202
+ try {
203
+ backendDoc.free();
204
+ } catch {}
205
+ throw err;
206
+ }
207
+ backendDoc.free();
208
+ return result;
209
+ }
210
+
164
211
  /**
165
212
  * Throw on options that are not a plain object, or on a key of them, or of a
166
213
  * null-prototype object they inherit from, outside `known`. The JS twin of the
@@ -568,16 +615,15 @@ export class Engine {
568
615
  ? null
569
616
  : quill.toTree();
570
617
  const { mod, engine } = await this.#resolveBackend(backendId);
571
- // The doc clone and `fn` share one try so the clone is freed even if a
572
- // later step throws; the cached quill clone is intentionally not freed.
573
- // `fn` MUST be synchronous: an async one would run against a freed clone.
574
- const backendQuill = this.#cachedQuillClone(mod, backendId, quill, quillTree);
575
- let backendDoc = null;
618
+ // The cached quill clone is intentionally not freed. `fn` MUST be
619
+ // synchronous: an async one would run against a freed doc clone.
576
620
  try {
577
- backendDoc = mod.Document.fromStored(docJson);
578
- return fn({ mod, engine, quill: backendQuill, doc: backendDoc });
579
- } finally {
580
- backendDoc?.free();
621
+ const backendQuill = this.#cachedQuillClone(mod, backendId, quill, quillTree);
622
+ return withDocClone(mod, docJson, (backendDoc) =>
623
+ fn({ mod, engine, quill: backendQuill, doc: backendDoc })
624
+ );
625
+ } catch (err) {
626
+ throw backendTrap(err);
581
627
  }
582
628
  }
583
629
 
@@ -654,12 +700,11 @@ export class LiveSession {
654
700
  */
655
701
  update(doc) {
656
702
  requireLocalDoc(doc, 'session.update(doc)');
657
- let backendDoc = null;
703
+ const docJson = doc.toStored();
658
704
  try {
659
- backendDoc = this.#mod.Document.fromStored(doc.toStored());
660
- return this.#inner.update(backendDoc);
661
- } finally {
662
- backendDoc?.free();
705
+ return withDocClone(this.#mod, docJson, (backendDoc) => this.#inner.update(backendDoc));
706
+ } catch (err) {
707
+ throw backendTrap(err);
663
708
  }
664
709
  }
665
710
 
@@ -675,7 +720,11 @@ export class LiveSession {
675
720
 
676
721
  /** @param {object} [options] */
677
722
  render(options) {
678
- return this.#inner.render(options ?? undefined);
723
+ try {
724
+ return this.#inner.render(options ?? undefined);
725
+ } catch (err) {
726
+ throw backendTrap(err);
727
+ }
679
728
  }
680
729
 
681
730
  /**
@@ -693,6 +742,13 @@ export class LiveSession {
693
742
  return this.#inner.fieldBoxes(field);
694
743
  }
695
744
 
745
+ /**
746
+ * @returns {import('./runtime.d.ts').FieldCoverage[]}
747
+ */
748
+ coverage() {
749
+ return this.#inner.coverage();
750
+ }
751
+
696
752
  /**
697
753
  * @param {number} page
698
754
  * @param {number} x
@@ -735,7 +791,11 @@ export class LiveSession {
735
791
  * @param {number} scale
736
792
  */
737
793
  paint(ctx, page, scale) {
738
- this.#inner.paint(ctx, page, scale);
794
+ try {
795
+ this.#inner.paint(ctx, page, scale);
796
+ } catch (err) {
797
+ throw backendTrap(err);
798
+ }
739
799
  }
740
800
 
741
801
  free() {
@@ -767,14 +827,14 @@ export class DocumentWriter {
767
827
  /**
768
828
  * @param {string} name
769
829
  * @param {unknown} value
770
- * @returns {void}
830
+ * @returns {import('../core/wasm.js').Diagnostic[]}
771
831
  */
772
832
  set(name, value) {
773
833
  return this.#doc._commitField(this.#quill, name, value);
774
834
  }
775
835
  /**
776
836
  * @param {Record<string, unknown>} fields
777
- * @returns {void}
837
+ * @returns {import('../core/wasm.js').Diagnostic[]}
778
838
  */
779
839
  setAll(fields) {
780
840
  return this.#doc._commitFields(this.#quill, MAIN_CARD_ADDR, fields);
@@ -855,14 +915,14 @@ export class CardWriter {
855
915
  /**
856
916
  * @param {string} name
857
917
  * @param {unknown} value
858
- * @returns {void}
918
+ * @returns {import('../core/wasm.js').Diagnostic[]}
859
919
  */
860
920
  set(name, value) {
861
921
  return this.#doc._commitField(this.#quill, { card: this.#index, field: name }, value);
862
922
  }
863
923
  /**
864
924
  * @param {Record<string, unknown>} fields
865
- * @returns {void}
925
+ * @returns {import('../core/wasm.js').Diagnostic[]}
866
926
  */
867
927
  setAll(fields) {
868
928
  return this.#doc._commitFields(this.#quill, { card: this.#index }, fields);