@quillmark/wasm 0.98.0 → 0.100.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 +208 -1
- package/LICENSE +13 -0
- package/README.md +116 -64
- package/backends/pdfform/wasm.d.ts +336 -146
- package/backends/pdfform/wasm_bg.js +223 -122
- package/backends/pdfform/wasm_bg.wasm +0 -0
- package/backends/pdfform/wasm_bg.wasm.d.ts +3 -1
- package/backends/typst/wasm.d.ts +336 -146
- package/backends/typst/wasm_bg.js +223 -122
- package/backends/typst/wasm_bg.wasm +0 -0
- package/backends/typst/wasm_bg.wasm.d.ts +3 -1
- package/core/wasm.d.ts +163 -96
- package/core/wasm_bg.js +206 -102
- package/core/wasm_bg.wasm +0 -0
- package/core/wasm_bg.wasm.d.ts +3 -1
- package/package.json +2 -2
- package/runtime/runtime.d.ts +151 -76
- package/runtime/runtime.js +381 -100
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,37 +78,64 @@ 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
|
|
94
94
|
invalid quill reference.
|
|
95
95
|
|
|
96
96
|
### `Document.fromMarkdown(markdown)`
|
|
97
|
-
Parse markdown to a parsed document
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
Parse markdown to a parsed document, quill-free: the **transport door**
|
|
98
|
+
(migrations, `$ext` stamping, a quill that will not load, opening a document to
|
|
99
|
+
fix its `$quill`). Throws a JS `Error` (with `.diagnostics` attached, see
|
|
100
|
+
[Errors](#errors)) on any parse failure, including a missing root `$quill`
|
|
101
|
+
metadata line, malformed YAML, and inputs over the 10 MiB
|
|
102
|
+
`parse::input_too_large` limit. A content field rests as authored; `quill.parse`
|
|
103
|
+
below is the bound door that lands it at its canonical rest.
|
|
104
|
+
|
|
105
|
+
### `quill.parse(markdown)` / `quill.conform(doc)`
|
|
106
|
+
The **bound door**, and the primary ingestion path. `quill.parse` is
|
|
107
|
+
`Document.fromMarkdown` followed by `conform`: the returned document's declared
|
|
108
|
+
content fields rest at one form per codec (a `richtext` field as the canonical
|
|
109
|
+
content object, a `plaintext` field as its literal string), so `getStored`
|
|
110
|
+
answers "corpus or string?" by the field's declared codec rather than by how the
|
|
111
|
+
document was built. Parse warnings and the `conform::*` warnings both ride
|
|
112
|
+
`doc.warnings`.
|
|
113
|
+
|
|
114
|
+
`quill.conform(doc)` is the same walk in place on a document that arrived any
|
|
115
|
+
other way (`fromJson`, a stored row), returning the `conform::*` `Diagnostic[]`
|
|
116
|
+
(`[]` when everything rested). It is idempotent and a byte no-op on an
|
|
117
|
+
already-canonical document, YAML comments included, so calling it on every load
|
|
118
|
+
is safe. A `!must_fill` marker anywhere in a field's value skips that field; a
|
|
119
|
+
value the strict write refuses stays as authored under a warning. Both throw
|
|
120
|
+
when the document declares a `$quill` this quill does not answer to, before any
|
|
121
|
+
mutation.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const doc = quill.parse(markdown); // rests canonical
|
|
125
|
+
const stale = Document.fromJson(row);
|
|
126
|
+
const diags = quill.conform(stale); // converges in place
|
|
127
|
+
```
|
|
101
128
|
|
|
102
129
|
### `doc.toMarkdown()`
|
|
103
130
|
Emit canonical Quillmark Markdown. Type-fidelity round-trip safe:
|
|
104
131
|
`Document.fromMarkdown(doc.toMarkdown())` returns a document equal to `doc`
|
|
105
132
|
under [`doc.equals`](#docequalsother). The output is **not** guaranteed
|
|
106
|
-
byte-equal to the original source
|
|
133
|
+
byte-equal to the original source: YAML quoting, key ordering, and
|
|
107
134
|
whitespace are normalised. Use `equals` (not string comparison) to test
|
|
108
135
|
semantic equality.
|
|
109
136
|
|
|
110
137
|
### `doc.toJson()`
|
|
111
|
-
Serialize the document to a versioned storage DTO
|
|
138
|
+
Serialize the document to a versioned storage DTO: a JSON **string**
|
|
112
139
|
carrying a `schema` version. Use this (not `toMarkdown`) to persist a
|
|
113
140
|
document across a process restart or crate upgrade: the wire format is
|
|
114
141
|
frozen per `schema` version, whereas Markdown syntax evolves. Parse-time
|
|
@@ -116,11 +143,11 @@ frozen per `schema` version, whereas Markdown syntax evolves. Parse-time
|
|
|
116
143
|
|
|
117
144
|
The string is produced inside the module by `serde_json`; the JS `JSON`
|
|
118
145
|
global is not involved. It is standard JSON text, so callers may
|
|
119
|
-
`JSON.parse` it to inspect it
|
|
146
|
+
`JSON.parse` it to inspect it, but it is intended as an opaque blob you
|
|
120
147
|
persist and hand back.
|
|
121
148
|
|
|
122
149
|
`toJson()` is **deterministic**: a `Document` that is `equals` to another
|
|
123
|
-
serializes to a byte-identical string
|
|
150
|
+
serializes to a byte-identical string: across repeated calls, and across
|
|
124
151
|
any crate upgrade that keeps the same `schema` version (every release does until
|
|
125
152
|
the `Document` model changes; see [Storage compatibility](#storage-compatibility-across-versions)).
|
|
126
153
|
Field order is fixed and object key order is preserved, so content hashes
|
|
@@ -145,7 +172,7 @@ not a valid storage DTO. Use it to branch on format without a heuristic or
|
|
|
145
172
|
`try`/`catch` as control flow:
|
|
146
173
|
|
|
147
174
|
```ts
|
|
148
|
-
// "JSON canonical, Markdown fallback"
|
|
175
|
+
// "JSON canonical, Markdown fallback": no exceptions, no string sniffing
|
|
149
176
|
const doc = Document.tryFromJson(content) ?? Document.fromMarkdown(content);
|
|
150
177
|
```
|
|
151
178
|
|
|
@@ -156,14 +183,14 @@ genuinely malformed Markdown.
|
|
|
156
183
|
|
|
157
184
|
The `schema` value (`quillmark/document@0.93.0`) is the **model version**,
|
|
158
185
|
not the running crate version. It is a hand-set constant, bumped only when
|
|
159
|
-
the `Document` model itself changes
|
|
186
|
+
the `Document` model itself changes, so every `0.93.x` patch release reads
|
|
160
187
|
and writes that same value.
|
|
161
188
|
|
|
162
189
|
- **Upgrading is safe.** A newer build always reads documents written by an
|
|
163
190
|
older one. Each schema version's wire format is frozen and never changes;
|
|
164
191
|
when the model does change, the new build ships a migration that converts
|
|
165
192
|
old payloads on `fromJson`. A document you commit as your canonical
|
|
166
|
-
on-disk format keeps loading across crate upgrades
|
|
193
|
+
on-disk format keeps loading across crate upgrades: there is no need to
|
|
167
194
|
pin old wasm to read old data.
|
|
168
195
|
- **Downgrading is not.** `fromJson` rejects an *unknown* (i.e. newer)
|
|
169
196
|
`schema` version rather than guessing at a format it predates. Don't feed
|
|
@@ -178,13 +205,13 @@ if (v && v !== Document.currentSchemaVersion()) {
|
|
|
178
205
|
}
|
|
179
206
|
```
|
|
180
207
|
|
|
181
|
-
`schemaVersionOf` does not validate the payload
|
|
208
|
+
`schemaVersionOf` does not validate the payload: it only reads the
|
|
182
209
|
`schema` field, returning `undefined` for non-JSON, non-objects, or
|
|
183
210
|
payloads that don't carry one. Use it to distinguish "wrong version" from
|
|
184
211
|
"corrupt" when `fromJson` throws.
|
|
185
212
|
|
|
186
213
|
In short: persist the `toJson` string, upgrade freely, never downgrade. The
|
|
187
|
-
full design
|
|
214
|
+
full design (including how migrations are added) is in
|
|
188
215
|
`prose/canon/DOCUMENT_STORAGE.md`.
|
|
189
216
|
|
|
190
217
|
### `doc.equals(other)`
|
|
@@ -201,7 +228,7 @@ Use this to validate indices before calling card mutators (`removeCard`,
|
|
|
201
228
|
|
|
202
229
|
### `quill.validate(doc)`
|
|
203
230
|
|
|
204
|
-
Returns `Diagnostic[]
|
|
231
|
+
Returns `Diagnostic[]`: the document validated against the quill schema,
|
|
205
232
|
without invoking the backend. An empty array means the document is valid.
|
|
206
233
|
Each diagnostic carries the canonical `validation::*` `code`, `path`, and
|
|
207
234
|
`hint`. Includes the non-fatal `validation::must_fill` warning for each
|
|
@@ -214,14 +241,14 @@ const errors = diagnostics.filter(d => d.severity === "error");
|
|
|
214
241
|
```
|
|
215
242
|
|
|
216
243
|
To render a form editor, read field definitions from `quill.schema` (walk
|
|
217
|
-
`fields` in key order
|
|
218
|
-
values from the `Document` payload
|
|
244
|
+
`fields` in key order: declaration order is display order) and the authored
|
|
245
|
+
values from the `Document` payload: there is no separate form-view projection.
|
|
219
246
|
|
|
220
247
|
### `quill.seedDocument()`
|
|
221
248
|
|
|
222
249
|
Returns a starter `Document` seeded from the schema: each field's `example:`
|
|
223
250
|
is committed and every other field is left absent (the render layer fills
|
|
224
|
-
`default:` → type-empty zero). Illustration-first
|
|
251
|
+
`default:` → type-empty zero). Illustration-first: a field with both an
|
|
225
252
|
`example` and a `default` renders its example. Use as the initial state for a
|
|
226
253
|
"new document" editor.
|
|
227
254
|
|
|
@@ -243,7 +270,7 @@ doc.insertCard({ kind: "note" }, 0); // insert at index 0
|
|
|
243
270
|
```
|
|
244
271
|
|
|
245
272
|
Reads and writes are two aligned shapes. A read `Card` always has `body:
|
|
246
|
-
Content` (canonical content, never a raw string)
|
|
273
|
+
Content` (canonical content, never a raw string): no narrowing, no guessing
|
|
247
274
|
whether the body was normalized. The write shape `CardInput` widens `body` to
|
|
248
275
|
`Content | string` (a markdown string imports to the content) and makes every
|
|
249
276
|
field but `kind` optional. Every `Card` is a valid `CardInput`, so `insertCard`
|
|
@@ -251,22 +278,26 @@ still takes exactly what `cards` / `removeCard` / `seedCard` return.
|
|
|
251
278
|
Build a fresh card from a flat field map with
|
|
252
279
|
`Document.makeCard(kind, fields?, body?)`.
|
|
253
280
|
|
|
254
|
-
**One address for the whole surface.** Reads and writes navigate by an `Addr
|
|
255
|
-
`{ card?, field? }`, absent `card` = main, absent `field` = body
|
|
281
|
+
**One address for the whole surface.** Reads and writes navigate by an `Addr`:
|
|
282
|
+
`{ card?, field? }`, absent `card` = main, absent `field` = body, and a bare
|
|
256
283
|
string is shorthand for `{ field }`. So `doc.storeField("qty", 3)` targets the
|
|
257
284
|
main card's `qty`, `doc.storeField({ card: 2, field: "qty" }, 3)` a composable
|
|
258
285
|
card's. Reads are total over the field axis (`getStored` → `undefined`, `isFill` → `false` for
|
|
259
286
|
an absent field; only an out-of-range card throws); field writes throw on a body
|
|
260
287
|
address. `getStored` is the verbatim transport read, distinct from the interpreted
|
|
261
288
|
`quill.reader(doc).get`; `getMarkdown` is the body markdown read (a `CardAddr`; a field's
|
|
262
|
-
markdown is read through `quill.reader(doc).get(field)`).
|
|
289
|
+
markdown is read through `quill.reader(doc).get(field)`). A content field's stored
|
|
290
|
+
form follows how the document was built (a canonical content object when the
|
|
291
|
+
typed writer committed it, the authored string when a markdown parse produced
|
|
292
|
+
it), so for the corpus either way read `quill.reader(doc).getContent(addr)`, which
|
|
293
|
+
decodes through the codec the field's declared type names. Card-scoped verbs take a
|
|
263
294
|
`CardAddr` (`{ card? }`) first: `doc.getExt({ card: 2 })`, and the batch below.
|
|
264
295
|
|
|
265
296
|
Batch mutation: `doc.storeFields({}, {...})` / `doc.storeFields({ card: index }, {...})`
|
|
266
|
-
apply a whole object atomically
|
|
297
|
+
apply a whole object atomically: on any invalid field nothing is applied and
|
|
267
298
|
the thrown error carries one diagnostic per offending field (`path` = field
|
|
268
299
|
name). The address is first (never shape-overloaded, since `card` is a legal
|
|
269
|
-
field name), and parses strictly
|
|
300
|
+
field name), and parses strictly: a stray key throws rather than silently
|
|
270
301
|
reading as `{}`. The main card is `{}`, or **`MAIN_CARD_ADDR`** (from
|
|
271
302
|
`@quillmark/wasm/runtime`), a frozen alias that spells the intent:
|
|
272
303
|
`doc.storeFields(MAIN_CARD_ADDR, {...})`.
|
|
@@ -277,12 +308,12 @@ A `Document` holds only a `$quill` *reference*, not the resolved schema, so type
|
|
|
277
308
|
writes go through the schema-bound writer while the quill-free opaque store sits
|
|
278
309
|
on `Document` itself (**store** = verbatim, **set** = typed):
|
|
279
310
|
|
|
280
|
-
- **`quill.writer(doc)
|
|
311
|
+
- **`quill.writer(doc)`: the typed door whenever a quill is in hand.** Bind the
|
|
281
312
|
schema once and issue bare `set` / `setAll` / `setBody` / `reviseField` /
|
|
282
313
|
`addCard` / `card(i)`. Each resolves the field's schema `type`, coerces the
|
|
283
314
|
value to its canonical form (`"3"` → `3`, a markdown string → a richtext
|
|
284
315
|
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
|
|
316
|
+
does not declare throws `UnknownField` rather than falling to the opaque store:
|
|
286
317
|
on the typed path an undeclared name is a typo, not a fallback. The batch form
|
|
287
318
|
(`setAll`) is all-or-nothing: an undeclared name aborts the whole write and its
|
|
288
319
|
per-field diagnostics name every offending field, so a whole-form submit
|
|
@@ -290,21 +321,21 @@ on `Document` itself (**store** = verbatim, **set** = typed):
|
|
|
290
321
|
carries the quill-taking `_commitField` / `_commitFields` / `_addCard` /
|
|
291
322
|
`_reviseField` ABI the writer delegates to, hidden from the `.d.ts`.)
|
|
292
323
|
|
|
293
|
-
- **`store
|
|
324
|
+
- **`store*`: the deliberate quill-free primitive.** `doc.storeField(addr, value)`
|
|
294
325
|
/ `doc.storeFields(cardAddr, {...})` (and `storeFill`) validate only the field
|
|
295
326
|
name/depth/kind and store the value verbatim, no quill required. Reach for it
|
|
296
327
|
on purpose when you *want* the opaque store: quill-agnostic storage/migration
|
|
297
328
|
infra that has no bundle and must write regardless of a drifted schema;
|
|
298
329
|
store-now-validate-later editors holding in-progress input that `commit`
|
|
299
330
|
would reject; or verbatim passthrough of fields the schema doesn't own. It is
|
|
300
|
-
the lower layer, not a lighter `commit
|
|
331
|
+
the lower layer, not a lighter `commit`: a typo'd field name stores silently
|
|
301
332
|
and only surfaces at `quill.validate` / render.
|
|
302
333
|
|
|
303
334
|
Per-keystroke cost is the same either way (both mutate the in-memory `Document`
|
|
304
335
|
in place; no seam is crossed), so steering to the writer buys the type check for
|
|
305
336
|
free.
|
|
306
337
|
|
|
307
|
-
#### `DocumentWriter` / `CardWriter
|
|
338
|
+
#### `DocumentWriter` / `CardWriter`: bind the quill once
|
|
308
339
|
|
|
309
340
|
`quill.writer(doc)` binds the quill's schema to the document once, so a form
|
|
310
341
|
editor or MCP writer that holds both issues bare verbs (the writer forwards to
|
|
@@ -315,25 +346,43 @@ const ed = quill.writer(doc); // Rust `quill.writer(doc)`
|
|
|
315
346
|
ed.set("subject", "Q3 results"); // strict-committed to the schema type
|
|
316
347
|
ed.setAll({ qty: "3", subject: "Q3" }); // all-or-nothing batch
|
|
317
348
|
ed.reviseField("subject", "Q3 **results**"); // typed AND anchor-preserving; returns a Delta
|
|
318
|
-
ed.set("titel", "x"); // throws UnknownField
|
|
349
|
+
ed.set("titel", "x"); // throws UnknownField: a typo, not a fallback
|
|
319
350
|
ed.card(2).set("body", "**note**"); // composable card, resolved by its $kind
|
|
320
351
|
```
|
|
321
352
|
|
|
322
353
|
`DocumentWriter` / `CardWriter` are pure JS holding references to your existing
|
|
323
|
-
`quill` and `doc
|
|
354
|
+
`quill` and `doc`: no WASM handle of their own, nothing to `free()`. `card(i)`
|
|
324
355
|
is lazy: it never throws; an out-of-range index throws `IndexOutOfRange` at the
|
|
325
356
|
write.
|
|
326
357
|
|
|
358
|
+
#### `DocumentReader` / `CardReader`: the read twin
|
|
359
|
+
|
|
360
|
+
`quill.reader(doc)` carries the writer's ephemerality and its schema authority:
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
const v = quill.reader(doc);
|
|
364
|
+
v.get("subject"); // by declared type: richtext → markdown, plaintext → literal text
|
|
365
|
+
v.getContent("subject"); // the same read as a Content corpus, whichever lane stored it
|
|
366
|
+
v.getBody(); // the main body markdown (quill-free)
|
|
367
|
+
v.card(0).get("body"); // a card field, resolved by its $kind
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
`get` projects and `getContent` returns the corpus; both decode through the codec
|
|
371
|
+
the field's **declared type** names, which is why they bind the quill and the
|
|
372
|
+
verbatim `doc.getStored` does not. An undeclared name throws `UnknownField`, a
|
|
373
|
+
type that is not a content leaf throws `FieldNotContent`, and an undecodable
|
|
374
|
+
value throws `FieldRichtextDecode`; an absent field reads back `undefined`.
|
|
375
|
+
|
|
327
376
|
### `engine.render(quill, parsed, opts?)` vs. `engine.open(quill, parsed)`
|
|
328
377
|
|
|
329
|
-
Use **`engine.render`** for one-shot exports (PDF/SVG/PNG)
|
|
378
|
+
Use **`engine.render`** for one-shot exports (PDF/SVG/PNG): compiles, emits
|
|
330
379
|
artifacts, done. Use **`LiveSession`** (returned by `engine.open`) for
|
|
331
380
|
reactive previews: the session is a persistent compiler. `paint` / `render` /
|
|
332
381
|
`regions` / `fieldAt` read its current compile without recompiling, and `apply(doc)`
|
|
333
382
|
recompiles in place on each edit, returning a `ChangeSet` whose `dirtyPages`
|
|
334
|
-
tells you which pages to repaint (`dirty ∩ visible`). Apply is transactional
|
|
383
|
+
tells you which pages to repaint (`dirty ∩ visible`). Apply is transactional:
|
|
335
384
|
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
|
|
385
|
+
per export, and don't re-open per edit: `apply` instead.
|
|
337
386
|
|
|
338
387
|
### `engine.render(quill, parsed, opts?)`
|
|
339
388
|
Render a pre-parsed `Document` against `quill`. Throws an
|
|
@@ -344,7 +393,7 @@ declared backend.
|
|
|
344
393
|
Open once, render all or selected pages (`opts.pages`).
|
|
345
394
|
|
|
346
395
|
The session also exposes `pageCount`, `backendId`, `supportsCanvas`,
|
|
347
|
-
`warnings` (non-fatal diagnostics of the current compile
|
|
396
|
+
`warnings` (non-fatal diagnostics of the current compile: set at `open`,
|
|
348
397
|
refreshed by each committed `apply`),
|
|
349
398
|
`apply(doc)` for in-place recompiles, `pageSize(page)`, and
|
|
350
399
|
`paint(ctx, page, opts?)` for canvas previews. See below.
|
|
@@ -361,7 +410,7 @@ render a "no pages to preview" UI without relying on the throw.
|
|
|
361
410
|
`OffscreenCanvasRenderingContext2D` (Worker), skipping PNG/SVG byte
|
|
362
411
|
round-trips.
|
|
363
412
|
|
|
364
|
-
The painter owns `canvas.width` / `canvas.height
|
|
413
|
+
The painter owns `canvas.width` / `canvas.height`: it sizes the backing
|
|
365
414
|
store itself. Consumers own `canvas.style.*` (or the layout system that
|
|
366
415
|
sets them) and read `layoutWidth` / `layoutHeight` from the returned
|
|
367
416
|
`PaintResult`.
|
|
@@ -390,16 +439,16 @@ canvas.style.height = `${result.layoutHeight}px`;
|
|
|
390
439
|
clamped page renders soft at the same `canvas.style` size.
|
|
391
440
|
- `paint` writes the whole backing store with `putImageData`, which
|
|
392
441
|
ignores the 2D context transform, `globalAlpha`, and clip. Give each
|
|
393
|
-
visible page its own
|
|
442
|
+
visible page its own `<canvas>` element: you cannot composite two pages,
|
|
394
443
|
a sub-rect, or a context transform through `paint`.
|
|
395
|
-
- `paint` is always a full repaint
|
|
444
|
+
- `paint` is always a full repaint: setting the backing-store width /
|
|
396
445
|
height clears it. No `clearRect` required. Each call re-rasterizes from
|
|
397
446
|
scratch (no per-page raster cache), so keep a page's canvas alive while
|
|
398
447
|
it stays near the viewport rather than pooling one canvas across pages:
|
|
399
448
|
an idle canvas retains its pixels for free, whereas reusing a canvas on
|
|
400
449
|
scroll re-runs a full render.
|
|
401
450
|
- `pageCount` and `pageSize(page)` are stable for the session's
|
|
402
|
-
lifetime (immutable snapshot)
|
|
451
|
+
lifetime (immutable snapshot): cache them.
|
|
403
452
|
- Worker support: pass an `OffscreenCanvasRenderingContext2D` and the
|
|
404
453
|
same call signature works. `layoutWidth` / `layoutHeight` are
|
|
405
454
|
informational in that mode (no CSS layout box); fold everything into
|
|
@@ -415,14 +464,14 @@ canvas.style.height = `${result.layoutHeight}px`;
|
|
|
415
464
|
|
|
416
465
|
A field's *cell* is inferred from whether its schema declares a `default:`:
|
|
417
466
|
|
|
418
|
-
- **Unendorsed** (no `default:`)
|
|
467
|
+
- **Unendorsed** (no `default:`): `quill.blueprint` renders the
|
|
419
468
|
`!must_fill` marker in the value cell (carrying the field's `example` as a
|
|
420
469
|
suggested value when one exists). An absent Unendorsed field zero-fills
|
|
421
470
|
silently. A `!must_fill` marker left in the document is non-fatal: it emits
|
|
422
471
|
the `validation::must_fill` warning and still renders. Partial documents
|
|
423
472
|
are accepted; `engine.render(quill, doc)` only throws for malformed
|
|
424
473
|
input.
|
|
425
|
-
- **Endorsed** (with `default:`)
|
|
474
|
+
- **Endorsed** (with `default:`): `quill.blueprint` renders the
|
|
426
475
|
default value with a type-only `# <type>` annotation (shippable as-is),
|
|
427
476
|
and the default is used when the document omits the field.
|
|
428
477
|
|
|
@@ -431,7 +480,7 @@ document emits the non-fatal `validation::must_fill` warning.
|
|
|
431
480
|
|
|
432
481
|
### Errors
|
|
433
482
|
|
|
434
|
-
Every method that can fail throws a **`QuillmarkError
|
|
483
|
+
Every method that can fail throws a **`QuillmarkError`**: a JS `Error` with
|
|
435
484
|
`.diagnostics` attached. The type and a guard are exported from the root:
|
|
436
485
|
|
|
437
486
|
```ts
|
|
@@ -443,17 +492,17 @@ try {
|
|
|
443
492
|
if (isQuillmarkError(e)) {
|
|
444
493
|
for (const d of e.diagnostics) console.error(d.severity, d.message);
|
|
445
494
|
} else {
|
|
446
|
-
throw e; // not a quillmark failure
|
|
495
|
+
throw e; // not a quillmark failure: programming error, re-throw
|
|
447
496
|
}
|
|
448
497
|
}
|
|
449
498
|
```
|
|
450
499
|
|
|
451
|
-
`QuillmarkError` is a **structural interface, not a class
|
|
500
|
+
`QuillmarkError` is a **structural interface, not a class**: the WASM layer
|
|
452
501
|
throws a real `Error` and attaches the property, so there is no constructor to
|
|
453
502
|
`instanceof` against; narrow with `isQuillmarkError` (which also works on
|
|
454
503
|
errors from any build or WASM instance in the page).
|
|
455
504
|
|
|
456
|
-
`diagnostics` is always non-empty
|
|
505
|
+
`diagnostics` is always non-empty: length 1 for most failures, length N for
|
|
457
506
|
backend compilation errors. `message` is derived from `diagnostics`
|
|
458
507
|
(`diagnostics[0].message` for single-diagnostic errors; an aggregate
|
|
459
508
|
`"<N> error(s): <first.message>"` summary for compilation failures).
|
|
@@ -461,15 +510,20 @@ backend compilation errors. `message` is derived from `diagnostics`
|
|
|
461
510
|
Read `err.diagnostics[0]` for the primary diagnostic; iterate the array for
|
|
462
511
|
compilation failures. The same shape applies to every throw site:
|
|
463
512
|
|
|
464
|
-
- `Document.fromMarkdown
|
|
465
|
-
errors, `parse::input_too_large` for inputs > 10
|
|
466
|
-
- `Document` mutators (`storeField`, the writer's `set`, etc.)
|
|
513
|
+
- `Document.fromMarkdown`: parse errors (missing root `$quill` metadata, YAML
|
|
514
|
+
errors, `parse::input_too_large` for inputs > 10 MiB).
|
|
515
|
+
- `Document` mutators (`storeField`, the writer's `set`, etc.): mutator
|
|
467
516
|
failures carry a namespaced `edit::*` `code` on `diagnostics[0]`
|
|
468
517
|
(`edit::invalid_field_name`, `edit::unknown_field`, `edit::index_out_of_range`,
|
|
469
518
|
`edit::field_conform`, …). Route on `diagnostics[0].code`, never on message
|
|
470
519
|
text.
|
|
471
|
-
- `engine.render` / `session.render
|
|
520
|
+
- `engine.render` / `session.render`: backend compilation failures and
|
|
472
521
|
validation errors.
|
|
522
|
+
- Any method taking a `Quill` or `Document`: a handle from a *second* copy of
|
|
523
|
+
`@quillmark/wasm` is refused with `runtime::foreign_handle`, hinting `npm ls
|
|
524
|
+
@quillmark/wasm`. Two copies are two WASM memories and two `Quill`/`Document`
|
|
525
|
+
classes; dedupe to one. A value that is not a handle at all keeps its own
|
|
526
|
+
`runtime::not_a_document` / `runtime::not_a_quill`.
|
|
473
527
|
|
|
474
528
|
### Lifecycle
|
|
475
529
|
|
|
@@ -480,17 +534,15 @@ teardown hook for callers that want deterministic release.
|
|
|
480
534
|
|
|
481
535
|
`engine.render` and `engine.open` read the `quill` and `doc` handles
|
|
482
536
|
synchronously, before their first await, so freeing a handle as soon as the
|
|
483
|
-
call returns
|
|
484
|
-
{ doc.free(); }
|
|
537
|
+
call returns: `try { return engine.render(quill, doc); } finally
|
|
538
|
+
{ doc.free(); }`: is safe even on the first render, while the backend
|
|
485
539
|
binary is still loading.
|
|
486
540
|
|
|
487
541
|
The package floor is Node 22+ (`engines: { node: ">=22" }`) and current
|
|
488
542
|
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`:
|
|
543
|
+
sugar ([explicit resource management][erm]) needs Node 24 and is optional.
|
|
544
|
+
Where it hasn't landed, an explicit `try` / `finally` runs on the Node 22
|
|
545
|
+
floor:
|
|
494
546
|
|
|
495
547
|
```ts
|
|
496
548
|
const session = await engine.open(quill, doc);
|