@quillmark/wasm 0.97.0 → 0.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +303 -0
- package/LICENSE +13 -0
- package/README.md +63 -60
- package/backends/pdfform/wasm.d.ts +300 -143
- package/backends/pdfform/wasm_bg.js +101 -102
- package/backends/pdfform/wasm_bg.wasm +0 -0
- package/backends/typst/wasm.d.ts +300 -143
- package/backends/typst/wasm_bg.js +101 -102
- package/backends/typst/wasm_bg.wasm +0 -0
- package/core/wasm.d.ts +127 -93
- package/core/wasm_bg.js +84 -82
- package/core/wasm_bg.wasm +0 -0
- package/package.json +2 -2
- package/runtime/runtime.d.ts +157 -79
- package/runtime/runtime.js +375 -95
package/README.md
CHANGED
|
@@ -10,16 +10,16 @@ Use Quillmark in browsers/Node.js with explicit in-memory trees (`Map<string, Ui
|
|
|
10
10
|
|
|
11
11
|
The package exposes **one import surface**:
|
|
12
12
|
|
|
13
|
-
- `@quillmark/wasm` (the root)
|
|
13
|
+
- `@quillmark/wasm` (the root), the **canonical API**: `Quill`, `Document`, and
|
|
14
14
|
an `Engine` that renders them.
|
|
15
15
|
|
|
16
16
|
`Quill` and `Document` are re-exported verbatim from the internal Typst-less
|
|
17
17
|
core build, so editor/validation code (`Quill.fromTree`,
|
|
18
|
-
`Document.fromMarkdown`) loads only that small core binary
|
|
18
|
+
`Document.fromMarkdown`) loads only that small core binary: no backend is
|
|
19
19
|
loaded until you render. The `Engine` hides everything else: each backend
|
|
20
20
|
(`typst`, `pdfform`) is a separate, private WASM binary with its own linear
|
|
21
21
|
memory, lazily loaded on the first render. The Engine clones a `Quill` /
|
|
22
|
-
`Document` into the backend's memory as data and frees the clones
|
|
22
|
+
`Document` into the backend's memory as data and frees the clones: you never
|
|
23
23
|
hold a backend object or cross a memory boundary yourself.
|
|
24
24
|
|
|
25
25
|
## Build
|
|
@@ -28,8 +28,8 @@ hold a backend object or cross a memory boundary yourself.
|
|
|
28
28
|
bash scripts/build-wasm.sh
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
The script builds three variants
|
|
32
|
-
(default features), and the Typst-free pdfform backend (`pdfform` feature)
|
|
31
|
+
The script builds three variants: the core (no backend), the Typst backend
|
|
32
|
+
(default features), and the Typst-free pdfform backend (`pdfform` feature):
|
|
33
33
|
each with `--target bundler` and `--weak-refs` enabled (see
|
|
34
34
|
[Lifecycle](#lifecycle)).
|
|
35
35
|
|
|
@@ -66,7 +66,7 @@ const result = await engine.render(quill, parsed, { format: "pdf" });
|
|
|
66
66
|
|
|
67
67
|
### `new Engine(options?)`
|
|
68
68
|
Create the render dispatcher. Routes each quill to its backend by
|
|
69
|
-
`quill.backendId`, lazily loads that backend binary, and renders
|
|
69
|
+
`quill.backendId`, lazily loads that backend binary, and renders: cloning the
|
|
70
70
|
quill/document into the backend's memory and freeing the clones internally.
|
|
71
71
|
`render`, `open`, `supportedFormats`, and `supportsCanvas` are **async** (the
|
|
72
72
|
first call may load a backend). Pass `{ backends }` to register or override
|
|
@@ -78,16 +78,16 @@ the backend id.
|
|
|
78
78
|
|
|
79
79
|
**Capability probes are always free.** `supportedFormats` and `supportsCanvas`
|
|
80
80
|
depend only on `quill.backendId`, and answer from the descriptor's required
|
|
81
|
-
`formats`/`canvas` manifest
|
|
81
|
+
`formats`/`canvas` manifest: never loading the multi-MB backend binary and
|
|
82
82
|
never cloning the quill. Use them as non-failing pre-render probes.
|
|
83
83
|
|
|
84
84
|
### `Quill.fromTree(tree)`
|
|
85
|
-
Build + validate a `Quill` from an in-memory tree. Pure
|
|
85
|
+
Build + validate a `Quill` from an in-memory tree. Pure: the declared backend
|
|
86
86
|
is resolved at render time, not here. Loads no backend binary.
|
|
87
87
|
|
|
88
88
|
### `new Document(quillRef)`
|
|
89
89
|
A blank document: a main card carrying only `$quill`, an empty body, and no
|
|
90
|
-
composable cards
|
|
90
|
+
composable cards: the programmatic blank canvas. Absent fields resolve at
|
|
91
91
|
render time (schema `default`, else type-empty zero), so nothing the caller
|
|
92
92
|
did not set reaches the output. Build it up with `storeFields` / `insertCard`.
|
|
93
93
|
For an example-filled starter use `quill.seedDocument()`. Throws on an
|
|
@@ -96,19 +96,19 @@ invalid quill reference.
|
|
|
96
96
|
### `Document.fromMarkdown(markdown)`
|
|
97
97
|
Parse markdown to a parsed document. Throws a JS `Error` (with `.diagnostics`
|
|
98
98
|
attached, see [Errors](#errors)) on any parse failure, including a missing
|
|
99
|
-
root `$quill` metadata line, malformed YAML, and inputs over the 10
|
|
99
|
+
root `$quill` metadata line, malformed YAML, and inputs over the 10 MiB
|
|
100
100
|
`parse::input_too_large` limit.
|
|
101
101
|
|
|
102
102
|
### `doc.toMarkdown()`
|
|
103
103
|
Emit canonical Quillmark Markdown. Type-fidelity round-trip safe:
|
|
104
104
|
`Document.fromMarkdown(doc.toMarkdown())` returns a document equal to `doc`
|
|
105
105
|
under [`doc.equals`](#docequalsother). The output is **not** guaranteed
|
|
106
|
-
byte-equal to the original source
|
|
106
|
+
byte-equal to the original source: YAML quoting, key ordering, and
|
|
107
107
|
whitespace are normalised. Use `equals` (not string comparison) to test
|
|
108
108
|
semantic equality.
|
|
109
109
|
|
|
110
110
|
### `doc.toJson()`
|
|
111
|
-
Serialize the document to a versioned storage DTO
|
|
111
|
+
Serialize the document to a versioned storage DTO: a JSON **string**
|
|
112
112
|
carrying a `schema` version. Use this (not `toMarkdown`) to persist a
|
|
113
113
|
document across a process restart or crate upgrade: the wire format is
|
|
114
114
|
frozen per `schema` version, whereas Markdown syntax evolves. Parse-time
|
|
@@ -116,11 +116,11 @@ frozen per `schema` version, whereas Markdown syntax evolves. Parse-time
|
|
|
116
116
|
|
|
117
117
|
The string is produced inside the module by `serde_json`; the JS `JSON`
|
|
118
118
|
global is not involved. It is standard JSON text, so callers may
|
|
119
|
-
`JSON.parse` it to inspect it
|
|
119
|
+
`JSON.parse` it to inspect it, but it is intended as an opaque blob you
|
|
120
120
|
persist and hand back.
|
|
121
121
|
|
|
122
122
|
`toJson()` is **deterministic**: a `Document` that is `equals` to another
|
|
123
|
-
serializes to a byte-identical string
|
|
123
|
+
serializes to a byte-identical string: across repeated calls, and across
|
|
124
124
|
any crate upgrade that keeps the same `schema` version (every release does until
|
|
125
125
|
the `Document` model changes; see [Storage compatibility](#storage-compatibility-across-versions)).
|
|
126
126
|
Field order is fixed and object key order is preserved, so content hashes
|
|
@@ -145,7 +145,7 @@ not a valid storage DTO. Use it to branch on format without a heuristic or
|
|
|
145
145
|
`try`/`catch` as control flow:
|
|
146
146
|
|
|
147
147
|
```ts
|
|
148
|
-
// "JSON canonical, Markdown fallback"
|
|
148
|
+
// "JSON canonical, Markdown fallback": no exceptions, no string sniffing
|
|
149
149
|
const doc = Document.tryFromJson(content) ?? Document.fromMarkdown(content);
|
|
150
150
|
```
|
|
151
151
|
|
|
@@ -156,14 +156,14 @@ genuinely malformed Markdown.
|
|
|
156
156
|
|
|
157
157
|
The `schema` value (`quillmark/document@0.93.0`) is the **model version**,
|
|
158
158
|
not the running crate version. It is a hand-set constant, bumped only when
|
|
159
|
-
the `Document` model itself changes
|
|
159
|
+
the `Document` model itself changes, so every `0.93.x` patch release reads
|
|
160
160
|
and writes that same value.
|
|
161
161
|
|
|
162
162
|
- **Upgrading is safe.** A newer build always reads documents written by an
|
|
163
163
|
older one. Each schema version's wire format is frozen and never changes;
|
|
164
164
|
when the model does change, the new build ships a migration that converts
|
|
165
165
|
old payloads on `fromJson`. A document you commit as your canonical
|
|
166
|
-
on-disk format keeps loading across crate upgrades
|
|
166
|
+
on-disk format keeps loading across crate upgrades: there is no need to
|
|
167
167
|
pin old wasm to read old data.
|
|
168
168
|
- **Downgrading is not.** `fromJson` rejects an *unknown* (i.e. newer)
|
|
169
169
|
`schema` version rather than guessing at a format it predates. Don't feed
|
|
@@ -178,13 +178,13 @@ if (v && v !== Document.currentSchemaVersion()) {
|
|
|
178
178
|
}
|
|
179
179
|
```
|
|
180
180
|
|
|
181
|
-
`schemaVersionOf` does not validate the payload
|
|
181
|
+
`schemaVersionOf` does not validate the payload: it only reads the
|
|
182
182
|
`schema` field, returning `undefined` for non-JSON, non-objects, or
|
|
183
183
|
payloads that don't carry one. Use it to distinguish "wrong version" from
|
|
184
184
|
"corrupt" when `fromJson` throws.
|
|
185
185
|
|
|
186
186
|
In short: persist the `toJson` string, upgrade freely, never downgrade. The
|
|
187
|
-
full design
|
|
187
|
+
full design (including how migrations are added) is in
|
|
188
188
|
`prose/canon/DOCUMENT_STORAGE.md`.
|
|
189
189
|
|
|
190
190
|
### `doc.equals(other)`
|
|
@@ -201,7 +201,7 @@ Use this to validate indices before calling card mutators (`removeCard`,
|
|
|
201
201
|
|
|
202
202
|
### `quill.validate(doc)`
|
|
203
203
|
|
|
204
|
-
Returns `Diagnostic[]
|
|
204
|
+
Returns `Diagnostic[]`: the document validated against the quill schema,
|
|
205
205
|
without invoking the backend. An empty array means the document is valid.
|
|
206
206
|
Each diagnostic carries the canonical `validation::*` `code`, `path`, and
|
|
207
207
|
`hint`. Includes the non-fatal `validation::must_fill` warning for each
|
|
@@ -214,14 +214,14 @@ const errors = diagnostics.filter(d => d.severity === "error");
|
|
|
214
214
|
```
|
|
215
215
|
|
|
216
216
|
To render a form editor, read field definitions from `quill.schema` (walk
|
|
217
|
-
`fields` in key order
|
|
218
|
-
values from the `Document` payload
|
|
217
|
+
`fields` in key order: declaration order is display order) and the authored
|
|
218
|
+
values from the `Document` payload: there is no separate form-view projection.
|
|
219
219
|
|
|
220
220
|
### `quill.seedDocument()`
|
|
221
221
|
|
|
222
222
|
Returns a starter `Document` seeded from the schema: each field's `example:`
|
|
223
223
|
is committed and every other field is left absent (the render layer fills
|
|
224
|
-
`default:` → type-empty zero). Illustration-first
|
|
224
|
+
`default:` → type-empty zero). Illustration-first: a field with both an
|
|
225
225
|
`example` and a `default` renders its example. Use as the initial state for a
|
|
226
226
|
"new document" editor.
|
|
227
227
|
|
|
@@ -243,7 +243,7 @@ doc.insertCard({ kind: "note" }, 0); // insert at index 0
|
|
|
243
243
|
```
|
|
244
244
|
|
|
245
245
|
Reads and writes are two aligned shapes. A read `Card` always has `body:
|
|
246
|
-
Content` (canonical content, never a raw string)
|
|
246
|
+
Content` (canonical content, never a raw string): no narrowing, no guessing
|
|
247
247
|
whether the body was normalized. The write shape `CardInput` widens `body` to
|
|
248
248
|
`Content | string` (a markdown string imports to the content) and makes every
|
|
249
249
|
field but `kind` optional. Every `Card` is a valid `CardInput`, so `insertCard`
|
|
@@ -251,8 +251,8 @@ still takes exactly what `cards` / `removeCard` / `seedCard` return.
|
|
|
251
251
|
Build a fresh card from a flat field map with
|
|
252
252
|
`Document.makeCard(kind, fields?, body?)`.
|
|
253
253
|
|
|
254
|
-
**One address for the whole surface.** Reads and writes navigate by an `Addr
|
|
255
|
-
`{ card?, field? }`, absent `card` = main, absent `field` = body
|
|
254
|
+
**One address for the whole surface.** Reads and writes navigate by an `Addr`:
|
|
255
|
+
`{ card?, field? }`, absent `card` = main, absent `field` = body, and a bare
|
|
256
256
|
string is shorthand for `{ field }`. So `doc.storeField("qty", 3)` targets the
|
|
257
257
|
main card's `qty`, `doc.storeField({ card: 2, field: "qty" }, 3)` a composable
|
|
258
258
|
card's. Reads are total over the field axis (`getStored` → `undefined`, `isFill` → `false` for
|
|
@@ -263,10 +263,10 @@ markdown is read through `quill.reader(doc).get(field)`). Card-scoped verbs take
|
|
|
263
263
|
`CardAddr` (`{ card? }`) first: `doc.getExt({ card: 2 })`, and the batch below.
|
|
264
264
|
|
|
265
265
|
Batch mutation: `doc.storeFields({}, {...})` / `doc.storeFields({ card: index }, {...})`
|
|
266
|
-
apply a whole object atomically
|
|
266
|
+
apply a whole object atomically: on any invalid field nothing is applied and
|
|
267
267
|
the thrown error carries one diagnostic per offending field (`path` = field
|
|
268
268
|
name). The address is first (never shape-overloaded, since `card` is a legal
|
|
269
|
-
field name), and parses strictly
|
|
269
|
+
field name), and parses strictly: a stray key throws rather than silently
|
|
270
270
|
reading as `{}`. The main card is `{}`, or **`MAIN_CARD_ADDR`** (from
|
|
271
271
|
`@quillmark/wasm/runtime`), a frozen alias that spells the intent:
|
|
272
272
|
`doc.storeFields(MAIN_CARD_ADDR, {...})`.
|
|
@@ -277,12 +277,12 @@ A `Document` holds only a `$quill` *reference*, not the resolved schema, so type
|
|
|
277
277
|
writes go through the schema-bound writer while the quill-free opaque store sits
|
|
278
278
|
on `Document` itself (**store** = verbatim, **set** = typed):
|
|
279
279
|
|
|
280
|
-
- **`quill.writer(doc)
|
|
280
|
+
- **`quill.writer(doc)`: the typed door whenever a quill is in hand.** Bind the
|
|
281
281
|
schema once and issue bare `set` / `setAll` / `setBody` / `reviseField` /
|
|
282
282
|
`addCard` / `card(i)`. Each resolves the field's schema `type`, coerces the
|
|
283
283
|
value to its canonical form (`"3"` → `3`, a markdown string → a richtext
|
|
284
284
|
content), and **fails now** on a mismatch instead of at render. A name the schema
|
|
285
|
-
does not declare throws `UnknownField` rather than falling to the opaque store
|
|
285
|
+
does not declare throws `UnknownField` rather than falling to the opaque store:
|
|
286
286
|
on the typed path an undeclared name is a typo, not a fallback. The batch form
|
|
287
287
|
(`setAll`) is all-or-nothing: an undeclared name aborts the whole write and its
|
|
288
288
|
per-field diagnostics name every offending field, so a whole-form submit
|
|
@@ -290,21 +290,21 @@ on `Document` itself (**store** = verbatim, **set** = typed):
|
|
|
290
290
|
carries the quill-taking `_commitField` / `_commitFields` / `_addCard` /
|
|
291
291
|
`_reviseField` ABI the writer delegates to, hidden from the `.d.ts`.)
|
|
292
292
|
|
|
293
|
-
- **`store
|
|
293
|
+
- **`store*`: the deliberate quill-free primitive.** `doc.storeField(addr, value)`
|
|
294
294
|
/ `doc.storeFields(cardAddr, {...})` (and `storeFill`) validate only the field
|
|
295
295
|
name/depth/kind and store the value verbatim, no quill required. Reach for it
|
|
296
296
|
on purpose when you *want* the opaque store: quill-agnostic storage/migration
|
|
297
297
|
infra that has no bundle and must write regardless of a drifted schema;
|
|
298
298
|
store-now-validate-later editors holding in-progress input that `commit`
|
|
299
299
|
would reject; or verbatim passthrough of fields the schema doesn't own. It is
|
|
300
|
-
the lower layer, not a lighter `commit
|
|
300
|
+
the lower layer, not a lighter `commit`: a typo'd field name stores silently
|
|
301
301
|
and only surfaces at `quill.validate` / render.
|
|
302
302
|
|
|
303
303
|
Per-keystroke cost is the same either way (both mutate the in-memory `Document`
|
|
304
304
|
in place; no seam is crossed), so steering to the writer buys the type check for
|
|
305
305
|
free.
|
|
306
306
|
|
|
307
|
-
#### `DocumentWriter` / `CardWriter
|
|
307
|
+
#### `DocumentWriter` / `CardWriter`: bind the quill once
|
|
308
308
|
|
|
309
309
|
`quill.writer(doc)` binds the quill's schema to the document once, so a form
|
|
310
310
|
editor or MCP writer that holds both issues bare verbs (the writer forwards to
|
|
@@ -315,25 +315,25 @@ const ed = quill.writer(doc); // Rust `quill.writer(doc)`
|
|
|
315
315
|
ed.set("subject", "Q3 results"); // strict-committed to the schema type
|
|
316
316
|
ed.setAll({ qty: "3", subject: "Q3" }); // all-or-nothing batch
|
|
317
317
|
ed.reviseField("subject", "Q3 **results**"); // typed AND anchor-preserving; returns a Delta
|
|
318
|
-
ed.set("titel", "x"); // throws UnknownField
|
|
318
|
+
ed.set("titel", "x"); // throws UnknownField: a typo, not a fallback
|
|
319
319
|
ed.card(2).set("body", "**note**"); // composable card, resolved by its $kind
|
|
320
320
|
```
|
|
321
321
|
|
|
322
322
|
`DocumentWriter` / `CardWriter` are pure JS holding references to your existing
|
|
323
|
-
`quill` and `doc
|
|
323
|
+
`quill` and `doc`: no WASM handle of their own, nothing to `free()`. `card(i)`
|
|
324
324
|
is lazy: it never throws; an out-of-range index throws `IndexOutOfRange` at the
|
|
325
325
|
write.
|
|
326
326
|
|
|
327
327
|
### `engine.render(quill, parsed, opts?)` vs. `engine.open(quill, parsed)`
|
|
328
328
|
|
|
329
|
-
Use **`engine.render`** for one-shot exports (PDF/SVG/PNG)
|
|
329
|
+
Use **`engine.render`** for one-shot exports (PDF/SVG/PNG): compiles, emits
|
|
330
330
|
artifacts, done. Use **`LiveSession`** (returned by `engine.open`) for
|
|
331
331
|
reactive previews: the session is a persistent compiler. `paint` / `render` /
|
|
332
332
|
`regions` / `fieldAt` read its current compile without recompiling, and `apply(doc)`
|
|
333
333
|
recompiles in place on each edit, returning a `ChangeSet` whose `dirtyPages`
|
|
334
|
-
tells you which pages to repaint (`dirty ∩ visible`). Apply is transactional
|
|
334
|
+
tells you which pages to repaint (`dirty ∩ visible`). Apply is transactional:
|
|
335
335
|
on throw, every read keeps serving the last-good compile. Don't open a session
|
|
336
|
-
per export, and don't re-open per edit
|
|
336
|
+
per export, and don't re-open per edit: `apply` instead.
|
|
337
337
|
|
|
338
338
|
### `engine.render(quill, parsed, opts?)`
|
|
339
339
|
Render a pre-parsed `Document` against `quill`. Throws an
|
|
@@ -344,7 +344,7 @@ declared backend.
|
|
|
344
344
|
Open once, render all or selected pages (`opts.pages`).
|
|
345
345
|
|
|
346
346
|
The session also exposes `pageCount`, `backendId`, `supportsCanvas`,
|
|
347
|
-
`warnings` (non-fatal diagnostics of the current compile
|
|
347
|
+
`warnings` (non-fatal diagnostics of the current compile: set at `open`,
|
|
348
348
|
refreshed by each committed `apply`),
|
|
349
349
|
`apply(doc)` for in-place recompiles, `pageSize(page)`, and
|
|
350
350
|
`paint(ctx, page, opts?)` for canvas previews. See below.
|
|
@@ -361,7 +361,7 @@ render a "no pages to preview" UI without relying on the throw.
|
|
|
361
361
|
`OffscreenCanvasRenderingContext2D` (Worker), skipping PNG/SVG byte
|
|
362
362
|
round-trips.
|
|
363
363
|
|
|
364
|
-
The painter owns `canvas.width` / `canvas.height
|
|
364
|
+
The painter owns `canvas.width` / `canvas.height`: it sizes the backing
|
|
365
365
|
store itself. Consumers own `canvas.style.*` (or the layout system that
|
|
366
366
|
sets them) and read `layoutWidth` / `layoutHeight` from the returned
|
|
367
367
|
`PaintResult`.
|
|
@@ -390,16 +390,16 @@ canvas.style.height = `${result.layoutHeight}px`;
|
|
|
390
390
|
clamped page renders soft at the same `canvas.style` size.
|
|
391
391
|
- `paint` writes the whole backing store with `putImageData`, which
|
|
392
392
|
ignores the 2D context transform, `globalAlpha`, and clip. Give each
|
|
393
|
-
visible page its own
|
|
393
|
+
visible page its own `<canvas>` element: you cannot composite two pages,
|
|
394
394
|
a sub-rect, or a context transform through `paint`.
|
|
395
|
-
- `paint` is always a full repaint
|
|
395
|
+
- `paint` is always a full repaint: setting the backing-store width /
|
|
396
396
|
height clears it. No `clearRect` required. Each call re-rasterizes from
|
|
397
397
|
scratch (no per-page raster cache), so keep a page's canvas alive while
|
|
398
398
|
it stays near the viewport rather than pooling one canvas across pages:
|
|
399
399
|
an idle canvas retains its pixels for free, whereas reusing a canvas on
|
|
400
400
|
scroll re-runs a full render.
|
|
401
401
|
- `pageCount` and `pageSize(page)` are stable for the session's
|
|
402
|
-
lifetime (immutable snapshot)
|
|
402
|
+
lifetime (immutable snapshot): cache them.
|
|
403
403
|
- Worker support: pass an `OffscreenCanvasRenderingContext2D` and the
|
|
404
404
|
same call signature works. `layoutWidth` / `layoutHeight` are
|
|
405
405
|
informational in that mode (no CSS layout box); fold everything into
|
|
@@ -415,14 +415,14 @@ canvas.style.height = `${result.layoutHeight}px`;
|
|
|
415
415
|
|
|
416
416
|
A field's *cell* is inferred from whether its schema declares a `default:`:
|
|
417
417
|
|
|
418
|
-
- **Unendorsed** (no `default:`)
|
|
418
|
+
- **Unendorsed** (no `default:`): `quill.blueprint` renders the
|
|
419
419
|
`!must_fill` marker in the value cell (carrying the field's `example` as a
|
|
420
420
|
suggested value when one exists). An absent Unendorsed field zero-fills
|
|
421
421
|
silently. A `!must_fill` marker left in the document is non-fatal: it emits
|
|
422
422
|
the `validation::must_fill` warning and still renders. Partial documents
|
|
423
423
|
are accepted; `engine.render(quill, doc)` only throws for malformed
|
|
424
424
|
input.
|
|
425
|
-
- **Endorsed** (with `default:`)
|
|
425
|
+
- **Endorsed** (with `default:`): `quill.blueprint` renders the
|
|
426
426
|
default value with a type-only `# <type>` annotation (shippable as-is),
|
|
427
427
|
and the default is used when the document omits the field.
|
|
428
428
|
|
|
@@ -431,7 +431,7 @@ document emits the non-fatal `validation::must_fill` warning.
|
|
|
431
431
|
|
|
432
432
|
### Errors
|
|
433
433
|
|
|
434
|
-
Every method that can fail throws a **`QuillmarkError
|
|
434
|
+
Every method that can fail throws a **`QuillmarkError`**: a JS `Error` with
|
|
435
435
|
`.diagnostics` attached. The type and a guard are exported from the root:
|
|
436
436
|
|
|
437
437
|
```ts
|
|
@@ -443,17 +443,17 @@ try {
|
|
|
443
443
|
if (isQuillmarkError(e)) {
|
|
444
444
|
for (const d of e.diagnostics) console.error(d.severity, d.message);
|
|
445
445
|
} else {
|
|
446
|
-
throw e; // not a quillmark failure
|
|
446
|
+
throw e; // not a quillmark failure: programming error, re-throw
|
|
447
447
|
}
|
|
448
448
|
}
|
|
449
449
|
```
|
|
450
450
|
|
|
451
|
-
`QuillmarkError` is a **structural interface, not a class
|
|
451
|
+
`QuillmarkError` is a **structural interface, not a class**: the WASM layer
|
|
452
452
|
throws a real `Error` and attaches the property, so there is no constructor to
|
|
453
453
|
`instanceof` against; narrow with `isQuillmarkError` (which also works on
|
|
454
454
|
errors from any build or WASM instance in the page).
|
|
455
455
|
|
|
456
|
-
`diagnostics` is always non-empty
|
|
456
|
+
`diagnostics` is always non-empty: length 1 for most failures, length N for
|
|
457
457
|
backend compilation errors. `message` is derived from `diagnostics`
|
|
458
458
|
(`diagnostics[0].message` for single-diagnostic errors; an aggregate
|
|
459
459
|
`"<N> error(s): <first.message>"` summary for compilation failures).
|
|
@@ -461,15 +461,20 @@ backend compilation errors. `message` is derived from `diagnostics`
|
|
|
461
461
|
Read `err.diagnostics[0]` for the primary diagnostic; iterate the array for
|
|
462
462
|
compilation failures. The same shape applies to every throw site:
|
|
463
463
|
|
|
464
|
-
- `Document.fromMarkdown
|
|
465
|
-
errors, `parse::input_too_large` for inputs > 10
|
|
466
|
-
- `Document` mutators (`storeField`, the writer's `set`, etc.)
|
|
464
|
+
- `Document.fromMarkdown`: parse errors (missing root `$quill` metadata, YAML
|
|
465
|
+
errors, `parse::input_too_large` for inputs > 10 MiB).
|
|
466
|
+
- `Document` mutators (`storeField`, the writer's `set`, etc.): mutator
|
|
467
467
|
failures carry a namespaced `edit::*` `code` on `diagnostics[0]`
|
|
468
468
|
(`edit::invalid_field_name`, `edit::unknown_field`, `edit::index_out_of_range`,
|
|
469
469
|
`edit::field_conform`, …). Route on `diagnostics[0].code`, never on message
|
|
470
470
|
text.
|
|
471
|
-
- `engine.render` / `session.render
|
|
471
|
+
- `engine.render` / `session.render`: backend compilation failures and
|
|
472
472
|
validation errors.
|
|
473
|
+
- Any method taking a `Quill` or `Document`: a handle from a *second* copy of
|
|
474
|
+
`@quillmark/wasm` is refused with `runtime::foreign_handle`, hinting `npm ls
|
|
475
|
+
@quillmark/wasm`. Two copies are two WASM memories and two `Quill`/`Document`
|
|
476
|
+
classes; dedupe to one. A value that is not a handle at all keeps its own
|
|
477
|
+
`runtime::not_a_document` / `runtime::not_a_quill`.
|
|
473
478
|
|
|
474
479
|
### Lifecycle
|
|
475
480
|
|
|
@@ -480,17 +485,15 @@ teardown hook for callers that want deterministic release.
|
|
|
480
485
|
|
|
481
486
|
`engine.render` and `engine.open` read the `quill` and `doc` handles
|
|
482
487
|
synchronously, before their first await, so freeing a handle as soon as the
|
|
483
|
-
call returns
|
|
484
|
-
{ doc.free(); }
|
|
488
|
+
call returns: `try { return engine.render(quill, doc); } finally
|
|
489
|
+
{ doc.free(); }`: is safe even on the first render, while the backend
|
|
485
490
|
binary is still loading.
|
|
486
491
|
|
|
487
492
|
The package floor is Node 22+ (`engines: { node: ">=22" }`) and current
|
|
488
493
|
evergreen browsers; `--weak-refs` itself only needs Node 14.6+. The `using`
|
|
489
|
-
sugar
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
For environments where `using` (the [explicit resource management][erm]
|
|
493
|
-
proposal) hasn't landed, use an explicit `try` / `finally`:
|
|
494
|
+
sugar ([explicit resource management][erm]) needs Node 24 and is optional.
|
|
495
|
+
Where it hasn't landed, an explicit `try` / `finally` runs on the Node 22
|
|
496
|
+
floor:
|
|
494
497
|
|
|
495
498
|
```ts
|
|
496
499
|
const session = await engine.open(quill, doc);
|