@quillmark/wasm 0.104.0 → 0.106.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 +244 -27
- package/README.md +72 -206
- package/backends/pdfform/wasm.d.ts +465 -713
- package/backends/pdfform/wasm.js +365 -491
- package/backends/pdfform/wasm_bg.wasm +0 -0
- package/backends/pdfform/wasm_bg.wasm.d.ts +1 -0
- package/backends/typst/wasm.d.ts +465 -713
- package/backends/typst/wasm.js +365 -491
- package/backends/typst/wasm_bg.wasm +0 -0
- package/backends/typst/wasm_bg.wasm.d.ts +1 -0
- package/core/wasm.d.ts +337 -492
- package/core/wasm.js +310 -411
- package/core/wasm_bg.wasm +0 -0
- package/core/wasm_bg.wasm.d.ts +1 -0
- package/package.json +1 -1
- package/runtime/runtime.d.ts +181 -255
- package/runtime/runtime.js +190 -353
package/README.md
CHANGED
|
@@ -6,12 +6,11 @@ Maintained by [TTQ](https://tonguetoquill.com).
|
|
|
6
6
|
|
|
7
7
|
## Overview
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Quillmark in browsers and Node, over explicit in-memory trees
|
|
10
|
+
(`Map<string, Uint8Array>` / `Record<string, Uint8Array>`).
|
|
10
11
|
|
|
11
|
-
The package
|
|
12
|
-
|
|
13
|
-
- `@quillmark/wasm` (the root), the **canonical API**: `init`, resolving to
|
|
14
|
-
`Quill` and `Document`, and an `Engine` that renders them.
|
|
12
|
+
The package has one import surface: `@quillmark/wasm`, whose `init` resolves to
|
|
13
|
+
`Quill` and `Document`, plus an `Engine` that renders them.
|
|
15
14
|
|
|
16
15
|
`Quill` and `Document` are the internal Typst-less core build's own classes,
|
|
17
16
|
handed out verbatim by `init`, so editor/validation code (`Quill.fromTree`,
|
|
@@ -101,11 +100,8 @@ the same value again is fine, so several entry points may each
|
|
|
101
100
|
|
|
102
101
|
**Both failures reject.** `runtime::init_conflict` and `runtime::init_failed`
|
|
103
102
|
alike ride the returned promise, so one `catch` around `await init(...)` covers
|
|
104
|
-
the gate.
|
|
105
|
-
|
|
106
|
-
**You cannot forget.** The core surface has no static export, so a call site
|
|
107
|
-
that skips the await has no name to call. The precondition is structural rather
|
|
108
|
-
than a convention: no load order can make one entry point pass and another fail.
|
|
103
|
+
the gate. The core surface has no static export, so a call site that skips the
|
|
104
|
+
await has no name to call.
|
|
109
105
|
|
|
110
106
|
**Vite's dev server** pre-bundles dependencies, which moves the package away
|
|
111
107
|
from its binary. Exclude it:
|
|
@@ -136,29 +132,14 @@ depend only on `quill.backendId`, and answer from the descriptor's required
|
|
|
136
132
|
`formats`/`canvas` manifest: never loading the multi-MB backend binary and
|
|
137
133
|
never cloning the quill. Use them as non-failing pre-render probes.
|
|
138
134
|
|
|
139
|
-
### `
|
|
140
|
-
|
|
141
|
-
is
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
did not set reaches the output. Build it up with `storeFields` / `insertCard`.
|
|
148
|
-
For an example-filled starter use `quill.seedDocument()`. Throws on an
|
|
149
|
-
invalid quill reference.
|
|
150
|
-
|
|
151
|
-
### `Document.fromMarkdown(markdown)`
|
|
152
|
-
Parse markdown to a parsed document, quill-free: the **transport door**
|
|
153
|
-
(migrations, `$ext` stamping, a quill that will not load, opening a document to
|
|
154
|
-
fix its `$quill`). Throws a JS `Error` (with `.diagnostics` attached, see
|
|
155
|
-
[Errors](#errors)) on any parse failure, including a missing root `$quill`
|
|
156
|
-
metadata line, malformed YAML, and inputs over the 10 MiB
|
|
157
|
-
`parse::input_too_large` limit. A content field rests as authored; `quill.parse`
|
|
158
|
-
below is the bound door that lands it at its canonical rest.
|
|
159
|
-
|
|
160
|
-
### `quill.parse(markdown)` / `quill.conform(doc)`
|
|
161
|
-
The **bound door**, and the primary ingestion path. `quill.parse` is
|
|
135
|
+
### The two doors: `Document.fromMarkdown` vs `quill.parse` / `quill.conform`
|
|
136
|
+
|
|
137
|
+
`Document.fromMarkdown` is the quill-free **transport door** (migrations, `$ext`
|
|
138
|
+
stamping, a quill that will not load, opening a document to fix its `$quill`).
|
|
139
|
+
It needs a root `~~~` block carrying a `$quill` line, and a content field rests
|
|
140
|
+
as authored.
|
|
141
|
+
|
|
142
|
+
`quill.parse` is the **bound door**, and the primary ingestion path. It is
|
|
162
143
|
`Document.fromMarkdown` followed by `conform`: the returned document's declared
|
|
163
144
|
content fields rest at one form per codec (a `richtext` field as the canonical
|
|
164
145
|
content object, a `plaintext` field as its literal string), so `getStored`
|
|
@@ -181,61 +162,17 @@ const stale = Document.fromJson(row);
|
|
|
181
162
|
const diags = quill.conform(stale); // converges in place
|
|
182
163
|
```
|
|
183
164
|
|
|
184
|
-
###
|
|
185
|
-
Emit canonical Quillmark Markdown. Type-fidelity round-trip safe:
|
|
186
|
-
`Document.fromMarkdown(doc.toMarkdown())` returns a document equal to `doc`
|
|
187
|
-
under [`doc.equals`](#docequalsother). The output is **not** guaranteed
|
|
188
|
-
byte-equal to the original source: YAML quoting, key ordering, and
|
|
189
|
-
whitespace are normalised. Use `equals` (not string comparison) to test
|
|
190
|
-
semantic equality.
|
|
191
|
-
|
|
192
|
-
### `doc.toJson()`
|
|
193
|
-
Serialize the document to a versioned storage DTO: a JSON **string**
|
|
194
|
-
carrying a `schema` version. Use this (not `toMarkdown`) to persist a
|
|
195
|
-
document across a process restart or crate upgrade: the wire format is
|
|
196
|
-
frozen per `schema` version, whereas Markdown syntax evolves. Parse-time
|
|
197
|
-
`warnings` are not part of the DTO.
|
|
198
|
-
|
|
199
|
-
The string is produced inside the module by `serde_json`; the JS `JSON`
|
|
200
|
-
global is not involved. It is standard JSON text, so callers may
|
|
201
|
-
`JSON.parse` it to inspect it, but it is intended as an opaque blob you
|
|
202
|
-
persist and hand back.
|
|
203
|
-
|
|
204
|
-
`toJson()` is **deterministic**: a `Document` that is `equals` to another
|
|
205
|
-
serializes to a byte-identical string: across repeated calls, and across
|
|
206
|
-
any crate upgrade that keeps the same `schema` version (every release does until
|
|
207
|
-
the `Document` model changes; see [Storage compatibility](#storage-compatibility-across-versions)).
|
|
208
|
-
Field order is fixed and object key order is preserved, so content hashes
|
|
209
|
-
and string-equality dirty-checks over the output are stable.
|
|
210
|
-
|
|
211
|
-
### `Document.fromJson(json)`
|
|
212
|
-
Reconstruct a `Document` from a storage DTO string produced by `toJson`.
|
|
213
|
-
Round-trips losslessly:
|
|
214
|
-
|
|
215
|
-
```ts
|
|
216
|
-
const stored = doc.toJson(); // persist this string
|
|
217
|
-
const restored = Document.fromJson(stored);
|
|
218
|
-
restored.equals(doc); // true
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
Throws a JS `Error` on malformed JSON, an unknown `schema` version, or a
|
|
222
|
-
malformed payload. The restored document has no parse-time `warnings`.
|
|
165
|
+
### Storage compatibility across versions
|
|
223
166
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
167
|
+
Persist `doc.toJson()`, not `doc.toMarkdown()`: the DTO wire format is frozen
|
|
168
|
+
per `schema` version, whereas Markdown syntax evolves, and `toMarkdown` output
|
|
169
|
+
is normalised rather than byte-equal to the source. `Document.tryFromJson`
|
|
170
|
+
discriminates the two formats without exceptions as control flow:
|
|
228
171
|
|
|
229
172
|
```ts
|
|
230
|
-
// "JSON canonical, Markdown fallback": no exceptions, no string sniffing
|
|
231
173
|
const doc = Document.tryFromJson(content) ?? Document.fromMarkdown(content);
|
|
232
174
|
```
|
|
233
175
|
|
|
234
|
-
`undefined` means only "not a storage DTO"; `fromMarkdown` still throws on
|
|
235
|
-
genuinely malformed Markdown.
|
|
236
|
-
|
|
237
|
-
### Storage compatibility across versions
|
|
238
|
-
|
|
239
176
|
The `schema` value (`quillmark/document@0.93.0`) is the **model version**,
|
|
240
177
|
not the running crate version. It is a hand-set constant, bumped only when
|
|
241
178
|
the `Document` model itself changes, so every `0.93.x` patch release reads
|
|
@@ -269,53 +206,17 @@ In short: persist the `toJson` string, upgrade freely, never downgrade. The
|
|
|
269
206
|
full design (including how migrations are added) is in
|
|
270
207
|
`prose/canon/DOCUMENT_STORAGE.md`.
|
|
271
208
|
|
|
272
|
-
###
|
|
273
|
-
Structural equality between two `Document` handles. Compares `main` and
|
|
274
|
-
`cards` by value; parse-time `warnings` are intentionally excluded.
|
|
275
|
-
|
|
276
|
-
Use this to debounce upstream prop updates: keep the last parsed `Document`
|
|
277
|
-
and compare instead of re-parsing on every keystroke.
|
|
278
|
-
|
|
279
|
-
### `doc.cardCount`
|
|
280
|
-
O(1) getter for the number of composable cards (excluding the main card).
|
|
281
|
-
Use this to validate indices before calling card mutators (`removeCard`,
|
|
282
|
-
`storeField({ card, field }, …)`, etc.) without allocating the full `cards` array.
|
|
283
|
-
|
|
284
|
-
### `quill.validate(doc)`
|
|
285
|
-
|
|
286
|
-
Returns `Diagnostic[]`: the document validated against the quill schema,
|
|
287
|
-
without invoking the backend. An empty array means the document is valid.
|
|
288
|
-
Each diagnostic carries the canonical `validation::*` `code`, `path`, and
|
|
289
|
-
`hint`. Includes the non-fatal `validation::must_fill` warning for each
|
|
290
|
-
`!must_fill` marker left in the document (render zero-fills these rather
|
|
291
|
-
than failing), so filter by `severity`/`code` for blockers vs. hints:
|
|
292
|
-
|
|
293
|
-
```ts
|
|
294
|
-
const diagnostics = quill.validate(Document.fromMarkdown(markdown));
|
|
295
|
-
const errors = diagnostics.filter(d => d.severity === "error");
|
|
296
|
-
```
|
|
209
|
+
### Cards, seeds, and addresses
|
|
297
210
|
|
|
298
211
|
To render a form editor, read field definitions from `quill.schema` (walk
|
|
299
212
|
`fields` in key order: declaration order is display order) and the authored
|
|
300
213
|
values from the `Document` payload: there is no separate form-view projection.
|
|
214
|
+
`quill.validate(doc)` scores it without invoking the backend.
|
|
301
215
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
`default:` → type-empty zero). Illustration-first: a field with both an
|
|
307
|
-
`example` and a `default` renders its example. Use as the initial state for a
|
|
308
|
-
"new document" editor.
|
|
309
|
-
|
|
310
|
-
```ts
|
|
311
|
-
const doc = quill.seedDocument();
|
|
312
|
-
const markdown = doc.toMarkdown();
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
For per-card seeding, `quill.seedMain()` returns just the `$kind: main` card
|
|
316
|
-
and `quill.seedCard(kind)` returns a starter composable card (or `undefined`
|
|
317
|
-
if the kind is not declared). Both return the read `Card` shape of
|
|
318
|
-
`doc.main` / `doc.cards`, which `doc.insertCard` accepts directly:
|
|
216
|
+
`quill.seedDocument()` returns a starter document with each field's `example:`
|
|
217
|
+
committed; `quill.seedMain()` and `quill.seedCard(kind)` seed one card. All
|
|
218
|
+
return the read `Card` shape of `doc.main` / `doc.cards`, which `doc.insertCard`
|
|
219
|
+
accepts directly:
|
|
319
220
|
|
|
320
221
|
```ts
|
|
321
222
|
doc.insertCard(quill.seedCard("note")); // seed → append
|
|
@@ -412,7 +313,7 @@ write.
|
|
|
412
313
|
|
|
413
314
|
#### `DocumentReader` / `CardReader`: the read twin
|
|
414
315
|
|
|
415
|
-
`quill.reader(doc)` carries the writer's ephemerality and
|
|
316
|
+
`quill.reader(doc)` carries the writer's ephemerality and schema authority:
|
|
416
317
|
|
|
417
318
|
```ts
|
|
418
319
|
const v = quill.reader(doc);
|
|
@@ -439,24 +340,10 @@ tells you which pages to repaint (`dirty ∩ visible`). Apply is transactional:
|
|
|
439
340
|
on throw, every read keeps serving the last-good compile. Don't open a session
|
|
440
341
|
per export, and don't re-open per edit: `apply` instead.
|
|
441
342
|
|
|
442
|
-
### `engine.render(quill, parsed, opts?)`
|
|
443
|
-
Render a pre-parsed `Document` against `quill`. Throws an
|
|
444
|
-
`engine::backend_not_found` error if no registered backend matches the quill's
|
|
445
|
-
declared backend.
|
|
446
|
-
|
|
447
|
-
### `engine.open(quill, parsed)` + `session.render(opts?)`
|
|
448
|
-
Open once, render all or selected pages (`opts.pages`).
|
|
449
|
-
|
|
450
|
-
The session also exposes `pageCount`, `backendId`, `supportsCanvas`,
|
|
451
|
-
`warnings` (non-fatal diagnostics of the current compile: set at `open`,
|
|
452
|
-
refreshed by each committed `apply`),
|
|
453
|
-
`apply(doc)` for in-place recompiles, `pageSize(page)`, and
|
|
454
|
-
`paint(ctx, page, opts?)` for canvas previews. See below.
|
|
455
|
-
|
|
456
343
|
A document that compiles to zero pages still produces a valid session
|
|
457
|
-
(`pageCount === 0`); `paint(ctx, 0)` and `pageSize(0)` then throw
|
|
458
|
-
`
|
|
459
|
-
|
|
344
|
+
(`pageCount === 0`); `paint(ctx, 0)` and `pageSize(0)` then throw. Branch on
|
|
345
|
+
`pageCount === 0` to render a "no pages to preview" UI rather than relying on
|
|
346
|
+
the throw.
|
|
460
347
|
|
|
461
348
|
### Canvas Preview
|
|
462
349
|
|
|
@@ -480,58 +367,47 @@ canvas.style.width = `${result.layoutWidth}px`;
|
|
|
480
367
|
canvas.style.height = `${result.layoutHeight}px`;
|
|
481
368
|
```
|
|
482
369
|
|
|
483
|
-
- `layoutScale`
|
|
484
|
-
`
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
it stays near the viewport rather than pooling one canvas across pages:
|
|
503
|
-
an idle canvas retains its pixels for free, whereas reusing a canvas on
|
|
504
|
-
scroll re-runs a full render.
|
|
505
|
-
- `pageCount` and `pageSize(page)` are stable for the session's
|
|
506
|
-
lifetime (immutable snapshot): cache them.
|
|
507
|
-
- Worker support: pass an `OffscreenCanvasRenderingContext2D` and the
|
|
508
|
-
same call signature works. `layoutWidth` / `layoutHeight` are
|
|
509
|
-
informational in that mode (no CSS layout box); fold everything into
|
|
510
|
-
`densityScale`. Loading the WASM module inside a Worker is the host's
|
|
511
|
-
responsibility.
|
|
512
|
-
- Backend support: gated by `supportsCanvas`. Probe upfront with
|
|
513
|
-
`engine.supportsCanvas(quill)` (or `session.supportsCanvas`) before mounting
|
|
514
|
-
a canvas-based UI; the throw on `paint` / `pageSize` remains the
|
|
515
|
-
enforcement contract and includes the resolved `backendId` for
|
|
516
|
-
debugging.
|
|
370
|
+
- `layoutScale` sets the display-box size (`layoutWidth = widthPt * layoutScale`);
|
|
371
|
+
fold `devicePixelRatio`, in-app zoom, and `visualViewport.scale` into
|
|
372
|
+
`densityScale`. Their product is the rasterization scale, clamped at 16384 px
|
|
373
|
+
per side (`result.clamped`, `result.effectiveDensityScale`).
|
|
374
|
+
- `paint` writes the whole backing store with `putImageData`, which ignores the
|
|
375
|
+
2D context transform, `globalAlpha`, and clip. Give each visible page its own
|
|
376
|
+
`<canvas>`: no compositing, sub-rect, or transform reaches through `paint`.
|
|
377
|
+
- `paint` is always a full repaint, and there is no per-page raster cache. Keep
|
|
378
|
+
a page's canvas alive while it stays near the viewport: an idle canvas retains
|
|
379
|
+
its pixels for free, whereas pooling one canvas across pages re-renders on
|
|
380
|
+
every scroll.
|
|
381
|
+
- `pageCount` and `pageSize(page)` are stable for the session's lifetime: cache
|
|
382
|
+
them.
|
|
383
|
+
- In a Worker, pass an `OffscreenCanvasRenderingContext2D`; the layout
|
|
384
|
+
dimensions are informational there. Loading the WASM module inside the Worker
|
|
385
|
+
is the host's responsibility.
|
|
386
|
+
- Backend support is gated by `supportsCanvas`. Probe upfront with
|
|
387
|
+
`engine.supportsCanvas(quill)`; the throw on `paint` / `pageSize` remains the
|
|
388
|
+
enforcement contract and names the resolved `backendId`.
|
|
517
389
|
|
|
518
390
|
### Schema model
|
|
519
391
|
|
|
520
|
-
A field
|
|
392
|
+
A field carries two independent axes, and no `required` one.
|
|
393
|
+
|
|
394
|
+
**Value** — what the cell holds. With a `default:`, `quill.blueprint` renders
|
|
395
|
+
that value under a type-only `# <type>` annotation and the render path uses it
|
|
396
|
+
when the document omits the field. Without one, an `example` takes the cell as
|
|
397
|
+
a suggested value, and an absent field blank-fills.
|
|
521
398
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
and the default is used when the document omits the field.
|
|
399
|
+
**Obligation** — whether a human must author the field, declared by
|
|
400
|
+
`must_fill:` and deriving from `default:`'s absence when left unset. An obliged
|
|
401
|
+
field carries the `!must_fill` marker in `quill.blueprint`, and
|
|
402
|
+
`quill.validate(doc)` emits the non-fatal `validation::must_fill` warning while
|
|
403
|
+
the document leaves it unauthored — from either of two triggers, named by the
|
|
404
|
+
diagnostic's `trigger` arg: `marker` for a marker the document still carries,
|
|
405
|
+
`unauthored` for a cell the schema obliges and the document never filled.
|
|
406
|
+
Authoring the field's blank discharges the obligation; clearing the key does
|
|
407
|
+
not.
|
|
532
408
|
|
|
533
|
-
|
|
534
|
-
|
|
409
|
+
Neither axis gates render. Partial documents are accepted, and
|
|
410
|
+
`engine.render(quill, doc)` throws only for malformed input.
|
|
535
411
|
|
|
536
412
|
### Errors
|
|
537
413
|
|
|
@@ -562,16 +438,12 @@ guard.
|
|
|
562
438
|
|
|
563
439
|
`QuillmarkError` is a **structural interface, not a class**: the WASM layer
|
|
564
440
|
throws a real `Error` and attaches the property, so there is no constructor to
|
|
565
|
-
`instanceof` against
|
|
566
|
-
|
|
441
|
+
`instanceof` against. Narrow with `isQuillmarkError`, which also works on errors
|
|
442
|
+
from any build or WASM instance in the page.
|
|
567
443
|
|
|
568
444
|
`diagnostics` is always non-empty: length 1 for most failures, length N for
|
|
569
|
-
backend compilation errors
|
|
570
|
-
|
|
571
|
-
`"<N> error(s): <first.message>"` summary for compilation failures).
|
|
572
|
-
|
|
573
|
-
Read `err.diagnostics[0]` for the primary diagnostic; iterate the array for
|
|
574
|
-
compilation failures. The same shape applies to every throw site:
|
|
445
|
+
backend compilation errors, and `message` is derived from it. The same shape
|
|
446
|
+
applies to every throw site:
|
|
575
447
|
|
|
576
448
|
- `Document.fromMarkdown`: parse errors (missing root `$quill` metadata, YAML
|
|
577
449
|
errors, `parse::input_too_large` for inputs > 10 MiB).
|
|
@@ -582,6 +454,9 @@ compilation failures. The same shape applies to every throw site:
|
|
|
582
454
|
text.
|
|
583
455
|
- `engine.render` / `session.render`: backend compilation failures and
|
|
584
456
|
validation errors.
|
|
457
|
+
- `engine.render(quill, parsed)` against a quill whose *name* differs
|
|
458
|
+
(`quill::name_mismatch`) or whose *version* falls outside the document's
|
|
459
|
+
selector (`quill::version_mismatch`): a throw, never a warning.
|
|
585
460
|
- Any method taking a `Quill` or `Document`: a handle from a *second* copy of
|
|
586
461
|
`@quillmark/wasm` is refused with `runtime::foreign_handle`, hinting `npm ls
|
|
587
462
|
@quillmark/wasm`. Two copies are two WASM memories and two `Quill`/`Document`
|
|
@@ -623,15 +498,6 @@ try {
|
|
|
623
498
|
|
|
624
499
|
[erm]: https://github.com/tc39/proposal-explicit-resource-management
|
|
625
500
|
|
|
626
|
-
## Notes
|
|
627
|
-
|
|
628
|
-
- Parsed markdown requires a root `~~~` block (a bare three-tilde fence;
|
|
629
|
-
`~~~card-yaml` is also accepted as a non-canonical alias)
|
|
630
|
-
with a `$quill` system-metadata line. Empty input surfaces a dedicated
|
|
631
|
-
"Empty markdown input cannot be parsed" message.
|
|
632
|
-
- 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.
|
|
633
|
-
- Output schema APIs live on `Quill`, not the engine.
|
|
634
|
-
|
|
635
501
|
## Changelog
|
|
636
502
|
|
|
637
503
|
See the [changelog](https://github.com/borb-sh/quillmark/blob/main/CHANGELOG.md)
|