@quillmark/wasm 0.100.0 → 0.102.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 +124 -0
- package/README.md +68 -15
- package/backends/pdfform/wasm.d.ts +265 -67
- package/backends/pdfform/wasm.js +2980 -8
- package/backends/pdfform/wasm_bg.wasm +0 -0
- package/backends/pdfform/wasm_bg.wasm.d.ts +8 -8
- package/backends/typst/wasm.d.ts +265 -67
- package/backends/typst/wasm.js +3004 -8
- package/backends/typst/wasm_bg.wasm +0 -0
- package/backends/typst/wasm_bg.wasm.d.ts +8 -8
- package/core/wasm.d.ts +232 -58
- package/core/wasm.js +2471 -8
- package/core/wasm_bg.wasm +0 -0
- package/core/wasm_bg.wasm.d.ts +7 -7
- package/package.json +7 -4
- package/runtime/env-node.js +20 -0
- package/runtime/env-web.js +19 -0
- package/runtime/runtime.d.ts +61 -26
- package/runtime/runtime.js +200 -41
- package/runtime/uninit.js +57 -0
- package/backends/pdfform/wasm_bg.js +0 -2858
- package/backends/typst/wasm_bg.js +0 -2882
- package/core/wasm_bg.js +0 -2354
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,129 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.102.0 - 2026-08-04
|
|
4
|
+
|
|
5
|
+
The pre-1.0 vocabulary reset. Verbs, diagnostic codes, and two words that meant
|
|
6
|
+
different things at different altitudes. Documents and stored blobs are
|
|
7
|
+
untouched: no document reparses, no blob remigrates, and the storage wire format
|
|
8
|
+
is byte-identical, but nearly every consumer touches at least one renamed verb.
|
|
9
|
+
The diagnostic codes are the part that could not wait, since consumers route on
|
|
10
|
+
them and 1.0 freezes them. All breaking changes are covered by
|
|
11
|
+
`docs/migrations/0.101-to-0.102.md`. Only two behaviors change, both called out
|
|
12
|
+
below. Separately, `@quillmark/wasm` ships `--target web`, which makes
|
|
13
|
+
`await init()` mandatory and the bundler plugins the package used to demand
|
|
14
|
+
unnecessary.
|
|
15
|
+
|
|
16
|
+
- refactor(core,wasm,python)!: the verb vocabulary is reset to say what the code
|
|
17
|
+
does. `install_body` / `install_field` / `doc.install` become `overwrite_*` /
|
|
18
|
+
`doc.overwrite`: the content lane is a ladder sorted by the fate of the
|
|
19
|
+
identity anchors already on the value (overwrite destroys, revise rebases,
|
|
20
|
+
apply preserves), and only the first keeps nothing, which grouping the three as
|
|
21
|
+
"identity-aware" hid. `LiveSession::apply` becomes `update`, taking the trait
|
|
22
|
+
seam, the feature-gated raw-plate seam, and `backend::apply_unsupported` →
|
|
23
|
+
`backend::update_unsupported` with it, so the content lane's `applyChange`
|
|
24
|
+
splice is no longer a homograph of a whole-document recompile.
|
|
25
|
+
`apply_field_richtext_change` becomes `apply_field_change`, matching its
|
|
26
|
+
schema-blind neighbour `revise_field`, which never carried the codec in its
|
|
27
|
+
name. `store_seed_namespace` / `remove_seed_namespace` become
|
|
28
|
+
`store_seed_overlay` / `remove_seed_overlay`, closing the asymmetry with the
|
|
29
|
+
`seedOverlay` read and the `SeedOverlay` type that already shipped: `$seed` is
|
|
30
|
+
keyed by a validated card-kind, `$ext` by a free-form consumer namespace.
|
|
31
|
+
`getMarkdown` and reader `get_body` become `bodyMarkdown` / `body_markdown`,
|
|
32
|
+
the name core already used, so one projection has one name on every surface
|
|
33
|
+
(#1186). See `docs/migrations/0.101-to-0.102.md`
|
|
34
|
+
- refactor(core,wasm,python)!: the mutator and validation diagnostic codes are
|
|
35
|
+
corrected, and the content-field failures carry the codec that ran.
|
|
36
|
+
`edit::field_richtext_decode` becomes `edit::field_decode` and
|
|
37
|
+
`edit::field_richtext_not_inline` becomes `edit::field_not_inline`, each
|
|
38
|
+
gaining a `codec` arg (`"richtext"` or `"plaintext"`): both were raised for
|
|
39
|
+
plaintext failures under a richtext name, and `plaintext(inline)` fell through
|
|
40
|
+
to the generic code. `edit::field_conform` becomes
|
|
41
|
+
`edit::field_coercion_failed`, killing the `conform::field_conform` stutter and
|
|
42
|
+
landing the variant beside its real twin `validation::coercion_failed`, minted
|
|
43
|
+
from the same `CoercionError`. `richtext::not_inline` and
|
|
44
|
+
`plaintext::not_plain` become `validation::not_inline` / `validation::not_plain`,
|
|
45
|
+
stage-namespaced like every other code. The `conform::*` twins rename in
|
|
46
|
+
lockstep. Route on the code and read `codec` for the lane (#1186). See
|
|
47
|
+
`docs/migrations/0.101-to-0.102.md`
|
|
48
|
+
- fix(core)!: a splice against an absent field is no longer a decode error. The
|
|
49
|
+
writer `set_body` / `setBody` becomes `revise_body` / `reviseBody` and returns
|
|
50
|
+
the text `Delta` it was already computing (a body carries no field schema, so a
|
|
51
|
+
typed-lane verb had nothing to type); Python discards the receipt, as on
|
|
52
|
+
`revise_field`. `apply_field_change` on an absent field splices against the
|
|
53
|
+
empty content, as `revise_field` diffs against it, instead of reporting
|
|
54
|
+
`FieldRichtextDecode { message: "field is absent" }`: the one place in the API
|
|
55
|
+
where a missing field was not simply `None`. No error is lost. A bundle that
|
|
56
|
+
expected content still fails, and now reports the condition it hit, since the
|
|
57
|
+
text delta declares the base length it was computed against, so a stale splice
|
|
58
|
+
lands as `edit::content_apply`. Route a vanished-field check on that code, or
|
|
59
|
+
check presence before splicing. These are the release's only behavior changes
|
|
60
|
+
(#1186). See `docs/migrations/0.101-to-0.102.md`
|
|
61
|
+
- refactor(core)!: `EditError::variant_name` is removed and the typed primitives
|
|
62
|
+
leave the documented surface. The bare variant name was a second discriminator
|
|
63
|
+
kept in lockstep with `code()` that nothing routed on: both binding error
|
|
64
|
+
mappers stamp `code()` onto the `Diagnostic` they raise, and `ERROR.md` has
|
|
65
|
+
said identity is the code since the `edit::*` family landed. Assert on `code()`
|
|
66
|
+
or match the enum, which is `#[non_exhaustive]` either way. `Card::commit_field`
|
|
67
|
+
and `Card::revise_field_checked` become `#[doc(hidden)]`: a resolved
|
|
68
|
+
`FieldSchema` argument was the only thing telling them from their opaque and
|
|
69
|
+
schema-blind neighbours, and disambiguating by argument is a third mechanism
|
|
70
|
+
beside the receiver and the verb. `quill.writer(&mut doc)` is the typed door on
|
|
71
|
+
every surface now, core included; the primitives stay callable on the same
|
|
72
|
+
terms as the other hidden items (#1186). See `docs/migrations/0.101-to-0.102.md`
|
|
73
|
+
- docs(content,core)!: restoring a deleted island re-lands its original id, and
|
|
74
|
+
the island channel states two contracts it had left to the implementation. The
|
|
75
|
+
never-ambient minting rule (continue the field's positional `isl-{n}` sequence,
|
|
76
|
+
never a UUID or a clock reading) covers a *new* island; a delete frees its id
|
|
77
|
+
and the id travels back with the island, so an editor that mints fresh on undo
|
|
78
|
+
renames it, moving the content hash of a document the user believes they
|
|
79
|
+
restored. A pasted copy of a live island is new and mints fresh. Alongside it:
|
|
80
|
+
an island op's `at` is *sequenced*, counting the text the delta and this
|
|
81
|
+
bundle's earlier island ops left rather than the shared post-delta frame; and a
|
|
82
|
+
producer's whole-field diff that carries an island slot must split into the
|
|
83
|
+
slot-free `delta` plus one `Insert` per slot, since a slot in an insert string
|
|
84
|
+
orphans. Neither is a behavior change, and a producer that guessed wrong got a
|
|
85
|
+
wrong document rather than an error (#1185)
|
|
86
|
+
- feat(wasm)!: the package ships `--target web` and the runtime owns
|
|
87
|
+
instantiation, so `await init()` once at startup is required before any export
|
|
88
|
+
is used. `--target bundler` emitted `import * as wasm from "./wasm_bg.wasm"`,
|
|
89
|
+
which no browser and no bundler resolves natively; the plugin that fixed it
|
|
90
|
+
rewrote the import into a top-level await, and because the runtime statically
|
|
91
|
+
re-exports core, that await landed on the static module graph of everything
|
|
92
|
+
importing `@quillmark/wasm`: a permanent constraint on consumer architecture
|
|
93
|
+
and a blank SvelteKit route in Safari's dev server that neither Chrome nor
|
|
94
|
+
`vite build` showed. In exchange, `vite-plugin-wasm` and
|
|
95
|
+
`vite-plugin-top-level-await` are no longer needed, a static import is safe
|
|
96
|
+
anywhere including SSR, and plain Node can import the package at all (the ESM
|
|
97
|
+
`.wasm` import needed `--experimental-wasm-modules`). Only
|
|
98
|
+
`optimizeDeps: { exclude: ['@quillmark/wasm'] }` stays, for Vite's dev server.
|
|
99
|
+
`init` memoizes its promise, so several entry points share one instantiation
|
|
100
|
+
and a failed attempt clears the memo for a retry; backends are not the
|
|
101
|
+
consumer's to initialize, since `Engine` instantiates one inside its lazy load.
|
|
102
|
+
Reaching the surface early throws `runtime::not_initialized` naming the fix,
|
|
103
|
+
and `build-wasm.sh` asserts both the guard's anchors and that no artifact
|
|
104
|
+
carries a `.wasm` import or a top-level await. `initSync` is not exported: the
|
|
105
|
+
capability remains through `init(source)`, which takes bytes, a `Response`, a
|
|
106
|
+
`WebAssembly.Module`, or a URL (#1189). See `docs/migrations/0.101-to-0.102.md`
|
|
107
|
+
|
|
108
|
+
## v0.101.0 - 2026-08-03
|
|
109
|
+
|
|
110
|
+
- refactor(core): gate the raw-plate seam behind a feature, and fixes from review
|
|
111
|
+
- docs(core): state the conform gate's codes instead of linking a private item
|
|
112
|
+
- refactor(core): bind a live session to its quill so apply takes a Document
|
|
113
|
+
- feat(quillmark): the facade names what the read and preview flows return
|
|
114
|
+
- docs(content,wasm): density pass over the island channel's prose
|
|
115
|
+
- feat(content,core,wasm)!: reach islands through the op vocabulary
|
|
116
|
+
- refactor(core)!: `Payload` becomes a read view
|
|
117
|
+
- refactor(core)!: collapse the schema-free field projection
|
|
118
|
+
- refactor(core)!: fold `Quill`'s file queries into `FileTreeNode`
|
|
119
|
+
- fix(core): repoint the doc references the `from_yaml` removal orphaned
|
|
120
|
+
- refactor(quillmark): move the facade gate off the front page, and stop tests reaching past it
|
|
121
|
+
- docs: migration guide for the 0.101 surface removals
|
|
122
|
+
- refactor(core)!: drop the lossy `QuillConfig::from_yaml`
|
|
123
|
+
- refactor(core)!: `Document::from_main_and_cards` becomes crate-internal
|
|
124
|
+
- feat(quillmark): the facade covers authoring, and the examples enter through the bound door
|
|
125
|
+
|
|
126
|
+
|
|
3
127
|
## v0.100.0 - 2026-08-03
|
|
4
128
|
|
|
5
129
|
A content field gets one resting form, and the last reserved `$` key with no
|
package/README.md
CHANGED
|
@@ -30,8 +30,10 @@ bash scripts/build-wasm.sh
|
|
|
30
30
|
|
|
31
31
|
The script builds three variants: the core (no backend), the Typst backend
|
|
32
32
|
(default features), and the Typst-free pdfform backend (`pdfform` feature):
|
|
33
|
-
each with `--target
|
|
34
|
-
[Lifecycle](#lifecycle)).
|
|
33
|
+
each with `--target web` and `--weak-refs` enabled (see
|
|
34
|
+
[Initialization](#initialization) and [Lifecycle](#lifecycle)). It then patches
|
|
35
|
+
each generated build with the pre-init sentinel (`runtime/uninit.js`) and
|
|
36
|
+
asserts none of them carries a `.wasm` ESM import or a top-level await.
|
|
35
37
|
|
|
36
38
|
## Test
|
|
37
39
|
|
|
@@ -45,7 +47,9 @@ npm test
|
|
|
45
47
|
## Usage
|
|
46
48
|
|
|
47
49
|
```ts
|
|
48
|
-
import { Document, Quill, Engine } from "@quillmark/wasm";
|
|
50
|
+
import { init, Document, Quill, Engine } from "@quillmark/wasm";
|
|
51
|
+
|
|
52
|
+
await init(); // once at startup; see Initialization
|
|
49
53
|
|
|
50
54
|
const quill = Quill.fromTree(tree); // no engine needed: build + validate
|
|
51
55
|
const engine = new Engine(); // loads a backend lazily on first render
|
|
@@ -62,6 +66,52 @@ const parsed = Document.fromMarkdown(markdown);
|
|
|
62
66
|
const result = await engine.render(quill, parsed, { format: "pdf" });
|
|
63
67
|
```
|
|
64
68
|
|
|
69
|
+
## Initialization
|
|
70
|
+
|
|
71
|
+
`await init()` once, at startup, before any other export is used. Everything
|
|
72
|
+
after it is the synchronous surface the rest of this README describes.
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
import { init, Quill, Engine } from "@quillmark/wasm";
|
|
76
|
+
await init();
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The same line works everywhere: the binary streams from a URL in a browser and
|
|
80
|
+
is read off disk under Node, chosen by the package's `#quillmark-env` subpath
|
|
81
|
+
import rather than a runtime environment check. No bundler plugin is required:
|
|
82
|
+
the builds are `--target web`, so nothing in the package graph imports a `.wasm`
|
|
83
|
+
module or carries a top-level await, and a static `import` of this package is
|
|
84
|
+
safe anywhere, SSR included.
|
|
85
|
+
|
|
86
|
+
`init` is idempotent and concurrency-safe: every call returns the same promise,
|
|
87
|
+
so several entry points may each `await init()` for one instantiation. A failed
|
|
88
|
+
init clears the memo, so a retry works. Each realm initializes its own copy; a
|
|
89
|
+
Worker calls `init()` too.
|
|
90
|
+
|
|
91
|
+
**Backends need nothing.** `Engine` instantiates a backend inside its lazy load,
|
|
92
|
+
on the first render against it.
|
|
93
|
+
|
|
94
|
+
**Overriding the source.** `init(source)` accepts bytes, a `Response`, a
|
|
95
|
+
`WebAssembly.Module`, or a URL, for hosts that route assets themselves or embed
|
|
96
|
+
the binary. Pass it on the first call; a later call passing a *different* source
|
|
97
|
+
throws `runtime::init_conflict` rather than silently ignoring it. Passing the
|
|
98
|
+
same value again is fine, so several entry points may each `await init(BYTES)`
|
|
99
|
+
against one constant.
|
|
100
|
+
|
|
101
|
+
**If you forget.** Reaching the surface early throws a `QuillmarkError` coded
|
|
102
|
+
`runtime::not_initialized` that names the fix, rather than a `TypeError` from
|
|
103
|
+
inside generated code.
|
|
104
|
+
|
|
105
|
+
**Vite's dev server** pre-bundles dependencies, which moves the package away
|
|
106
|
+
from its binary. Exclude it:
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
// vite.config.js
|
|
110
|
+
export default { optimizeDeps: { exclude: ["@quillmark/wasm"] } };
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
A load failure surfaces as `runtime::init_failed`, whose hint names that line.
|
|
114
|
+
|
|
65
115
|
## API
|
|
66
116
|
|
|
67
117
|
### `new Engine(options?)`
|
|
@@ -107,7 +157,7 @@ The **bound door**, and the primary ingestion path. `quill.parse` is
|
|
|
107
157
|
`Document.fromMarkdown` followed by `conform`: the returned document's declared
|
|
108
158
|
content fields rest at one form per codec (a `richtext` field as the canonical
|
|
109
159
|
content object, a `plaintext` field as its literal string), so `getStored`
|
|
110
|
-
answers "
|
|
160
|
+
answers "content object or string?" by the field's declared codec rather than by how the
|
|
111
161
|
document was built. Parse warnings and the `conform::*` warnings both ride
|
|
112
162
|
`doc.warnings`.
|
|
113
163
|
|
|
@@ -199,13 +249,13 @@ and writes that same value.
|
|
|
199
249
|
To detect a version mismatch before parsing, use the static accessors:
|
|
200
250
|
|
|
201
251
|
```ts
|
|
202
|
-
const v = Document.
|
|
203
|
-
if (v && v !== Document.
|
|
252
|
+
const v = Document.storageVersionOf(blob); // undefined | string
|
|
253
|
+
if (v && v !== Document.currentStorageVersion()) {
|
|
204
254
|
// payload is from a build with a different model version
|
|
205
255
|
}
|
|
206
256
|
```
|
|
207
257
|
|
|
208
|
-
`
|
|
258
|
+
`storageVersionOf` does not validate the payload: it only reads the
|
|
209
259
|
`schema` field, returning `undefined` for non-JSON, non-objects, or
|
|
210
260
|
payloads that don't carry one. Use it to distinguish "wrong version" from
|
|
211
261
|
"corrupt" when `fromJson` throws.
|
|
@@ -285,11 +335,11 @@ main card's `qty`, `doc.storeField({ card: 2, field: "qty" }, 3)` a composable
|
|
|
285
335
|
card's. Reads are total over the field axis (`getStored` → `undefined`, `isFill` → `false` for
|
|
286
336
|
an absent field; only an out-of-range card throws); field writes throw on a body
|
|
287
337
|
address. `getStored` is the verbatim transport read, distinct from the interpreted
|
|
288
|
-
`quill.reader(doc).get`; `
|
|
338
|
+
`quill.reader(doc).get`; `bodyMarkdown` is the body markdown read (a `CardAddr`; a field's
|
|
289
339
|
markdown is read through `quill.reader(doc).get(field)`). A content field's stored
|
|
290
340
|
form follows how the document was built (a canonical content object when the
|
|
291
341
|
typed writer committed it, the authored string when a markdown parse produced
|
|
292
|
-
it), so for the
|
|
342
|
+
it), so for the `Content` either way read `quill.reader(doc).getContent(addr)`, which
|
|
293
343
|
decodes through the codec the field's declared type names. Card-scoped verbs take a
|
|
294
344
|
`CardAddr` (`{ card? }`) first: `doc.getExt({ card: 2 })`, and the batch below.
|
|
295
345
|
|
|
@@ -309,7 +359,7 @@ writes go through the schema-bound writer while the quill-free opaque store sits
|
|
|
309
359
|
on `Document` itself (**store** = verbatim, **set** = typed):
|
|
310
360
|
|
|
311
361
|
- **`quill.writer(doc)`: the typed door whenever a quill is in hand.** Bind the
|
|
312
|
-
schema once and issue bare `set` / `setAll` / `
|
|
362
|
+
schema once and issue bare `set` / `setAll` / `reviseBody` / `reviseField` /
|
|
313
363
|
`addCard` / `card(i)`. Each resolves the field's schema `type`, coerces the
|
|
314
364
|
value to its canonical form (`"3"` → `3`, a markdown string → a richtext
|
|
315
365
|
content), and **fails now** on a mismatch instead of at render. A name the schema
|
|
@@ -362,16 +412,16 @@ write.
|
|
|
362
412
|
```ts
|
|
363
413
|
const v = quill.reader(doc);
|
|
364
414
|
v.get("subject"); // by declared type: richtext → markdown, plaintext → literal text
|
|
365
|
-
v.getContent("subject"); // the same read as a Content
|
|
366
|
-
v.
|
|
415
|
+
v.getContent("subject"); // the same read as a `Content`, whichever lane stored it
|
|
416
|
+
v.bodyMarkdown(); // the main body markdown (quill-free)
|
|
367
417
|
v.card(0).get("body"); // a card field, resolved by its $kind
|
|
368
418
|
```
|
|
369
419
|
|
|
370
|
-
`get` projects and `getContent` returns the
|
|
420
|
+
`get` projects and `getContent` returns the `Content`; both decode through the codec
|
|
371
421
|
the field's **declared type** names, which is why they bind the quill and the
|
|
372
422
|
verbatim `doc.getStored` does not. An undeclared name throws `UnknownField`, a
|
|
373
423
|
type that is not a content leaf throws `FieldNotContent`, and an undecodable
|
|
374
|
-
value throws `
|
|
424
|
+
value throws `FieldDecode`; an absent field reads back `undefined`.
|
|
375
425
|
|
|
376
426
|
### `engine.render(quill, parsed, opts?)` vs. `engine.open(quill, parsed)`
|
|
377
427
|
|
|
@@ -515,7 +565,7 @@ compilation failures. The same shape applies to every throw site:
|
|
|
515
565
|
- `Document` mutators (`storeField`, the writer's `set`, etc.): mutator
|
|
516
566
|
failures carry a namespaced `edit::*` `code` on `diagnostics[0]`
|
|
517
567
|
(`edit::invalid_field_name`, `edit::unknown_field`, `edit::index_out_of_range`,
|
|
518
|
-
`edit::
|
|
568
|
+
`edit::field_coercion_failed`, …). Route on `diagnostics[0].code`, never on message
|
|
519
569
|
text.
|
|
520
570
|
- `engine.render` / `session.render`: backend compilation failures and
|
|
521
571
|
validation errors.
|
|
@@ -527,6 +577,9 @@ compilation failures. The same shape applies to every throw site:
|
|
|
527
577
|
|
|
528
578
|
### Lifecycle
|
|
529
579
|
|
|
580
|
+
Handles begin at [`init`](#initialization), which instantiates the core build;
|
|
581
|
+
`Engine` instantiates a backend on the first render against it.
|
|
582
|
+
|
|
530
583
|
The wasm bindings are built with `--weak-refs`, so dropped `Document`,
|
|
531
584
|
`Quill`, and `LiveSession` handles are reclaimed by `FinalizationRegistry`
|
|
532
585
|
without manual `.free()` discipline. `.free()` is still emitted as an eager
|