@quillmark/wasm 0.92.1 → 0.95.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +301 -4
- package/README.md +167 -66
- package/backends/pdfform/wasm.d.ts +1066 -0
- package/backends/pdfform/wasm.js +9 -0
- package/backends/pdfform/wasm_bg.js +2654 -0
- package/backends/pdfform/wasm_bg.wasm +0 -0
- package/backends/pdfform/wasm_bg.wasm.d.ts +98 -0
- package/backends/typst/wasm.d.ts +584 -167
- package/backends/typst/wasm.js +1 -1
- package/backends/typst/wasm_bg.js +1047 -397
- package/backends/typst/wasm_bg.wasm +0 -0
- package/backends/typst/wasm_bg.wasm.d.ts +45 -25
- package/core/wasm.d.ts +412 -110
- package/core/wasm.js +1 -1
- package/core/wasm_bg.js +713 -226
- package/core/wasm_bg.wasm +0 -0
- package/core/wasm_bg.wasm.d.ts +31 -17
- package/package.json +4 -3
- package/runtime/runtime.d.ts +459 -21
- package/runtime/runtime.js +536 -30
package/README.md
CHANGED
|
@@ -14,13 +14,13 @@ The package exposes **one import surface**:
|
|
|
14
14
|
an `Engine` that renders them.
|
|
15
15
|
|
|
16
16
|
`Quill` and `Document` are re-exported verbatim from the internal Typst-less
|
|
17
|
-
core build, so
|
|
17
|
+
core build, so editor/validation code (`Quill.fromTree`,
|
|
18
18
|
`Document.fromMarkdown`) loads only that small core binary — no backend is
|
|
19
|
-
loaded until you render. The `Engine` hides everything else: each backend
|
|
20
|
-
|
|
21
|
-
loaded on the first render. The Engine clones a `Quill` /
|
|
22
|
-
backend's memory as data and frees the clones — you never
|
|
23
|
-
or cross a memory boundary yourself.
|
|
19
|
+
loaded until you render. The `Engine` hides everything else: each backend
|
|
20
|
+
(`typst`, `pdfform`) is a separate, private WASM binary with its own linear
|
|
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 — you never
|
|
23
|
+
hold a backend object or cross a memory boundary yourself.
|
|
24
24
|
|
|
25
25
|
## Build
|
|
26
26
|
|
|
@@ -28,9 +28,10 @@ or cross a memory boundary yourself.
|
|
|
28
28
|
bash scripts/build-wasm.sh
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
The script builds
|
|
32
|
-
(
|
|
33
|
-
(see
|
|
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
|
+
each with `--target bundler` and `--weak-refs` enabled (see
|
|
34
|
+
[Lifecycle](#lifecycle)).
|
|
34
35
|
|
|
35
36
|
## Test
|
|
36
37
|
|
|
@@ -46,7 +47,7 @@ npm test
|
|
|
46
47
|
```ts
|
|
47
48
|
import { Document, Quill, Engine } from "@quillmark/wasm";
|
|
48
49
|
|
|
49
|
-
const quill = Quill.fromTree(tree); // engine
|
|
50
|
+
const quill = Quill.fromTree(tree); // no engine needed: build + validate
|
|
50
51
|
const engine = new Engine(); // loads a backend lazily on first render
|
|
51
52
|
|
|
52
53
|
const markdown = `~~~
|
|
@@ -84,6 +85,14 @@ never cloning the quill. Use them as non-failing pre-render probes.
|
|
|
84
85
|
Build + validate a `Quill` from an in-memory tree. Pure — the declared backend
|
|
85
86
|
is resolved at render time, not here. Loads no backend binary.
|
|
86
87
|
|
|
88
|
+
### `new Document(quillRef)`
|
|
89
|
+
A blank document: a main card carrying only `$quill`, an empty body, and no
|
|
90
|
+
composable cards — the programmatic blank canvas. Absent fields resolve at
|
|
91
|
+
render time (schema `default`, else type-empty zero), so nothing the caller
|
|
92
|
+
did not set reaches the output. Build it up with `storeFields` / `insertCard`.
|
|
93
|
+
For an example-filled starter use `quill.seedDocument()`. Throws on an
|
|
94
|
+
invalid quill reference.
|
|
95
|
+
|
|
87
96
|
### `Document.fromMarkdown(markdown)`
|
|
88
97
|
Parse markdown to a parsed document. Throws a JS `Error` (with `.diagnostics`
|
|
89
98
|
attached, see [Errors](#errors)) on any parse failure, including a missing
|
|
@@ -145,9 +154,9 @@ genuinely malformed Markdown.
|
|
|
145
154
|
|
|
146
155
|
### Storage compatibility across versions
|
|
147
156
|
|
|
148
|
-
The `schema` value (`quillmark/document@0.
|
|
157
|
+
The `schema` value (`quillmark/document@0.93.0`) is the **model version**,
|
|
149
158
|
not the running crate version. It is a hand-set constant, bumped only when
|
|
150
|
-
the `Document` model itself changes — so every `0.
|
|
159
|
+
the `Document` model itself changes — so every `0.93.x` patch release reads
|
|
151
160
|
and writes that same value.
|
|
152
161
|
|
|
153
162
|
- **Upgrading is safe.** A newer build always reads documents written by an
|
|
@@ -188,15 +197,15 @@ and compare instead of re-parsing on every keystroke.
|
|
|
188
197
|
### `doc.cardCount`
|
|
189
198
|
O(1) getter for the number of composable cards (excluding the main card).
|
|
190
199
|
Use this to validate indices before calling card mutators (`removeCard`,
|
|
191
|
-
`
|
|
200
|
+
`storeField({ card, field }, …)`, etc.) without allocating the full `cards` array.
|
|
192
201
|
|
|
193
202
|
### `quill.validate(doc)`
|
|
194
203
|
|
|
195
204
|
Returns `Diagnostic[]` — the document validated against the quill schema,
|
|
196
205
|
without invoking the backend. An empty array means the document is valid.
|
|
197
206
|
Each diagnostic carries the canonical `validation::*` `code`, `path`, and
|
|
198
|
-
`hint`. Includes the non-fatal `validation::
|
|
199
|
-
|
|
207
|
+
`hint`. Includes the non-fatal `validation::must_fill` warning for each
|
|
208
|
+
`!must_fill` marker left in the document (render zero-fills these rather
|
|
200
209
|
than failing), so filter by `severity`/`code` for blockers vs. hints:
|
|
201
210
|
|
|
202
211
|
```ts
|
|
@@ -204,9 +213,9 @@ const diagnostics = quill.validate(Document.fromMarkdown(markdown));
|
|
|
204
213
|
const errors = diagnostics.filter(d => d.severity === "error");
|
|
205
214
|
```
|
|
206
215
|
|
|
207
|
-
To render a form editor, read field definitions from `quill.schema` (
|
|
208
|
-
fields
|
|
209
|
-
`Document` payload — there is no separate form-view projection.
|
|
216
|
+
To render a form editor, read field definitions from `quill.schema` (walk
|
|
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.
|
|
210
219
|
|
|
211
220
|
### `quill.seedDocument()`
|
|
212
221
|
|
|
@@ -223,51 +232,128 @@ const markdown = doc.toMarkdown();
|
|
|
223
232
|
|
|
224
233
|
For per-card seeding, `quill.seedMain()` returns just the `$kind: main` card
|
|
225
234
|
and `quill.seedCard(kind)` returns a starter composable card (or `undefined`
|
|
226
|
-
if the kind is not declared). Both return the
|
|
227
|
-
`doc.main` / `doc.cards`, which `doc.
|
|
228
|
-
directly:
|
|
235
|
+
if the kind is not declared). Both return the read `Card` shape of
|
|
236
|
+
`doc.main` / `doc.cards`, which `doc.insertCard` accepts directly:
|
|
229
237
|
|
|
230
238
|
```ts
|
|
231
|
-
doc.
|
|
232
|
-
doc.
|
|
239
|
+
doc.insertCard(quill.seedCard("note")); // seed → append
|
|
240
|
+
doc.insertCard(Document.makeCard("note", { x: 1 })); // build from a flat map
|
|
241
|
+
doc.insertCard({ kind: "note", body: "Plain **markdown**." }); // bare inline
|
|
242
|
+
doc.insertCard({ kind: "note" }, 0); // insert at index 0
|
|
233
243
|
```
|
|
234
244
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
245
|
+
Reads and writes are two aligned shapes. A read `Card` always has `body:
|
|
246
|
+
Content` (canonical content, never a raw string) — no narrowing, no guessing
|
|
247
|
+
whether the body was normalized. The write shape `CardInput` widens `body` to
|
|
248
|
+
`Content | string` (a markdown string imports to the content) and makes every
|
|
249
|
+
field but `kind` optional. Every `Card` is a valid `CardInput`, so `insertCard`
|
|
250
|
+
still takes exactly what `cards` / `removeCard` / `seedCard` return.
|
|
251
|
+
Build a fresh card from a flat field map with
|
|
252
|
+
`Document.makeCard(kind, fields?, body?)`.
|
|
253
|
+
|
|
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
|
+
string is shorthand for `{ field }`. So `doc.storeField("qty", 3)` targets the
|
|
257
|
+
main card's `qty`, `doc.storeField({ card: 2, field: "qty" }, 3)` a composable
|
|
258
|
+
card's. Reads are total over the field axis (`get` → `undefined`, `isFill` → `false` for
|
|
259
|
+
an absent field; only an out-of-range card throws); field writes throw on a body
|
|
260
|
+
address. `getMarkdown` is the body markdown read (a `CardAddr`; a field's
|
|
261
|
+
markdown is read through `quill.view(doc).get(field)`). Card-scoped verbs take a
|
|
262
|
+
`CardAddr` (`{ card? }`) first: `doc.getExt({ card: 2 })`, and the batch below.
|
|
263
|
+
|
|
264
|
+
Batch mutation: `doc.storeFields({}, {...})` / `doc.storeFields({ card: index }, {...})`
|
|
265
|
+
apply a whole object atomically — on any invalid field nothing is applied and
|
|
266
|
+
the thrown error carries one diagnostic per offending field (`path` = field
|
|
267
|
+
name). The address is first (never shape-overloaded, since `card` is a legal
|
|
268
|
+
field name), and parses strictly — a stray key throws rather than silently
|
|
269
|
+
reading as `{}`. The main card is `{}`, or **`MAIN_CARD_ADDR`** (from
|
|
270
|
+
`@quillmark/wasm/runtime`), a frozen alias that spells the intent:
|
|
271
|
+
`doc.storeFields(MAIN_CARD_ADDR, {...})`.
|
|
272
|
+
|
|
273
|
+
### Typed writes: `commit*` is the default, `store*` is the quill-free primitive
|
|
274
|
+
|
|
275
|
+
A `Document` holds only a `$quill` *reference*, not the resolved schema, so typed
|
|
276
|
+
writes go through the schema-bound writer while the quill-free opaque store sits
|
|
277
|
+
on `Document` itself (**store** = verbatim, **set** = typed):
|
|
278
|
+
|
|
279
|
+
- **`quill.writer(doc)` — the typed door whenever a quill is in hand.** Bind the
|
|
280
|
+
schema once and issue bare `set` / `setAll` / `setBody` / `reviseField` /
|
|
281
|
+
`addCard` / `card(i)`. Each resolves the field's schema `type`, coerces the
|
|
282
|
+
value to its canonical form (`"3"` → `3`, a markdown string → a richtext
|
|
283
|
+
content), and **fails now** on a mismatch instead of at render. A name the schema
|
|
284
|
+
does not declare throws `UnknownField` rather than falling to the opaque store —
|
|
285
|
+
on the typed path an undeclared name is a typo, not a fallback. The batch form
|
|
286
|
+
(`setAll`) is all-or-nothing: an undeclared name aborts the whole write and its
|
|
287
|
+
per-field diagnostics name every offending field, so a whole-form submit
|
|
288
|
+
surfaces every typo `storeFields` would silently absorb. (The raw wasm class
|
|
289
|
+
carries the quill-taking `_commitField` / `_commitFields` / `_addCard` /
|
|
290
|
+
`_reviseField` ABI the writer delegates to, hidden from the `.d.ts`.)
|
|
291
|
+
|
|
292
|
+
- **`store*` — the deliberate quill-free primitive.** `doc.storeField(addr, value)`
|
|
293
|
+
/ `doc.storeFields(cardAddr, {...})` (and `storeFill`) validate only the field
|
|
294
|
+
name/depth/kind and store the value verbatim, no quill required. Reach for it
|
|
295
|
+
on purpose when you *want* the opaque store: quill-agnostic storage/migration
|
|
296
|
+
infra that has no bundle and must write regardless of a drifted schema;
|
|
297
|
+
store-now-validate-later editors holding in-progress input that `commit`
|
|
298
|
+
would reject; or verbatim passthrough of fields the schema doesn't own. It is
|
|
299
|
+
the lower layer, not a lighter `commit` — a typo'd field name stores silently
|
|
300
|
+
and only surfaces at `quill.validate` / render.
|
|
301
|
+
|
|
302
|
+
Per-keystroke cost is the same either way (both mutate the in-memory `Document`
|
|
303
|
+
in place; no seam is crossed), so steering to the writer buys the type check for
|
|
304
|
+
free.
|
|
305
|
+
|
|
306
|
+
#### `DocumentWriter` / `CardWriter` — bind the quill once
|
|
307
|
+
|
|
308
|
+
`quill.writer(doc)` binds the quill's schema to the document once, so a form
|
|
309
|
+
editor or MCP writer that holds both issues bare verbs (the writer forwards to
|
|
310
|
+
the per-call `_commit*` ABI):
|
|
238
311
|
|
|
239
|
-
|
|
312
|
+
```ts
|
|
313
|
+
const ed = quill.writer(doc); // Rust `quill.writer(doc)` twin; new DocumentWriter(quill, doc) also works
|
|
314
|
+
ed.set("subject", "Q3 results"); // strict-committed to the schema type
|
|
315
|
+
ed.setAll({ qty: "3", subject: "Q3" }); // all-or-nothing batch
|
|
316
|
+
ed.reviseField("subject", "Q3 **results**"); // typed AND anchor-preserving; returns a Delta
|
|
317
|
+
ed.set("titel", "x"); // throws UnknownField — a typo, not a fallback
|
|
318
|
+
ed.card(2).set("body", "**note**"); // composable card, resolved by its $kind
|
|
319
|
+
```
|
|
240
320
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
321
|
+
`DocumentWriter` / `CardWriter` are pure JS holding references to your existing
|
|
322
|
+
`quill` and `doc` — no WASM handle of their own, nothing to `free()`. `card(i)`
|
|
323
|
+
is lazy: it never throws; an out-of-range index throws `IndexOutOfRange` at the
|
|
324
|
+
write.
|
|
325
|
+
|
|
326
|
+
### `engine.render(quill, parsed, opts?)` vs. `engine.open(quill, parsed)`
|
|
245
327
|
|
|
246
328
|
Use **`engine.render`** for one-shot exports (PDF/SVG/PNG) — compiles, emits
|
|
247
|
-
artifacts, done. Use **`
|
|
248
|
-
reactive previews
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
329
|
+
artifacts, done. Use **`LiveSession`** (returned by `engine.open`) for
|
|
330
|
+
reactive previews: the session is a persistent compiler. `paint` / `render` /
|
|
331
|
+
`regions` / `fieldAt` read its current compile without recompiling, and `apply(doc)`
|
|
332
|
+
recompiles in place on each edit, returning a `ChangeSet` whose `dirtyPages`
|
|
333
|
+
tells you which pages to repaint (`dirty ∩ visible`). Apply is transactional —
|
|
334
|
+
on throw, every read keeps serving the last-good compile. Don't open a session
|
|
335
|
+
per export, and don't re-open per edit — `apply` instead.
|
|
252
336
|
|
|
253
337
|
### `engine.render(quill, parsed, opts?)`
|
|
254
|
-
Render a pre-parsed `Document` against `quill`. Throws
|
|
255
|
-
no registered backend matches the quill's
|
|
338
|
+
Render a pre-parsed `Document` against `quill`. Throws an
|
|
339
|
+
`engine::backend_not_found` error if no registered backend matches the quill's
|
|
340
|
+
declared backend.
|
|
256
341
|
|
|
257
342
|
### `engine.open(quill, parsed)` + `session.render(opts?)`
|
|
258
343
|
Open once, render all or selected pages (`opts.pages`).
|
|
259
344
|
|
|
260
345
|
The session also exposes `pageCount`, `backendId`, `supportsCanvas`,
|
|
261
|
-
`warnings` (
|
|
262
|
-
|
|
263
|
-
|
|
346
|
+
`warnings` (non-fatal diagnostics of the current compile — set at `open`,
|
|
347
|
+
refreshed by each committed `apply`),
|
|
348
|
+
`apply(doc)` for in-place recompiles, `pageSize(page)`, and
|
|
349
|
+
`paint(ctx, page, opts?)` for canvas previews. See below.
|
|
264
350
|
|
|
265
351
|
A document that compiles to zero pages still produces a valid session
|
|
266
352
|
(`pageCount === 0`); `paint(ctx, 0)` and `pageSize(0)` then throw
|
|
267
353
|
`page index 0 out of range (pageCount=0)`. Branch on `pageCount === 0` to
|
|
268
354
|
render a "no pages to preview" UI without relying on the throw.
|
|
269
355
|
|
|
270
|
-
### Canvas Preview
|
|
356
|
+
### Canvas Preview
|
|
271
357
|
|
|
272
358
|
`session.paint(ctx, page, opts?)` rasterizes a page directly into a
|
|
273
359
|
`CanvasRenderingContext2D` (main thread) or
|
|
@@ -281,7 +367,7 @@ sets them) and read `layoutWidth` / `layoutHeight` from the returned
|
|
|
281
367
|
|
|
282
368
|
```ts
|
|
283
369
|
const result = session.paint(canvas.getContext("2d"), 0, {
|
|
284
|
-
layoutScale: 1, // layout px per
|
|
370
|
+
layoutScale: 1, // layout px per pt (page geometry unit)
|
|
285
371
|
densityScale: window.devicePixelRatio, // backing-store density
|
|
286
372
|
});
|
|
287
373
|
|
|
@@ -291,17 +377,26 @@ canvas.style.height = `${result.layoutHeight}px`;
|
|
|
291
377
|
|
|
292
378
|
- `layoutScale` (default 1) sets the canvas's display-box size:
|
|
293
379
|
`layoutWidth = widthPt * layoutScale`. For on-screen canvases this is
|
|
294
|
-
CSS pixels per
|
|
380
|
+
CSS pixels per point. Defaults to 1 (one CSS pixel per pt).
|
|
295
381
|
- `densityScale` (default 1) is the backing-store density multiplier.
|
|
296
382
|
Fold `window.devicePixelRatio`, in-app zoom, and `visualViewport.scale`
|
|
297
383
|
(pinch-zoom) into a single value here. Pass `devicePixelRatio` for
|
|
298
384
|
crisp output on high-DPI displays.
|
|
299
385
|
- The effective rasterization scale is `layoutScale * densityScale`. If
|
|
300
386
|
that would exceed the safe maximum (16384 px per side), `densityScale`
|
|
301
|
-
is clamped proportionally;
|
|
302
|
-
`
|
|
387
|
+
is clamped proportionally; `result.clamped` reports it and
|
|
388
|
+
`result.effectiveDensityScale` is the density actually applied. A
|
|
389
|
+
clamped page renders soft at the same `canvas.style` size.
|
|
390
|
+
- `paint` writes the whole backing store with `putImageData`, which
|
|
391
|
+
ignores the 2D context transform, `globalAlpha`, and clip. Give each
|
|
392
|
+
visible page its own `` element — you cannot composite two pages,
|
|
393
|
+
a sub-rect, or a context transform through `paint`.
|
|
303
394
|
- `paint` is always a full repaint — setting the backing-store width /
|
|
304
|
-
height clears it. No `clearRect` required.
|
|
395
|
+
height clears it. No `clearRect` required. Each call re-rasterizes from
|
|
396
|
+
scratch (no per-page raster cache), so keep a page's canvas alive while
|
|
397
|
+
it stays near the viewport rather than pooling one canvas across pages:
|
|
398
|
+
an idle canvas retains its pixels for free, whereas reusing a canvas on
|
|
399
|
+
scroll re-runs a full render.
|
|
305
400
|
- `pageCount` and `pageSize(page)` are stable for the session's
|
|
306
401
|
lifetime (immutable snapshot) — cache them.
|
|
307
402
|
- Worker support: pass an `OffscreenCanvasRenderingContext2D` and the
|
|
@@ -319,19 +414,19 @@ canvas.style.height = `${result.layoutHeight}px`;
|
|
|
319
414
|
|
|
320
415
|
A field's *cell* is inferred from whether its schema declares a `default:`:
|
|
321
416
|
|
|
322
|
-
- **Unendorsed** (no `default:`) — `quill.blueprint` renders
|
|
323
|
-
in the value cell
|
|
324
|
-
|
|
325
|
-
silently. A
|
|
326
|
-
|
|
327
|
-
|
|
417
|
+
- **Unendorsed** (no `default:`) — `quill.blueprint` renders the
|
|
418
|
+
`!must_fill` marker in the value cell (carrying the field's `example` as a
|
|
419
|
+
suggested value when one exists). An absent Unendorsed field zero-fills
|
|
420
|
+
silently. A `!must_fill` marker left in the document is non-fatal: it emits
|
|
421
|
+
the `validation::must_fill` warning and still renders. Partial documents
|
|
422
|
+
are accepted; `engine.render(quill, doc)` only throws for malformed
|
|
423
|
+
input.
|
|
328
424
|
- **Endorsed** (with `default:`) — `quill.blueprint` renders the
|
|
329
|
-
default value
|
|
330
|
-
is used when the document omits the field.
|
|
425
|
+
default value with a type-only `# <type>` annotation (shippable as-is),
|
|
426
|
+
and the default is used when the document omits the field.
|
|
331
427
|
|
|
332
|
-
`QuillFieldSchema` has no `required` axis.
|
|
333
|
-
|
|
334
|
-
`validation::must_fill_sentinel`.
|
|
428
|
+
`QuillFieldSchema` has no `required` axis. A `!must_fill` marker left in the
|
|
429
|
+
document emits the non-fatal `validation::must_fill` warning.
|
|
335
430
|
|
|
336
431
|
### Errors
|
|
337
432
|
|
|
@@ -367,20 +462,26 @@ compilation failures. The same shape applies to every throw site:
|
|
|
367
462
|
|
|
368
463
|
- `Document.fromMarkdown` — parse errors (missing root `$quill` metadata, YAML
|
|
369
464
|
errors, `parse::input_too_large` for inputs > 10 MB).
|
|
370
|
-
- `Document` mutators (`
|
|
465
|
+
- `Document` mutators (`storeField`, the writer's `set`, etc.) — `EditError`
|
|
371
466
|
variants (`InvalidFieldName`, `InvalidKindName`, `ReservedKind`,
|
|
372
|
-
`IndexOutOfRange`) appear in `diagnostics[0].message`
|
|
373
|
-
`[EditError::<Variant>]` prefix.
|
|
467
|
+
`IndexOutOfRange`, `ValueTooDeep`, `Import`) appear in `diagnostics[0].message`
|
|
468
|
+
with the `[EditError::<Variant>]` prefix.
|
|
374
469
|
- `engine.render` / `session.render` — backend compilation failures and
|
|
375
470
|
validation errors.
|
|
376
471
|
|
|
377
472
|
### Lifecycle
|
|
378
473
|
|
|
379
474
|
The wasm bindings are built with `--weak-refs`, so dropped `Document`,
|
|
380
|
-
`Quill`, and `
|
|
475
|
+
`Quill`, and `LiveSession` handles are reclaimed by `FinalizationRegistry`
|
|
381
476
|
without manual `.free()` discipline. `.free()` is still emitted as an eager
|
|
382
477
|
teardown hook for callers that want deterministic release.
|
|
383
478
|
|
|
479
|
+
`engine.render` and `engine.open` read the `quill` and `doc` handles
|
|
480
|
+
synchronously, before their first await, so freeing a handle as soon as the
|
|
481
|
+
call returns — `try { return engine.render(quill, doc); } finally
|
|
482
|
+
{ doc.free(); }` — is safe even on the first render, while the backend
|
|
483
|
+
binary is still loading.
|
|
484
|
+
|
|
384
485
|
The package floor is Node 22+ (`engines: { node: ">=22" }`) and current
|
|
385
486
|
evergreen browsers; `--weak-refs` itself only needs Node 14.6+. The `using`
|
|
386
487
|
sugar shown below ([explicit resource management][erm]) needs Node 24, but is
|
|
@@ -390,7 +491,7 @@ For environments where `using` (the [explicit resource management][erm]
|
|
|
390
491
|
proposal) hasn't landed, use an explicit `try` / `finally`:
|
|
391
492
|
|
|
392
493
|
```ts
|
|
393
|
-
const session = engine.open(quill, doc);
|
|
494
|
+
const session = await engine.open(quill, doc);
|
|
394
495
|
try {
|
|
395
496
|
for (let p = 0; p < session.pageCount; p++) {
|
|
396
497
|
session.paint(ctx, p);
|
|
@@ -405,7 +506,7 @@ try {
|
|
|
405
506
|
## Notes
|
|
406
507
|
|
|
407
508
|
- Parsed markdown requires a root `~~~` block (a bare three-tilde fence;
|
|
408
|
-
|
|
509
|
+
`~~~card-yaml` is also accepted as a non-canonical alias)
|
|
409
510
|
with a `$quill` system-metadata line. Empty input surfaces a dedicated
|
|
410
511
|
"Empty markdown input cannot be parsed" message.
|
|
411
512
|
- A `$quill` mismatch during `engine.render(quill, parsed)` is a thrown error, not a warning: rendering with a quill whose *name* differs (`quill::name_mismatch`) or whose *version* falls outside the selector (`quill::version_mismatch`) is rejected.
|
|
@@ -413,8 +514,8 @@ try {
|
|
|
413
514
|
|
|
414
515
|
## Changelog
|
|
415
516
|
|
|
416
|
-
See the [changelog](https://github.com/
|
|
417
|
-
and the [GitHub Releases](https://github.com/
|
|
517
|
+
See the [changelog](https://github.com/borb-sh/quillmark/blob/main/CHANGELOG.md)
|
|
518
|
+
and the [GitHub Releases](https://github.com/borb-sh/quillmark/releases) page for
|
|
418
519
|
release notes and version history.
|
|
419
520
|
|
|
420
521
|
## License
|