@quillmark/wasm 0.112.0 → 0.114.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 +1612 -0
- package/README.md +91 -66
- package/core/wasm.d.ts +153 -183
- package/core/wasm.js +283 -391
- package/core/wasm_bg.wasm +0 -0
- package/core/wasm_bg.wasm.d.ts +6 -11
- package/package.json +2 -2
- package/{backends/pdfform → render}/wasm.d.ts +204 -265
- package/{backends/pdfform → render}/wasm.js +364 -450
- package/{backends/typst → render}/wasm_bg.wasm +0 -0
- package/{backends/typst → render}/wasm_bg.wasm.d.ts +8 -15
- package/runtime/runtime.d.ts +88 -179
- package/runtime/runtime.js +144 -585
- package/backends/pdfform/wasm_bg.wasm +0 -0
- package/backends/pdfform/wasm_bg.wasm.d.ts +0 -107
- package/backends/typst/wasm.d.ts +0 -1511
- package/backends/typst/wasm.js +0 -3008
package/README.md
CHANGED
|
@@ -15,10 +15,10 @@ The package has one import surface: `@quillmark/wasm`, whose `init` resolves to
|
|
|
15
15
|
`Quill` and `Document` are the internal Typst-less core build's own classes,
|
|
16
16
|
handed out verbatim by `init`, so editor/validation code (`Quill.fromTree`,
|
|
17
17
|
`Document.fromMarkdown`) loads only that small core binary: no backend is
|
|
18
|
-
loaded until you render. The `Engine` hides everything else:
|
|
19
|
-
(`typst`, `
|
|
20
|
-
memory, lazily loaded on the first render. The Engine clones a
|
|
21
|
-
`Document` into
|
|
18
|
+
loaded until you render. The `Engine` hides everything else: the render build,
|
|
19
|
+
carrying both backends (`typst`, `acroform`), is a private WASM binary with its
|
|
20
|
+
own linear memory, lazily loaded on the first render. The Engine clones a
|
|
21
|
+
`Quill` / `Document` into that memory as data and frees the clones: you never
|
|
22
22
|
hold a backend object or cross a memory boundary yourself.
|
|
23
23
|
|
|
24
24
|
## Build
|
|
@@ -27,9 +27,9 @@ hold a backend object or cross a memory boundary yourself.
|
|
|
27
27
|
bash scripts/build-wasm.sh
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
The script builds
|
|
31
|
-
(default features),
|
|
32
|
-
|
|
30
|
+
The script builds two variants, the core (no backend) and the render build
|
|
31
|
+
(default features, both backends), each with `--target web` and `--weak-refs`
|
|
32
|
+
enabled (see
|
|
33
33
|
[Initialization](#initialization) and [Lifecycle](#lifecycle)). It then asserts
|
|
34
34
|
none of them carries a `.wasm` ESM import or a top-level await.
|
|
35
35
|
|
|
@@ -119,18 +119,17 @@ A load failure surfaces as `runtime::init_failed`, whose hint names that line.
|
|
|
119
119
|
Create the render dispatcher. Routes each quill to its backend by
|
|
120
120
|
`quill.backendId`, lazily loads that backend binary, and renders: cloning the
|
|
121
121
|
quill/document into the backend's memory and freeing the clones internally.
|
|
122
|
-
`render`, `open`,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
never cloning the quill. Use them as non-failing pre-render probes.
|
|
122
|
+
`render`, `open`, and `supportedFormats` are **async** (the first call may load
|
|
123
|
+
a backend). Pass `{ backends }` to register or override backend descriptors.
|
|
124
|
+
Each entry is a descriptor (`{ [backendId]: { load, formats } }`) where `load`
|
|
125
|
+
is the lazy thunk returning the backend module and `formats` is the
|
|
126
|
+
**required** static capability manifest. A malformed descriptor throws at
|
|
127
|
+
`new Engine(...)`, naming the backend id.
|
|
128
|
+
|
|
129
|
+
**The format probe is always free.** `supportedFormats` depends only on
|
|
130
|
+
`quill.backendId`, and answers from the descriptor's required `formats`
|
|
131
|
+
manifest: never loading the multi-MB backend binary and never cloning the
|
|
132
|
+
quill. Use it as a non-failing pre-render probe.
|
|
134
133
|
|
|
135
134
|
### The two doors: `Document.fromMarkdown` vs `quill.parse` / `quill.conform`
|
|
136
135
|
|
|
@@ -148,7 +147,7 @@ document was built. Parse warnings and the `conform::*` warnings both ride
|
|
|
148
147
|
`doc.warnings`.
|
|
149
148
|
|
|
150
149
|
`quill.conform(doc)` is the same walk in place on a document that arrived any
|
|
151
|
-
other way (`
|
|
150
|
+
other way (`fromStored`, a stored row), returning the `conform::*` `Diagnostic[]`
|
|
152
151
|
(`[]` when everything rested). It is idempotent and a byte no-op on an
|
|
153
152
|
already-canonical document, YAML comments included, so calling it on every load
|
|
154
153
|
is safe. A `!must_fill` marker anywhere in a field's value skips that field; a
|
|
@@ -158,19 +157,22 @@ mutation.
|
|
|
158
157
|
|
|
159
158
|
```ts
|
|
160
159
|
const doc = quill.parse(markdown); // rests canonical
|
|
161
|
-
const stale = Document.
|
|
160
|
+
const stale = Document.fromStored(row);
|
|
162
161
|
const diags = quill.conform(stale); // converges in place
|
|
163
162
|
```
|
|
164
163
|
|
|
165
164
|
### Storage compatibility across versions
|
|
166
165
|
|
|
167
|
-
Persist `doc.
|
|
166
|
+
Persist `doc.toStored()`, not `doc.toMarkdown()`: the DTO wire format is frozen
|
|
168
167
|
per `schema` version, whereas Markdown syntax evolves, and `toMarkdown` output
|
|
169
|
-
is normalised rather than byte-equal to the source. `Document.
|
|
170
|
-
discriminates the two formats without exceptions as control flow:
|
|
168
|
+
is normalised rather than byte-equal to the source. `Document.storageVersionOf`
|
|
169
|
+
discriminates the two formats without exceptions as control flow: it answers
|
|
170
|
+
`undefined` for anything that is not a storage DTO.
|
|
171
171
|
|
|
172
172
|
```ts
|
|
173
|
-
const doc = Document.
|
|
173
|
+
const doc = Document.storageVersionOf(content)
|
|
174
|
+
? Document.fromStored(content)
|
|
175
|
+
: Document.fromMarkdown(content);
|
|
174
176
|
```
|
|
175
177
|
|
|
176
178
|
The `schema` value (`quillmark/document@0.112.0`) is the **model version**,
|
|
@@ -178,13 +180,16 @@ not the running crate version. It is a hand-set constant, bumped only when
|
|
|
178
180
|
the `Document` model itself changes, so every `0.112.x` patch release reads
|
|
179
181
|
and writes that same value.
|
|
180
182
|
|
|
181
|
-
- **Upgrading is safe.** A newer build
|
|
182
|
-
|
|
183
|
-
when the model does change, the new build ships a migration that
|
|
184
|
-
old payloads on `
|
|
185
|
-
on-disk format keeps loading across crate upgrades: there is no
|
|
186
|
-
pin old wasm to read old data.
|
|
187
|
-
|
|
183
|
+
- **Upgrading is safe.** A newer build reads documents an older build's
|
|
184
|
+
*writer* produced. Each schema version's wire format is frozen and never
|
|
185
|
+
changes; when the model does change, the new build ships a migration that
|
|
186
|
+
converts old payloads on `fromStored`. A document you commit as your
|
|
187
|
+
canonical on-disk format keeps loading across crate upgrades: there is no
|
|
188
|
+
need to pin old wasm to read old data. The exception is a row a host
|
|
189
|
+
authored a content construct of its own into — a line `kind`, container,
|
|
190
|
+
mark `type`, island `type` or `loss` outside the vocabulary. Those are
|
|
191
|
+
refused from 0.113 on; see that release's migration guide.
|
|
192
|
+
- **Downgrading is not.** `fromStored` rejects an *unknown* (i.e. newer)
|
|
188
193
|
`schema` version rather than guessing at a format it predates. Don't feed
|
|
189
194
|
documents written by a newer build back into an older one.
|
|
190
195
|
|
|
@@ -200,9 +205,9 @@ if (v && v !== Document.currentStorageVersion()) {
|
|
|
200
205
|
`storageVersionOf` does not validate the payload: it only reads the
|
|
201
206
|
`schema` field, returning `undefined` for non-JSON, non-objects, or
|
|
202
207
|
payloads that don't carry one. Use it to distinguish "wrong version" from
|
|
203
|
-
"corrupt" when `
|
|
208
|
+
"corrupt" when `fromStored` throws.
|
|
204
209
|
|
|
205
|
-
In short: persist the `
|
|
210
|
+
In short: persist the `toStored` string, upgrade freely, never downgrade. The
|
|
206
211
|
full design (including how migrations are added) is in
|
|
207
212
|
`prose/canon/DOCUMENT_STORAGE.md`.
|
|
208
213
|
|
|
@@ -220,9 +225,12 @@ accepts directly:
|
|
|
220
225
|
|
|
221
226
|
```ts
|
|
222
227
|
doc.insertCard(quill.seedCard("note")); // seed → append
|
|
223
|
-
doc.insertCard(Document.makeCard("note", { x: 1 })); // build from a flat map
|
|
224
228
|
doc.insertCard({ kind: "note", body: "Plain **markdown**." }); // bare inline
|
|
225
229
|
doc.insertCard({ kind: "note" }, 0); // insert at index 0
|
|
230
|
+
doc.insertCard({ // fields → payload items
|
|
231
|
+
kind: "note",
|
|
232
|
+
payloadItems: [{ type: "field", key: "x", value: 1 }],
|
|
233
|
+
});
|
|
226
234
|
```
|
|
227
235
|
|
|
228
236
|
Reads and writes are two aligned shapes. A read `Card` always has `body:
|
|
@@ -230,9 +238,9 @@ Content` (canonical content, never a raw string): no narrowing, no guessing
|
|
|
230
238
|
whether the body was normalized. The write shape `CardInput` widens `body` to
|
|
231
239
|
`Content | string` (a markdown string imports to the content) and makes every
|
|
232
240
|
field but `kind` optional. Every `Card` is a valid `CardInput`, so `insertCard`
|
|
233
|
-
still takes exactly what `cards` / `removeCard` / `seedCard` return.
|
|
234
|
-
|
|
235
|
-
`
|
|
241
|
+
still takes exactly what `cards` / `removeCard` / `seedCard` return. A fresh
|
|
242
|
+
card is an object literal: one `{ type: "field", key, value }` per field in
|
|
243
|
+
`payloadItems`, in the order they should appear.
|
|
236
244
|
|
|
237
245
|
**One address for the whole surface.** Reads and writes navigate by an `Addr`:
|
|
238
246
|
`{ card?, field? }`, absent `card` = main, absent `field` = body, and a bare
|
|
@@ -243,10 +251,11 @@ an absent field; only an out-of-range card throws); field writes throw on a body
|
|
|
243
251
|
address. `getStored` is the verbatim transport read, distinct from the interpreted
|
|
244
252
|
`quill.reader(doc).get`; `bodyMarkdown` is the body markdown read (a `CardAddr`; a field's
|
|
245
253
|
markdown is read through `quill.reader(doc).get(field)`). A content field's stored
|
|
246
|
-
form
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
decodes through the codec the
|
|
254
|
+
form rests per its declared codec through the bound door (a `richtext` field as
|
|
255
|
+
the content object, a `plaintext` field as its literal string), and as authored
|
|
256
|
+
through the transport door. For the `Content` either way read
|
|
257
|
+
`quill.reader(doc).getContent(addr)`, which decodes through the codec the
|
|
258
|
+
field's declared type names. Card-scoped verbs take a
|
|
250
259
|
`CardAddr` (`{ card? }`) first: `doc.getExt({ card: 2 })`, and the batch below.
|
|
251
260
|
|
|
252
261
|
Batch mutation: `doc.storeFields({}, {...})` / `doc.storeFields({ card: index }, {...})`
|
|
@@ -255,7 +264,7 @@ the thrown error carries one diagnostic per offending field (`path` = field
|
|
|
255
264
|
name). The address is first (never shape-overloaded, since `card` is a legal
|
|
256
265
|
field name), and parses strictly: a stray key throws rather than silently
|
|
257
266
|
reading as `{}`. The main card is `{}`, or **`MAIN_CARD_ADDR`** (from
|
|
258
|
-
`@quillmark/wasm
|
|
267
|
+
`@quillmark/wasm`), a frozen alias that spells the intent:
|
|
259
268
|
`doc.storeFields(MAIN_CARD_ADDR, {...})`.
|
|
260
269
|
|
|
261
270
|
### Typed writes: `commit*` is the default, `store*` is the quill-free primitive
|
|
@@ -317,34 +326,43 @@ write.
|
|
|
317
326
|
|
|
318
327
|
```ts
|
|
319
328
|
const v = quill.reader(doc);
|
|
320
|
-
v.get("subject"); //
|
|
329
|
+
v.get("subject"); // the values form: every content leaf as its codec's text, else as stored
|
|
321
330
|
v.getContent("subject"); // the same read as a `Content`, whichever lane stored it
|
|
322
331
|
v.bodyMarkdown(); // the main body markdown (quill-free)
|
|
323
332
|
v.card(0).get("body"); // a card field, resolved by its $kind
|
|
333
|
+
v.resolve(); // the render view: blank-filled, coerced, each field tagged with its rung
|
|
324
334
|
```
|
|
325
335
|
|
|
326
336
|
`get` projects and `getContent` returns the `Content`; both decode through the codec
|
|
327
337
|
the field's **declared type** names, which is why they bind the quill and the
|
|
328
338
|
verbatim `doc.getStored` does not. An undeclared name throws `UnknownField`, a
|
|
329
339
|
type that is not a content leaf throws `FieldNotContent`, and an undecodable
|
|
330
|
-
value throws `FieldDecode`; an absent field reads back `undefined
|
|
340
|
+
value throws `FieldDecode`; an absent field reads back `undefined` and a
|
|
341
|
+
present-null `null`. A read never coerces a scalar (`qty: "3"` reads `"3"`);
|
|
342
|
+
`resolve()` is the coerced view.
|
|
331
343
|
|
|
332
344
|
### `engine.render(quill, parsed, opts?)` vs. `engine.open(quill, parsed)`
|
|
333
345
|
|
|
334
346
|
Use **`engine.render`** for one-shot exports (PDF/SVG/PNG): compiles, emits
|
|
335
347
|
artifacts, done. Use **`LiveSession`** (returned by `engine.open`) for
|
|
336
348
|
reactive previews: the session is a persistent compiler. `paint` / `render` /
|
|
337
|
-
`regions` / `fieldAt` read its current compile without recompiling, and `
|
|
349
|
+
`regions` / `fieldAt` read its current compile without recompiling, and `update(doc)`
|
|
338
350
|
recompiles in place on each edit, returning a `ChangeSet` whose `dirtyPages`
|
|
339
|
-
tells you which pages to repaint (`dirty ∩ visible`).
|
|
351
|
+
tells you which pages to repaint (`dirty ∩ visible`). `update` is transactional:
|
|
340
352
|
on throw, every read keeps serving the last-good compile. Don't open a session
|
|
341
|
-
per export, and don't re-open per edit: `
|
|
353
|
+
per export, and don't re-open per edit: `update` instead.
|
|
342
354
|
|
|
343
355
|
A document that compiles to zero pages still produces a valid session
|
|
344
356
|
(`pageCount === 0`); `paint(ctx, 0)` and `pageSize(0)` then throw. Branch on
|
|
345
357
|
`pageCount === 0` to render a "no pages to preview" UI rather than relying on
|
|
346
358
|
the throw.
|
|
347
359
|
|
|
360
|
+
Their `warnings` differ in reach. `engine.render` returns one list for the
|
|
361
|
+
whole pipeline: `doc.warnings` (parse, `conform::*`,
|
|
362
|
+
`plate::unsupported_construct`) ahead of the compile's own. A session outlives
|
|
363
|
+
the document it opened from, so `session.render` and `session.warnings` carry
|
|
364
|
+
the compile half alone — read `doc.warnings` beside them.
|
|
365
|
+
|
|
348
366
|
### Canvas Preview
|
|
349
367
|
|
|
350
368
|
`session.paint(ctx, page, opts?)` rasterizes a page directly into a
|
|
@@ -378,14 +396,16 @@ canvas.style.height = `${result.layoutHeight}px`;
|
|
|
378
396
|
a page's canvas alive while it stays near the viewport: an idle canvas retains
|
|
379
397
|
its pixels for free, whereas pooling one canvas across pages re-renders on
|
|
380
398
|
every scroll.
|
|
381
|
-
- `pageCount` and `pageSize(page)`
|
|
382
|
-
them.
|
|
399
|
+
- `pageCount` and `pageSize(page)` read the current compile, not the session:
|
|
400
|
+
cache them between committed `update`s only. After one, the count is
|
|
401
|
+
`ChangeSet.pageCount`, and every page in `ChangeSet.dirtyPages` needs its
|
|
402
|
+
`pageSize` re-read.
|
|
383
403
|
- In a Worker, pass an `OffscreenCanvasRenderingContext2D`; the layout
|
|
384
404
|
dimensions are informational there. Loading the WASM module inside the Worker
|
|
385
405
|
is the host's responsibility.
|
|
386
|
-
-
|
|
387
|
-
|
|
388
|
-
|
|
406
|
+
- `paint` / `pageSize` throw on a page the compile does not have, a zero-page
|
|
407
|
+
compile included, naming the index and the `pageCount` that excludes it. That
|
|
408
|
+
throw is the whole contract: open the session and handle it.
|
|
389
409
|
|
|
390
410
|
### Schema model
|
|
391
411
|
|
|
@@ -430,9 +450,9 @@ try {
|
|
|
430
450
|
|
|
431
451
|
**Delivery follows the function, not the failure.** A synchronous method throws;
|
|
432
452
|
a promise-returning one rejects. The promise-returning surface is `init` and the
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
453
|
+
three `Engine` verbs (`render`, `open`, `supportedFormats`), so a programming
|
|
454
|
+
error reached through one of them (a foreign handle, an unregistered backend)
|
|
455
|
+
rejects like any other failure. Nothing here both returns
|
|
436
456
|
a promise and throws, so a `.catch` on a promise-returning call is a whole
|
|
437
457
|
guard.
|
|
438
458
|
|
|
@@ -447,21 +467,26 @@ applies to every throw site:
|
|
|
447
467
|
|
|
448
468
|
- `Document.fromMarkdown`: parse errors (missing root `$quill` metadata, YAML
|
|
449
469
|
errors, `parse::input_too_large` for inputs > 10 MiB).
|
|
450
|
-
- `Document` mutators (`storeField`, the writer's
|
|
451
|
-
failures carry a namespaced `edit::*` `code` on
|
|
452
|
-
(`edit::invalid_field_name`, `edit::unknown_field`,
|
|
453
|
-
`edit::field_coercion_failed`, …). Route on
|
|
454
|
-
text.
|
|
470
|
+
- `Document` mutators (`storeField`, `insertCard`, the writer's
|
|
471
|
+
`set`, etc.): mutator failures carry a namespaced `edit::*` `code` on
|
|
472
|
+
`diagnostics[0]` (`edit::invalid_field_name`, `edit::unknown_field`,
|
|
473
|
+
`edit::index_out_of_range`, `edit::field_coercion_failed`, …). Route on
|
|
474
|
+
`diagnostics[0].code`, never on message text.
|
|
455
475
|
- `engine.render` / `session.render`: backend compilation failures and
|
|
456
476
|
validation errors.
|
|
457
477
|
- `engine.render(quill, parsed)` against a quill whose *name* differs
|
|
458
478
|
(`quill::name_mismatch`) or whose *version* falls outside the document's
|
|
459
479
|
selector (`quill::version_mismatch`): a throw, never a warning.
|
|
460
|
-
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
480
|
+
- The four `Engine` verbs against a quill whose declared `backend:` is not in
|
|
481
|
+
the registry: `engine::backend_not_found`, the code core raises for the same
|
|
482
|
+
condition, hinting the registered ids.
|
|
483
|
+
- The `Engine` verbs, `session.update`, and the writer/reader binds against a
|
|
484
|
+
value that is not one of *this* copy's handles — the wrong type, or the right
|
|
485
|
+
class from a second copy of `@quillmark/wasm`: `runtime::not_a_quill` /
|
|
486
|
+
`runtime::not_a_document`, hinting `npm ls @quillmark/wasm` for the second
|
|
487
|
+
case. Two copies are two WASM memories and two `Quill`/`Document` classes;
|
|
488
|
+
dedupe to one. Elsewhere a foreign handle meets wasm-bindgen's own
|
|
489
|
+
`expected instance of …`, which is not a `QuillmarkError`.
|
|
465
490
|
|
|
466
491
|
### Lifecycle
|
|
467
492
|
|