@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 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 bundler` and `--weak-refs` enabled (see
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 "corpus or string?" by the field's declared codec rather than by how the
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.schemaVersionOf(blob); // undefined | string
203
- if (v && v !== Document.currentSchemaVersion()) {
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
- `schemaVersionOf` does not validate the payload: it only reads the
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`; `getMarkdown` is the body markdown read (a `CardAddr`; a field's
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 corpus either way read `quill.reader(doc).getContent(addr)`, which
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` / `setBody` / `reviseField` /
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 corpus, whichever lane stored it
366
- v.getBody(); // the main body markdown (quill-free)
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 corpus; both decode through the codec
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 `FieldRichtextDecode`; an absent field reads back `undefined`.
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::field_conform`, …). Route on `diagnostics[0].code`, never on message
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