@quillmark/wasm 0.130.0 → 0.132.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/render/wasm.d.ts CHANGED
@@ -80,8 +80,7 @@ export type ContentLineKind =
80
80
  | { kind: "para" }
81
81
  | { kind: "heading"; attrs: { level: number } }
82
82
  | { kind: "code"; attrs?: { lang?: string } }
83
- | { kind: "rule" }
84
- | { kind: "term" };
83
+ | { kind: "rule" };
85
84
 
86
85
  /** An ancestor block a line nests inside, outermost first. Closed like
87
86
  * `ContentLine.kind`.
@@ -110,14 +109,7 @@ export type ContentContainer =
110
109
  attrs: { ordered: boolean; start: number; ordinal: number; checked?: boolean };
111
110
  instance?: number;
112
111
  }
113
- | { container: "quote"; instance?: number }
114
- | { container: "description"; instance?: number }
115
- | { container: "element"; attrs: ElementAttrs; instance?: number };
116
-
117
- /** A carrier element's name and attributes, one bag: `$name` is the part of
118
- * its `qm-<name>` tag after the prefix, and every other key an attribute as
119
- * written. */
120
- export type ElementAttrs = { $name: string; [attr: string]: string };
112
+ | { container: "quote"; instance?: number };
121
113
 
122
114
  /** A mark over char range `[start, end)` into `Content.text`. `type` is a
123
115
  * closed set, so `type === "link"` narrows `attrs` to `{ url: string }` with no
@@ -132,7 +124,8 @@ export type ContentMark = { start: number; end: number } & ContentMarkKind;
132
124
  export type ContentMarkKind =
133
125
  | { type: "strong" | "emph" | "underline" | "strike" | "code" }
134
126
  | { type: "link"; attrs: { url: string } }
135
- | { type: "anchor"; attrs: { id: string } };
127
+ | { type: "anchor"; attrs: { id: string } }
128
+ | { type: "font_family"; attrs: { name: string } };
136
129
 
137
130
  /** A cell in a `TableProps`. `marks` rides the prose `ContentMark` shape, but
138
131
  * each mark's `start`/`end` are USV offsets into this cell's `text`, not into
@@ -144,12 +137,17 @@ export interface TableCell {
144
137
  align?: "left" | "center" | "right";
145
138
  /** The cell's vertical alignment; absent is the quill's. */
146
139
  valign?: "top" | "middle" | "bottom";
140
+ /** The columns the cell spans, from 2; absent is one. Each cell it covers
141
+ * holds its slot as an empty placeholder. */
142
+ colspan?: number;
143
+ /** The rows the cell spans, from 2; absent is one. */
144
+ rowspan?: number;
147
145
  }
148
146
 
149
- /** `props` of a `type: "table"` island: a pipe table normalized to one column
150
- * count that `header`, every row of `rows`, `aligns` and `widths` all share.
151
- * A layout key at its default is absent; one holding a value outside its type
152
- * stays as written and reads as absent. */
147
+ /** `props` of a `type: "table"` island: a rectangular grid normalized to one
148
+ * column count that `header`, every row of `rows`, `aligns` and `widths` all
149
+ * share. A layout key at its default is absent; one holding a value outside
150
+ * its type stays as written and reads as absent. */
153
151
  export interface TableProps {
154
152
  header: TableCell[];
155
153
  rows: TableCell[][];
@@ -161,8 +159,9 @@ export interface TableProps {
161
159
  widths?: (number | null)[];
162
160
  /** The table's placement; absent is the quill's. */
163
161
  align?: "left" | "center" | "right";
164
- /** `true` draws `header` as the first body row; absent is a header row. */
165
- headless?: true;
162
+ /** How many rows from the top of `header` then `rows` are header rows;
163
+ * `0` draws `header` as the first body row, and absent is one. */
164
+ head?: number;
166
165
  }
167
166
 
168
167
  /** `props` of a `type: "image"` island. Stores and round-trips; no backend
@@ -300,7 +299,9 @@ export type Assoc = "before" | "after";
300
299
  /**
301
300
  * A mark edit in final-text coordinates (post-delta, post-line-op). `add` /
302
301
  * `remove` are a `ContentMark` under an op, so a held mark spreads in whole;
303
- * `removeAnchor` drops one identity anchor by id. An `add` of an `anchor`
302
+ * `removeAnchor` drops one identity anchor by id. An `add` of a `link` or a
303
+ * `font_family` replaces the one under its range, and a mark added across a
304
+ * line break reads back as one mark per line. An `add` of an `anchor`
304
305
  * requires a non-empty `id` not already live in the field; a collision or the
305
306
  * empty id throws.
306
307
  */
@@ -471,6 +472,19 @@ export interface Resolved {
471
472
 
472
473
 
473
474
 
475
+ /** One face a session sets text in, with what an editor needs for an
476
+ * `@font-face` rule: the family, the CSS weight and style, and the font file.
477
+ * `index` is the face's place in `data` where the file is a collection. */
478
+ export interface FontFace {
479
+ family: string;
480
+ weight: number;
481
+ style: "normal" | "italic" | "oblique";
482
+ index: number;
483
+ data: Uint8Array;
484
+ }
485
+
486
+
487
+
474
488
  /** UI layout hints for a single field. Display order is not a hint: key order
475
489
  * in the schema's `fields`/`properties` objects is the ordering contract. */
476
490
  export interface QuillFieldUi {
@@ -752,6 +766,11 @@ export interface RenderOptions {
752
766
  * document-space even when `pages` selects a subset.
753
767
  */
754
768
  regions?: boolean;
769
+ /**
770
+ * The PDF document title, over the plate's own and then the quill's
771
+ * `description`. A blank title counts as absent; ignored for PNG and SVG.
772
+ */
773
+ title?: string;
755
774
  }
756
775
 
757
776
  export interface RenderResult {
@@ -813,8 +832,12 @@ export class Document {
813
832
  card(index: number): Card;
814
833
  clone(): Document;
815
834
  /**
816
- * Storage version this build writes via [`toStored`](Document::to_stored). The
817
- * tag advances only when the wire format changes, not on every release.
835
+ * The newest storage version this build writes via
836
+ * [`toStored`](Document::to_stored): a document spelling a font mark, a
837
+ * `table` element, a merged table cell or a table head 0.130 cannot draw
838
+ * carries it, every other the tag before it, which a 0.130 build reads.
839
+ * The tag advances only when the wire format changes, not on every
840
+ * release.
818
841
  */
819
842
  static currentStorageVersion(): string;
820
843
  /**
@@ -1056,7 +1079,18 @@ export class Document {
1056
1079
  export class Engine {
1057
1080
  free(): void;
1058
1081
  [Symbol.dispose](): void;
1059
- constructor();
1082
+ /**
1083
+ * The text faces a `font_family` mark may set in a session of `quill`:
1084
+ * the host library's, in library order.
1085
+ */
1086
+ fontFaces(quill: Quill): FontFace[];
1087
+ /**
1088
+ * An engine whose sessions also set text in `fonts`, the host's font
1089
+ * library: each a `.ttf`, `.otf`, `.ttc` or `.otc` file, parsed once
1090
+ * here. A file in another format throws `engine::font_unsupported`, and
1091
+ * one holding no readable face `engine::font_unreadable`.
1092
+ */
1093
+ constructor(fonts: Uint8Array[] | undefined);
1060
1094
  /**
1061
1095
  * Open a live render session for `doc` against `quill`'s backend, on the
1062
1096
  * render date `today` (`YYYY-MM-DD`, default the local date).
@@ -1232,6 +1266,12 @@ export class Quill {
1232
1266
  * parse or its `$quill` is not this quill's bare name.
1233
1267
  */
1234
1268
  exampleDocument(): Document | undefined;
1269
+ /**
1270
+ * Read a `.quill` bundle, the zip archive holding one quill's file tree.
1271
+ * Pure, as [`fromTree`](Self::from_tree) is. A bundle outside the format
1272
+ * throws one `quill::bundle_*` diagnostic.
1273
+ */
1274
+ static fromBundle(bytes: Uint8Array): Quill;
1235
1275
  /**
1236
1276
  * Build a quill from a file tree. Pure: the declared backend is resolved
1237
1277
  * later, at render time. Accepts a `Map<string, Uint8Array>` or a plain
@@ -1274,6 +1314,11 @@ export class Quill {
1274
1314
  * `$kind: main` card of [`seedDocument`](Self::seed_document) alone.
1275
1315
  */
1276
1316
  seedMain(): Card;
1317
+ /**
1318
+ * This quill's file tree as a `.quill` bundle, the bytes
1319
+ * [`fromBundle`](Self::from_bundle) reads.
1320
+ */
1321
+ toBundle(): Uint8Array;
1277
1322
  /**
1278
1323
  * Flatten this quill back into its canonical file tree, the inverse of
1279
1324
  * [`fromTree`](Self::from_tree). Keys are `"/"`-joined relative paths.
@@ -1475,7 +1520,8 @@ export interface InitOutput {
1475
1520
  readonly document_toMarkdown: (a: number, b: number) => void;
1476
1521
  readonly document_toStored: (a: number, b: number) => void;
1477
1522
  readonly document_warnings: (a: number, b: number) => void;
1478
- readonly engine_new: () => number;
1523
+ readonly engine_fontFaces: (a: number, b: number, c: number) => void;
1524
+ readonly engine_new: (a: number, b: number) => void;
1479
1525
  readonly engine_open: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
1480
1526
  readonly engine_render: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
1481
1527
  readonly engine_supportedFormats: (a: number, b: number, c: number) => void;
@@ -1507,6 +1553,7 @@ export interface InitOutput {
1507
1553
  readonly quill_conform: (a: number, b: number, c: number) => void;
1508
1554
  readonly quill_emptyDocument: (a: number) => number;
1509
1555
  readonly quill_exampleDocument: (a: number, b: number) => void;
1556
+ readonly quill_fromBundle: (a: number, b: number, c: number) => void;
1510
1557
  readonly quill_fromTree: (a: number, b: number) => void;
1511
1558
  readonly quill_metadata: (a: number, b: number) => void;
1512
1559
  readonly quill_parse: (a: number, b: number, c: number, d: number) => void;
@@ -1514,6 +1561,7 @@ export interface InitOutput {
1514
1561
  readonly quill_seedCard: (a: number, b: number, c: number, d: number, e: number) => void;
1515
1562
  readonly quill_seedDocument: (a: number) => number;
1516
1563
  readonly quill_seedMain: (a: number, b: number) => void;
1564
+ readonly quill_toBundle: (a: number, b: number) => void;
1517
1565
  readonly quill_toTree: (a: number) => number;
1518
1566
  readonly quill_validate: (a: number, b: number, c: number) => void;
1519
1567
  readonly quill_warnings: (a: number, b: number) => void;
package/render/wasm.js CHANGED
@@ -441,8 +441,12 @@ export class Document {
441
441
  return Document.__wrap(ret);
442
442
  }
443
443
  /**
444
- * Storage version this build writes via [`toStored`](Document::to_stored). The
445
- * tag advances only when the wire format changes, not on every release.
444
+ * The newest storage version this build writes via
445
+ * [`toStored`](Document::to_stored): a document spelling a font mark, a
446
+ * `table` element, a merged table cell or a table head 0.130 cannot draw
447
+ * carries it, every other the tag before it, which a 0.130 build reads.
448
+ * The tag advances only when the wire format changes, not on every
449
+ * release.
446
450
  * @returns {string}
447
451
  */
448
452
  static currentStorageVersion() {
@@ -1203,11 +1207,51 @@ export class Engine {
1203
1207
  const ptr = this.__destroy_into_raw();
1204
1208
  wasm.__wbg_engine_free(ptr, 0);
1205
1209
  }
1206
- constructor() {
1207
- const ret = wasm.engine_new();
1208
- this.__wbg_ptr = ret;
1209
- EngineFinalization.register(this, this.__wbg_ptr, this);
1210
- return this;
1210
+ /**
1211
+ * The text faces a `font_family` mark may set in a session of `quill`:
1212
+ * the host library's, in library order.
1213
+ * @param {Quill} quill
1214
+ * @returns {FontFace[]}
1215
+ */
1216
+ fontFaces(quill) {
1217
+ try {
1218
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
1219
+ _assertClass(quill, Quill);
1220
+ wasm.engine_fontFaces(retptr, this.__wbg_ptr, quill.__wbg_ptr);
1221
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
1222
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
1223
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
1224
+ if (r2) {
1225
+ throw takeObject(r1);
1226
+ }
1227
+ return takeObject(r0);
1228
+ } finally {
1229
+ wasm.__wbindgen_add_to_stack_pointer(16);
1230
+ }
1231
+ }
1232
+ /**
1233
+ * An engine whose sessions also set text in `fonts`, the host's font
1234
+ * library: each a `.ttf`, `.otf`, `.ttc` or `.otc` file, parsed once
1235
+ * here. A file in another format throws `engine::font_unsupported`, and
1236
+ * one holding no readable face `engine::font_unreadable`.
1237
+ * @param {Uint8Array[] | undefined} fonts
1238
+ */
1239
+ constructor(fonts) {
1240
+ try {
1241
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
1242
+ wasm.engine_new(retptr, addHeapObject(fonts));
1243
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
1244
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
1245
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
1246
+ if (r2) {
1247
+ throw takeObject(r1);
1248
+ }
1249
+ this.__wbg_ptr = r0;
1250
+ EngineFinalization.register(this, this.__wbg_ptr, this);
1251
+ return this;
1252
+ } finally {
1253
+ wasm.__wbindgen_add_to_stack_pointer(16);
1254
+ }
1211
1255
  }
1212
1256
  /**
1213
1257
  * Open a live render session for `doc` against `quill`'s backend, on the
@@ -1788,6 +1832,30 @@ export class Quill {
1788
1832
  wasm.__wbindgen_add_to_stack_pointer(16);
1789
1833
  }
1790
1834
  }
1835
+ /**
1836
+ * Read a `.quill` bundle, the zip archive holding one quill's file tree.
1837
+ * Pure, as [`fromTree`](Self::from_tree) is. A bundle outside the format
1838
+ * throws one `quill::bundle_*` diagnostic.
1839
+ * @param {Uint8Array} bytes
1840
+ * @returns {Quill}
1841
+ */
1842
+ static fromBundle(bytes) {
1843
+ try {
1844
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
1845
+ const ptr0 = passArray8ToWasm0(bytes, wasm.__wbindgen_export);
1846
+ const len0 = WASM_VECTOR_LEN;
1847
+ wasm.quill_fromBundle(retptr, ptr0, len0);
1848
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
1849
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
1850
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
1851
+ if (r2) {
1852
+ throw takeObject(r1);
1853
+ }
1854
+ return Quill.__wrap(r0);
1855
+ } finally {
1856
+ wasm.__wbindgen_add_to_stack_pointer(16);
1857
+ }
1858
+ }
1791
1859
  /**
1792
1860
  * Build a quill from a file tree. Pure: the declared backend is resolved
1793
1861
  * later, at render time. Accepts a `Map<string, Uint8Array>` or a plain
@@ -1944,6 +2012,29 @@ export class Quill {
1944
2012
  wasm.__wbindgen_add_to_stack_pointer(16);
1945
2013
  }
1946
2014
  }
2015
+ /**
2016
+ * This quill's file tree as a `.quill` bundle, the bytes
2017
+ * [`fromBundle`](Self::from_bundle) reads.
2018
+ * @returns {Uint8Array}
2019
+ */
2020
+ toBundle() {
2021
+ try {
2022
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
2023
+ wasm.quill_toBundle(retptr, this.__wbg_ptr);
2024
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
2025
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
2026
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
2027
+ var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true);
2028
+ if (r3) {
2029
+ throw takeObject(r2);
2030
+ }
2031
+ var v1 = getArrayU8FromWasm0(r0, r1).slice();
2032
+ wasm.__wbindgen_export4(r0, r1 * 1, 1);
2033
+ return v1;
2034
+ } finally {
2035
+ wasm.__wbindgen_add_to_stack_pointer(16);
2036
+ }
2037
+ }
1947
2038
  /**
1948
2039
  * Flatten this quill back into its canonical file tree, the inverse of
1949
2040
  * [`fromTree`](Self::from_tree). Keys are `"/"`-joined relative paths.
@@ -2452,6 +2543,9 @@ function __wbg_get_imports() {
2452
2543
  const ret = Reflect.getPrototypeOf(getObject(arg0));
2453
2544
  return addHeapObject(ret);
2454
2545
  }, arguments); },
2546
+ __wbg_getRandomValues_436a51d0629d84e1: function() { return handleError(function (arg0, arg1) {
2547
+ globalThis.crypto.getRandomValues(getArrayU8FromWasm0(arg0, arg1));
2548
+ }, arguments); },
2455
2549
  __wbg_get_658f6698067d9515: function() { return handleError(function (arg0, arg1) {
2456
2550
  const ret = Reflect.get(getObject(arg0), getObject(arg1));
2457
2551
  return addHeapObject(ret);
@@ -2603,6 +2697,10 @@ function __wbg_get_imports() {
2603
2697
  __wbg_prototypesetcall_bc27214492979395: function(arg0, arg1, arg2) {
2604
2698
  Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), getObject(arg2));
2605
2699
  },
2700
+ __wbg_push_2baf45db356cf468: function(arg0, arg1) {
2701
+ const ret = getObject(arg0).push(getObject(arg1));
2702
+ return ret;
2703
+ },
2606
2704
  __wbg_putImageData_11570c06086c9e05: function() { return handleError(function (arg0, arg1, arg2, arg3) {
2607
2705
  getObject(arg0).putImageData(getObject(arg1), arg2, arg3);
2608
2706
  }, arguments); },
@@ -2849,6 +2947,13 @@ function isLikeNone(x) {
2849
2947
  return x === undefined || x === null;
2850
2948
  }
2851
2949
 
2950
+ function passArray8ToWasm0(arg, malloc) {
2951
+ const ptr = malloc(arg.length * 1, 1) >>> 0;
2952
+ getUint8ArrayMemory0().set(arg, ptr / 1);
2953
+ WASM_VECTOR_LEN = arg.length;
2954
+ return ptr;
2955
+ }
2956
+
2852
2957
  function passStringToWasm0(arg, malloc, realloc) {
2853
2958
  if (realloc === undefined) {
2854
2959
  const buf = cachedTextEncoder.encode(arg);
Binary file
@@ -52,7 +52,8 @@ export const document_toAnnotatedMarkdown: (a: number, b: number) => void;
52
52
  export const document_toMarkdown: (a: number, b: number) => void;
53
53
  export const document_toStored: (a: number, b: number) => void;
54
54
  export const document_warnings: (a: number, b: number) => void;
55
- export const engine_new: () => number;
55
+ export const engine_fontFaces: (a: number, b: number, c: number) => void;
56
+ export const engine_new: (a: number, b: number) => void;
56
57
  export const engine_open: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
57
58
  export const engine_render: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
58
59
  export const engine_supportedFormats: (a: number, b: number, c: number) => void;
@@ -84,6 +85,7 @@ export const quill_blueprint: (a: number, b: number) => void;
84
85
  export const quill_conform: (a: number, b: number, c: number) => void;
85
86
  export const quill_emptyDocument: (a: number) => number;
86
87
  export const quill_exampleDocument: (a: number, b: number) => void;
88
+ export const quill_fromBundle: (a: number, b: number, c: number) => void;
87
89
  export const quill_fromTree: (a: number, b: number) => void;
88
90
  export const quill_metadata: (a: number, b: number) => void;
89
91
  export const quill_parse: (a: number, b: number, c: number, d: number) => void;
@@ -91,6 +93,7 @@ export const quill_schema: (a: number, b: number) => void;
91
93
  export const quill_seedCard: (a: number, b: number, c: number, d: number, e: number) => void;
92
94
  export const quill_seedDocument: (a: number) => number;
93
95
  export const quill_seedMain: (a: number, b: number) => void;
96
+ export const quill_toBundle: (a: number, b: number) => void;
94
97
  export const quill_toTree: (a: number) => number;
95
98
  export const quill_validate: (a: number, b: number, c: number) => void;
96
99
  export const quill_warnings: (a: number, b: number) => void;
@@ -267,6 +267,12 @@ export interface RenderOptions {
267
267
  * consumers without a live session. Defaults to `false`.
268
268
  */
269
269
  regions?: boolean;
270
+ /**
271
+ * The PDF document title, over the plate's own `set document(title: ..)`
272
+ * and then the quill's `description`. A blank title counts as absent;
273
+ * ignored by PNG and SVG.
274
+ */
275
+ title?: string;
270
276
  }
271
277
 
272
278
  /**
@@ -415,6 +421,31 @@ export interface EngineOptions {
415
421
  * `"typst"` and `"acroform"` to the bundled render build.
416
422
  */
417
423
  backends?: Record<string, BackendDescriptor>;
424
+
425
+ /**
426
+ * The host's font library: each a `.ttf`, `.otf`, `.ttc` or `.otc` file,
427
+ * whose faces set only text a `font_family` mark sets. The bytes are
428
+ * copied at construction, cross into the render build once, when it loads,
429
+ * and are parsed there once. A file it refuses rejects every verb that
430
+ * needs that build, the first and each later one, with the same error:
431
+ * `engine::font_unsupported` for another format, `engine::font_unreadable`
432
+ * for a file holding no readable face. A value that is not an array of
433
+ * `Uint8Array` throws at construction.
434
+ */
435
+ fonts?: Uint8Array[];
436
+ }
437
+
438
+ /**
439
+ * One face a session sets text in, with what an editor needs for an
440
+ * `@font-face` rule: the family, the CSS weight and style, and the font file.
441
+ * `index` is the face's place in `data` where the file is a collection.
442
+ */
443
+ export interface FontFace {
444
+ family: string;
445
+ weight: number;
446
+ style: 'normal' | 'italic' | 'oblique';
447
+ index: number;
448
+ data: Uint8Array;
418
449
  }
419
450
 
420
451
  /**
@@ -485,6 +516,15 @@ export declare class Engine {
485
516
  * backend binary or cloning the quill. Async for API stability.
486
517
  */
487
518
  supportedFormats(quill: Quill): Promise<OutputFormat[]>;
519
+
520
+ /**
521
+ * The text faces a `font_family` mark may set in a session of `quill`: the
522
+ * host library's, in the order of `fonts`. A quill's own fonts and the
523
+ * embedded fallback set its plate, never a mark. Loads the backend and
524
+ * clones the quill as `open` does; a backend that reads no fonts answers
525
+ * `[]`.
526
+ */
527
+ fontFaces(quill: Quill): Promise<FontFace[]>;
488
528
  }
489
529
 
490
530
  /**
@@ -331,10 +331,9 @@ function requireLocalQuill(quill, method) {
331
331
  // `runtime.test.js` § "container run boundaries" stamps a pair and re-imports
332
332
  // it, where `Content::normalize` re-mints against the Rust predicate. A tag the
333
333
  // table omits welds with nothing, so `tests/known_names_drift.rs` pins that
334
- // every container has an entry. An entry of `'*'` welds on the whole bag: an
335
- // element's attributes are open, its name among them.
334
+ // every container has an entry.
336
335
 
337
- const WELD_KEYS = { list_item: ['ordered'], quote: [], description: [], element: '*' };
336
+ const WELD_KEYS = { list_item: ['ordered'], quote: [] };
338
337
 
339
338
  /**
340
339
  * @param {import('../core/wasm.js').ContentContainer} a
@@ -347,20 +346,7 @@ function weldsWith(a, b) {
347
346
  // `hasOwn`, so a tag colliding with an `Object.prototype` member answers
348
347
  // `false` rather than reaching a function.
349
348
  if (!Object.hasOwn(WELD_KEYS, a.container)) return false;
350
- const keys = WELD_KEYS[a.container];
351
- if (keys === '*') return sameBag(a.attrs, b.attrs);
352
- return keys.every((k) => a.attrs?.[k] === b.attrs?.[k]);
353
- }
354
-
355
- /**
356
- * @param {Record<string, unknown> | undefined} a
357
- * @param {Record<string, unknown> | undefined} b
358
- * @returns {boolean}
359
- */
360
- function sameBag(a, b) {
361
- const [x, y] = [a ?? {}, b ?? {}];
362
- const keys = Object.keys(x);
363
- return keys.length === Object.keys(y).length && keys.every((k) => Object.hasOwn(y, k) && x[k] === y[k]);
349
+ return WELD_KEYS[a.container].every((k) => a.attrs?.[k] === b.attrs?.[k]);
364
350
  }
365
351
 
366
352
  /**
@@ -470,10 +456,22 @@ export class Engine {
470
456
  */
471
457
  /** load → Promise<build module>, memoized so each build loads once. */
472
458
  #modules = new Map();
473
- /** load → that build's engine instance (the WASM backend registry). */
459
+ /**
460
+ * load → `{ engine }`, that build's engine instance (the WASM backend
461
+ * registry), or `{ refused }`, the error its construction threw. A refusal
462
+ * is the font library's, which every retry would parse and refuse again.
463
+ */
474
464
  #engines = new Map();
475
465
  /** backendId → descriptor `{ load, formats }`. */
476
466
  #loaders;
467
+ /**
468
+ * The host's font library, copied at construction and handed to each
469
+ * build's engine as it loads; `null` once every build has had it.
470
+ * @type {Uint8Array[] | null}
471
+ */
472
+ #fonts;
473
+ /** How many distinct builds the loaders name. */
474
+ #builds;
477
475
  /**
478
476
  * load → WeakMap<canonical Quill, build-memory clone>, caching the
479
477
  * expensive materialization. WeakMap so dropping the canonical quill makes
@@ -486,10 +484,15 @@ export class Engine {
486
484
  * `load` must resolve to a READY module: a registrant shipping its own
487
485
  * `--target web` build instantiates inside the thunk, and more than one
488
486
  * `Engine` may call it, so memoize (the built-ins do, at module scope).
489
- * @param {{ backends?: Record<string, { load: () => Promise<unknown>, formats: string[] }> }} [options]
487
+ * @param {{ backends?: Record<string, { load: () => Promise<unknown>, formats: string[] }>, fonts?: Uint8Array[] }} [options]
490
488
  */
491
489
  constructor(options) {
492
- rejectUnknownKeys(options, ['backends'], 'Engine options');
490
+ rejectUnknownKeys(options, ['backends', 'fonts'], 'Engine options');
491
+ const fonts = options?.fonts ?? [];
492
+ if (!Array.isArray(fonts) || !fonts.every((font) => font instanceof Uint8Array)) {
493
+ throw new Error('Engine: `fonts` must be an array of Uint8Array.');
494
+ }
495
+ this.#fonts = fonts.map((font) => font.slice());
493
496
  const merged = { ...DEFAULT_BACKENDS, ...(options?.backends ?? {}) };
494
497
  /** @type {Record<string, { load: () => Promise<unknown>, formats: string[] }>} */
495
498
  const loaders = {};
@@ -497,6 +500,7 @@ export class Engine {
497
500
  loaders[id] = validateBackend(id, entry);
498
501
  }
499
502
  this.#loaders = loaders;
503
+ this.#builds = new Set(Object.values(loaders).map((descriptor) => descriptor.load)).size;
500
504
  }
501
505
 
502
506
  /**
@@ -554,12 +558,18 @@ export class Engine {
554
558
  }
555
559
  const mod = await modPromise;
556
560
 
557
- let engine = this.#engines.get(load);
558
- if (!engine) {
559
- engine = new mod.Engine();
560
- this.#engines.set(load, engine);
561
+ let built = this.#engines.get(load);
562
+ if (!built) {
563
+ try {
564
+ built = { engine: new mod.Engine(this.#fonts) };
565
+ } catch (refused) {
566
+ built = { refused };
567
+ }
568
+ this.#engines.set(load, built);
569
+ if (this.#engines.size === this.#builds) this.#fonts = null;
561
570
  }
562
- return { mod, engine };
571
+ if ('refused' in built) throw built.refused;
572
+ return { mod, engine: built.engine };
563
573
  }
564
574
 
565
575
  /**
@@ -630,7 +640,7 @@ export class Engine {
630
640
  /**
631
641
  * @param {Quill} quill
632
642
  * @param {Document} doc
633
- * @param {object} [options] render options (`{ format, ppi, pages, regions }`)
643
+ * @param {object} [options] render options (`{ format, ppi, pages, regions, title }`)
634
644
  * @param {string} [today] the render date, `YYYY-MM-DD`
635
645
  * @returns {Promise<import('./runtime.js').RenderResult>}
636
646
  */
@@ -658,6 +668,36 @@ export class Engine {
658
668
  );
659
669
  }
660
670
 
671
+ /**
672
+ * Run `fn` against `quill`'s build-memory clone, `quill` snapshotted before
673
+ * the first await as `#withClones` snapshots it.
674
+ * @param {string} method the caller's name, for the rejection message
675
+ * @param {Quill} quill
676
+ * @param {(ctx: { engine: any, quill: any }) => any} fn
677
+ */
678
+ async #withQuill(method, quill, fn) {
679
+ const backendId = this.#backendOf(quill, method);
680
+ const quillTree = this.#quillClones.get(this.#descriptorFor(backendId).load)?.has(quill)
681
+ ? null
682
+ : quill.toTree();
683
+ const { mod, engine } = await this.#resolveBackend(backendId);
684
+ try {
685
+ return fn({ engine, quill: this.#cachedQuillClone(mod, backendId, quill, quillTree) });
686
+ } catch (err) {
687
+ throw backendTrap(err);
688
+ }
689
+ }
690
+
691
+ /**
692
+ * @param {Quill} quill
693
+ * @returns {Promise<import('./runtime.d.ts').FontFace[]>}
694
+ */
695
+ async fontFaces(quill) {
696
+ return this.#withQuill('engine.fontFaces(quill)', quill, ({ engine, quill: q }) =>
697
+ typeof engine.fontFaces === 'function' ? engine.fontFaces(q) : []
698
+ );
699
+ }
700
+
661
701
  /**
662
702
  * @param {Quill} quill
663
703
  * @returns {Promise<void>}