@quillmark/wasm 0.101.0 → 0.103.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 +181 -0
- package/README.md +85 -19
- package/backends/pdfform/wasm.d.ts +232 -68
- package/backends/pdfform/wasm.js +2979 -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 +232 -68
- package/backends/typst/wasm.js +3003 -8
- package/backends/typst/wasm_bg.wasm +0 -0
- package/backends/typst/wasm_bg.wasm.d.ts +8 -8
- package/core/wasm.d.ts +199 -59
- package/core/wasm.js +2470 -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 +109 -44
- package/runtime/runtime.js +287 -70
- package/backends/pdfform/wasm_bg.js +0 -2863
- package/backends/typst/wasm_bg.js +0 -2887
- package/core/wasm_bg.js +0 -2359
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,186 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.103.0 - 2026-08-09
|
|
4
|
+
|
|
5
|
+
- docs: `docs/integration/operations.md`, carrying what the other integration
|
|
6
|
+
pages leave unsaid: that **render is not bounded** — no deadline, no
|
|
7
|
+
cancellation, and the parse limits do not carry through — with the
|
|
8
|
+
worker-termination recipe that is the only abort a browser has; that
|
|
9
|
+
`Quillmark`, `Quill` and `Document` are `Send + Sync`, pinned by a test rather
|
|
10
|
+
than asserted; `comemo` eviction as what a long-lived process's memory tracks;
|
|
11
|
+
the no-network, no-ambient-filesystem isolation properties; and that a panic
|
|
12
|
+
is terminal on every surface.
|
|
13
|
+
- docs: `parse::input_too_large` carries four of the five §8 caps, separable
|
|
14
|
+
only by its `max` arg, which `error-handling.md` now says where it names the
|
|
15
|
+
code.
|
|
16
|
+
- test(cli): `quillmark-cli` gets its first tests. The bin carries
|
|
17
|
+
`test = false`, so twelve cases drive the built executable instead — every
|
|
18
|
+
subcommand, `-o` and `--stdout`, PDF and SVG output, and the error paths,
|
|
19
|
+
which must exit 1 rather than panic. The crate had no `[dev-dependencies]`
|
|
20
|
+
and no workflow invoked it (#1068).
|
|
21
|
+
- fix(cli)!: `render --verbose` writes its progress lines to stderr, as the
|
|
22
|
+
warning printer already did. Under `--stdout` they went to stdout ahead of and
|
|
23
|
+
after the artifact, so `quillmark render q --stdout --verbose > out.pdf`
|
|
24
|
+
produced a PDF with `Loading quill from: …` before its header and
|
|
25
|
+
`Rendering completed successfully` past its trailer. A script that parses
|
|
26
|
+
`--verbose` output from stdout reads it from stderr now.
|
|
27
|
+
- test(fuzz): `pdf_fuzz` covers the AcroForm stamp spine's byte-level reads,
|
|
28
|
+
the one hand-rolled parser with no fuzz target. Arbitrary bytes, and a real
|
|
29
|
+
form truncated, single-byte-corrupted, or spliced, all through
|
|
30
|
+
`page_media_boxes` / `PdfUpdate::begin` / `stamp`. The oracle is no panic:
|
|
31
|
+
nothing in the workspace catches unwind, so a panic there kills the CLI and
|
|
32
|
+
the Python extension and poisons the WASM module. No failures found.
|
|
33
|
+
- fix(core): `MAX_FIELD_COUNT`'s rustdoc said "per document"; the check is per
|
|
34
|
+
card-yaml block, counted after `$`-key extraction.
|
|
35
|
+
- refactor(wasm)!: `init()` resolves to the core surface, and it is the only way
|
|
36
|
+
to reach one. `Quill`, `Document`, `importMarkdown`, `exportMarkdown`,
|
|
37
|
+
`rebase`, `mapPos`, `parseDocPath` and `formatDocPath` leave the static
|
|
38
|
+
exports of `@quillmark/wasm`: `const { Quill, Document } = await init()`
|
|
39
|
+
replaces the value import. The precondition was carried entirely by
|
|
40
|
+
`init`'s signature, and a floating promise is an ESLint rule rather than a
|
|
41
|
+
`tsc` diagnostic, so a call site that skipped the await type-checked and then
|
|
42
|
+
passed or failed by load order. It now has no name to call. `Engine`,
|
|
43
|
+
`MAIN_CARD_ADDR`, `isQuillmarkError`, the open-set guards and the
|
|
44
|
+
writer/reader classes are unchanged, needing no instance or gated by their
|
|
45
|
+
arguments; the `Quill` / `Document` **type** exports are unchanged, so
|
|
46
|
+
annotations and `import type` compile as before. Class identity is untouched:
|
|
47
|
+
the gate hands out the core build's classes verbatim, and `instanceof` stays
|
|
48
|
+
the whole membership test. `runtime::not_initialized` and the build-time
|
|
49
|
+
sentinel that raised it retire with the door they guarded. Rust, Python,
|
|
50
|
+
documents and stored blobs are unaffected. See
|
|
51
|
+
`docs/migrations/0.102-to-0.103.md`
|
|
52
|
+
|
|
53
|
+
<!-- seed: commits since v0.102.0, confirm the entries above cover them, then delete this comment
|
|
54
|
+
- ci: tag the merge commit, and serialize a release against itself
|
|
55
|
+
- ci: drop the tagger GitHub App from the release workflows
|
|
56
|
+
- Cut SECURITY.md, and operations.md down to what only it can say
|
|
57
|
+
- Drop the audit gate; keep the reachability finding in ci.yml's note
|
|
58
|
+
- Fix two wrong claims, a dead property, and the stdout bug the review found
|
|
59
|
+
- dense-prose pass over the added prose, and read the fixture once
|
|
60
|
+
- Close four production-readiness gaps: audit gate, ops docs, CLI tests, PDF fuzzing
|
|
61
|
+
- Drop the README's claim about the deleted sentinel patch
|
|
62
|
+
- dense-prose pass: cut the restatement the change introduced
|
|
63
|
+
- docs: the 0.102 → 0.103 step, and correct the surface it describes
|
|
64
|
+
- The gate is the only door to the core surface
|
|
65
|
+
- docs(migration): a span guide for 0.92 → 0.102
|
|
66
|
+
- Cut over-specified style rules; author preference wins
|
|
67
|
+
- dense-prose: remove the em-dash ban
|
|
68
|
+
- dense-prose: drop the line-budget clause
|
|
69
|
+
- dense-prose: wrong is worse than missing; relax em-dash ban
|
|
70
|
+
- docs: dense-prose pass over the conform-scope and date-grammar prose
|
|
71
|
+
- docs: migration guides are era-stamped, not immutable
|
|
72
|
+
- docs(migrations): give the datetime split its corpus audit step
|
|
73
|
+
- docs: scope conform to content fields, state the date corpus precondition
|
|
74
|
+
- Tighten the failure-delivery prose
|
|
75
|
+
- Deliver init's conflict as a rejection, not a synchronous throw
|
|
76
|
+
-->
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
## v0.102.0 - 2026-08-04
|
|
80
|
+
|
|
81
|
+
The pre-1.0 vocabulary reset. Verbs, diagnostic codes, and two words that meant
|
|
82
|
+
different things at different altitudes. Documents and stored blobs are
|
|
83
|
+
untouched: no document reparses, no blob remigrates, and the storage wire format
|
|
84
|
+
is byte-identical, but nearly every consumer touches at least one renamed verb.
|
|
85
|
+
The diagnostic codes are the part that could not wait, since consumers route on
|
|
86
|
+
them and 1.0 freezes them. All breaking changes are covered by
|
|
87
|
+
`docs/migrations/0.101-to-0.102.md`. Only two behaviors change, both called out
|
|
88
|
+
below. Separately, `@quillmark/wasm` ships `--target web`, which makes
|
|
89
|
+
`await init()` mandatory and the bundler plugins the package used to demand
|
|
90
|
+
unnecessary.
|
|
91
|
+
|
|
92
|
+
- refactor(core,wasm,python)!: the verb vocabulary is reset to say what the code
|
|
93
|
+
does. `install_body` / `install_field` / `doc.install` become `overwrite_*` /
|
|
94
|
+
`doc.overwrite`: the content lane is a ladder sorted by the fate of the
|
|
95
|
+
identity anchors already on the value (overwrite destroys, revise rebases,
|
|
96
|
+
apply preserves), and only the first keeps nothing, which grouping the three as
|
|
97
|
+
"identity-aware" hid. `LiveSession::apply` becomes `update`, taking the trait
|
|
98
|
+
seam, the feature-gated raw-plate seam, and `backend::apply_unsupported` →
|
|
99
|
+
`backend::update_unsupported` with it, so the content lane's `applyChange`
|
|
100
|
+
splice is no longer a homograph of a whole-document recompile.
|
|
101
|
+
`apply_field_richtext_change` becomes `apply_field_change`, matching its
|
|
102
|
+
schema-blind neighbour `revise_field`, which never carried the codec in its
|
|
103
|
+
name. `store_seed_namespace` / `remove_seed_namespace` become
|
|
104
|
+
`store_seed_overlay` / `remove_seed_overlay`, closing the asymmetry with the
|
|
105
|
+
`seedOverlay` read and the `SeedOverlay` type that already shipped: `$seed` is
|
|
106
|
+
keyed by a validated card-kind, `$ext` by a free-form consumer namespace.
|
|
107
|
+
`getMarkdown` and reader `get_body` become `bodyMarkdown` / `body_markdown`,
|
|
108
|
+
the name core already used, so one projection has one name on every surface
|
|
109
|
+
(#1186). See `docs/migrations/0.101-to-0.102.md`
|
|
110
|
+
- refactor(core,wasm,python)!: the mutator and validation diagnostic codes are
|
|
111
|
+
corrected, and the content-field failures carry the codec that ran.
|
|
112
|
+
`edit::field_richtext_decode` becomes `edit::field_decode` and
|
|
113
|
+
`edit::field_richtext_not_inline` becomes `edit::field_not_inline`, each
|
|
114
|
+
gaining a `codec` arg (`"richtext"` or `"plaintext"`): both were raised for
|
|
115
|
+
plaintext failures under a richtext name, and `plaintext(inline)` fell through
|
|
116
|
+
to the generic code. `edit::field_conform` becomes
|
|
117
|
+
`edit::field_coercion_failed`, killing the `conform::field_conform` stutter and
|
|
118
|
+
landing the variant beside its real twin `validation::coercion_failed`, minted
|
|
119
|
+
from the same `CoercionError`. `richtext::not_inline` and
|
|
120
|
+
`plaintext::not_plain` become `validation::not_inline` / `validation::not_plain`,
|
|
121
|
+
stage-namespaced like every other code. The `conform::*` twins rename in
|
|
122
|
+
lockstep. Route on the code and read `codec` for the lane (#1186). See
|
|
123
|
+
`docs/migrations/0.101-to-0.102.md`
|
|
124
|
+
- fix(core)!: a splice against an absent field is no longer a decode error. The
|
|
125
|
+
writer `set_body` / `setBody` becomes `revise_body` / `reviseBody` and returns
|
|
126
|
+
the text `Delta` it was already computing (a body carries no field schema, so a
|
|
127
|
+
typed-lane verb had nothing to type); Python discards the receipt, as on
|
|
128
|
+
`revise_field`. `apply_field_change` on an absent field splices against the
|
|
129
|
+
empty content, as `revise_field` diffs against it, instead of reporting
|
|
130
|
+
`FieldRichtextDecode { message: "field is absent" }`: the one place in the API
|
|
131
|
+
where a missing field was not simply `None`. No error is lost. A bundle that
|
|
132
|
+
expected content still fails, and now reports the condition it hit, since the
|
|
133
|
+
text delta declares the base length it was computed against, so a stale splice
|
|
134
|
+
lands as `edit::content_apply`. Route a vanished-field check on that code, or
|
|
135
|
+
check presence before splicing. These are the release's only behavior changes
|
|
136
|
+
(#1186). See `docs/migrations/0.101-to-0.102.md`
|
|
137
|
+
- refactor(core)!: `EditError::variant_name` is removed and the typed primitives
|
|
138
|
+
leave the documented surface. The bare variant name was a second discriminator
|
|
139
|
+
kept in lockstep with `code()` that nothing routed on: both binding error
|
|
140
|
+
mappers stamp `code()` onto the `Diagnostic` they raise, and `ERROR.md` has
|
|
141
|
+
said identity is the code since the `edit::*` family landed. Assert on `code()`
|
|
142
|
+
or match the enum, which is `#[non_exhaustive]` either way. `Card::commit_field`
|
|
143
|
+
and `Card::revise_field_checked` become `#[doc(hidden)]`: a resolved
|
|
144
|
+
`FieldSchema` argument was the only thing telling them from their opaque and
|
|
145
|
+
schema-blind neighbours, and disambiguating by argument is a third mechanism
|
|
146
|
+
beside the receiver and the verb. `quill.writer(&mut doc)` is the typed door on
|
|
147
|
+
every surface now, core included; the primitives stay callable on the same
|
|
148
|
+
terms as the other hidden items (#1186). See `docs/migrations/0.101-to-0.102.md`
|
|
149
|
+
- docs(content,core)!: restoring a deleted island re-lands its original id, and
|
|
150
|
+
the island channel states two contracts it had left to the implementation. The
|
|
151
|
+
never-ambient minting rule (continue the field's positional `isl-{n}` sequence,
|
|
152
|
+
never a UUID or a clock reading) covers a *new* island; a delete frees its id
|
|
153
|
+
and the id travels back with the island, so an editor that mints fresh on undo
|
|
154
|
+
renames it, moving the content hash of a document the user believes they
|
|
155
|
+
restored. A pasted copy of a live island is new and mints fresh. Alongside it:
|
|
156
|
+
an island op's `at` is *sequenced*, counting the text the delta and this
|
|
157
|
+
bundle's earlier island ops left rather than the shared post-delta frame; and a
|
|
158
|
+
producer's whole-field diff that carries an island slot must split into the
|
|
159
|
+
slot-free `delta` plus one `Insert` per slot, since a slot in an insert string
|
|
160
|
+
orphans. Neither is a behavior change, and a producer that guessed wrong got a
|
|
161
|
+
wrong document rather than an error (#1185)
|
|
162
|
+
- feat(wasm)!: the package ships `--target web` and the runtime owns
|
|
163
|
+
instantiation, so `await init()` once at startup is required before any export
|
|
164
|
+
is used. `--target bundler` emitted `import * as wasm from "./wasm_bg.wasm"`,
|
|
165
|
+
which no browser and no bundler resolves natively; the plugin that fixed it
|
|
166
|
+
rewrote the import into a top-level await, and because the runtime statically
|
|
167
|
+
re-exports core, that await landed on the static module graph of everything
|
|
168
|
+
importing `@quillmark/wasm`: a permanent constraint on consumer architecture
|
|
169
|
+
and a blank SvelteKit route in Safari's dev server that neither Chrome nor
|
|
170
|
+
`vite build` showed. In exchange, `vite-plugin-wasm` and
|
|
171
|
+
`vite-plugin-top-level-await` are no longer needed, a static import is safe
|
|
172
|
+
anywhere including SSR, and plain Node can import the package at all (the ESM
|
|
173
|
+
`.wasm` import needed `--experimental-wasm-modules`). Only
|
|
174
|
+
`optimizeDeps: { exclude: ['@quillmark/wasm'] }` stays, for Vite's dev server.
|
|
175
|
+
`init` memoizes its promise, so several entry points share one instantiation
|
|
176
|
+
and a failed attempt clears the memo for a retry; backends are not the
|
|
177
|
+
consumer's to initialize, since `Engine` instantiates one inside its lazy load.
|
|
178
|
+
Reaching the surface early throws `runtime::not_initialized` naming the fix,
|
|
179
|
+
and `build-wasm.sh` asserts both the guard's anchors and that no artifact
|
|
180
|
+
carries a `.wasm` import or a top-level await. `initSync` is not exported: the
|
|
181
|
+
capability remains through `init(source)`, which takes bytes, a `Response`, a
|
|
182
|
+
`WebAssembly.Module`, or a URL (#1189). See `docs/migrations/0.101-to-0.102.md`
|
|
183
|
+
|
|
3
184
|
## v0.101.0 - 2026-08-03
|
|
4
185
|
|
|
5
186
|
- refactor(core): gate the raw-plate seam behind a feature, and fixes from review
|
package/README.md
CHANGED
|
@@ -10,11 +10,11 @@ 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), the **canonical API**: `
|
|
14
|
-
an `Engine` that renders them.
|
|
13
|
+
- `@quillmark/wasm` (the root), the **canonical API**: `init`, resolving to
|
|
14
|
+
`Quill` and `Document`, and an `Engine` that renders them.
|
|
15
15
|
|
|
16
|
-
`Quill` and `Document` are
|
|
17
|
-
|
|
16
|
+
`Quill` and `Document` are the internal Typst-less core build's own classes,
|
|
17
|
+
handed out verbatim by `init`, so editor/validation code (`Quill.fromTree`,
|
|
18
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
|
|
@@ -30,8 +30,9 @@ 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 asserts
|
|
35
|
+
none of them carries a `.wasm` ESM import or a top-level await.
|
|
35
36
|
|
|
36
37
|
## Test
|
|
37
38
|
|
|
@@ -45,7 +46,9 @@ npm test
|
|
|
45
46
|
## Usage
|
|
46
47
|
|
|
47
48
|
```ts
|
|
48
|
-
import {
|
|
49
|
+
import { init, Engine } from "@quillmark/wasm";
|
|
50
|
+
|
|
51
|
+
const { Quill, Document } = await init(); // see Initialization
|
|
49
52
|
|
|
50
53
|
const quill = Quill.fromTree(tree); // no engine needed: build + validate
|
|
51
54
|
const engine = new Engine(); // loads a backend lazily on first render
|
|
@@ -62,6 +65,58 @@ const parsed = Document.fromMarkdown(markdown);
|
|
|
62
65
|
const result = await engine.render(quill, parsed, { format: "pdf" });
|
|
63
66
|
```
|
|
64
67
|
|
|
68
|
+
## Initialization
|
|
69
|
+
|
|
70
|
+
`init` resolves to the core surface: `Quill`, `Document`, and the free
|
|
71
|
+
functions. Everything after the await is the synchronous surface the rest of
|
|
72
|
+
this README describes.
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
import { init, Engine } from "@quillmark/wasm";
|
|
76
|
+
const { Quill, Document } = 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 non-conflicting call returns
|
|
87
|
+
the same promise, so several entry points may each `await init()` for one
|
|
88
|
+
instantiation. Destructure at **every** entry point (route loader, hydration
|
|
89
|
+
path, worker) rather than threading one result around. A failed init clears the
|
|
90
|
+
memo, so a retry works. Each realm initializes its own copy, a Worker included.
|
|
91
|
+
|
|
92
|
+
**Backends need nothing.** `Engine` instantiates a backend inside its lazy load,
|
|
93
|
+
on the first render against it.
|
|
94
|
+
|
|
95
|
+
**Overriding the source.** `init(source)` accepts bytes, a `Response`, a
|
|
96
|
+
`WebAssembly.Module`, or a URL, for hosts that route assets themselves or embed
|
|
97
|
+
the binary. Pass it on the first call; a later call passing a *different* source
|
|
98
|
+
rejects with `runtime::init_conflict` rather than silently ignoring it. Passing
|
|
99
|
+
the same value again is fine, so several entry points may each
|
|
100
|
+
`await init(BYTES)` against one constant.
|
|
101
|
+
|
|
102
|
+
**Both failures reject.** `runtime::init_conflict` and `runtime::init_failed`
|
|
103
|
+
alike ride the returned promise, so one `catch` around `await init(...)` covers
|
|
104
|
+
the gate. See [Errors](#errors) for the rule this follows.
|
|
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.
|
|
109
|
+
|
|
110
|
+
**Vite's dev server** pre-bundles dependencies, which moves the package away
|
|
111
|
+
from its binary. Exclude it:
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
// vite.config.js
|
|
115
|
+
export default { optimizeDeps: { exclude: ["@quillmark/wasm"] } };
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
A load failure surfaces as `runtime::init_failed`, whose hint names that line.
|
|
119
|
+
|
|
65
120
|
## API
|
|
66
121
|
|
|
67
122
|
### `new Engine(options?)`
|
|
@@ -107,7 +162,7 @@ The **bound door**, and the primary ingestion path. `quill.parse` is
|
|
|
107
162
|
`Document.fromMarkdown` followed by `conform`: the returned document's declared
|
|
108
163
|
content fields rest at one form per codec (a `richtext` field as the canonical
|
|
109
164
|
content object, a `plaintext` field as its literal string), so `getStored`
|
|
110
|
-
answers "
|
|
165
|
+
answers "content object or string?" by the field's declared codec rather than by how the
|
|
111
166
|
document was built. Parse warnings and the `conform::*` warnings both ride
|
|
112
167
|
`doc.warnings`.
|
|
113
168
|
|
|
@@ -199,13 +254,13 @@ and writes that same value.
|
|
|
199
254
|
To detect a version mismatch before parsing, use the static accessors:
|
|
200
255
|
|
|
201
256
|
```ts
|
|
202
|
-
const v = Document.
|
|
203
|
-
if (v && v !== Document.
|
|
257
|
+
const v = Document.storageVersionOf(blob); // undefined | string
|
|
258
|
+
if (v && v !== Document.currentStorageVersion()) {
|
|
204
259
|
// payload is from a build with a different model version
|
|
205
260
|
}
|
|
206
261
|
```
|
|
207
262
|
|
|
208
|
-
`
|
|
263
|
+
`storageVersionOf` does not validate the payload: it only reads the
|
|
209
264
|
`schema` field, returning `undefined` for non-JSON, non-objects, or
|
|
210
265
|
payloads that don't carry one. Use it to distinguish "wrong version" from
|
|
211
266
|
"corrupt" when `fromJson` throws.
|
|
@@ -285,11 +340,11 @@ main card's `qty`, `doc.storeField({ card: 2, field: "qty" }, 3)` a composable
|
|
|
285
340
|
card's. Reads are total over the field axis (`getStored` → `undefined`, `isFill` → `false` for
|
|
286
341
|
an absent field; only an out-of-range card throws); field writes throw on a body
|
|
287
342
|
address. `getStored` is the verbatim transport read, distinct from the interpreted
|
|
288
|
-
`quill.reader(doc).get`; `
|
|
343
|
+
`quill.reader(doc).get`; `bodyMarkdown` is the body markdown read (a `CardAddr`; a field's
|
|
289
344
|
markdown is read through `quill.reader(doc).get(field)`). A content field's stored
|
|
290
345
|
form follows how the document was built (a canonical content object when the
|
|
291
346
|
typed writer committed it, the authored string when a markdown parse produced
|
|
292
|
-
it), so for the
|
|
347
|
+
it), so for the `Content` either way read `quill.reader(doc).getContent(addr)`, which
|
|
293
348
|
decodes through the codec the field's declared type names. Card-scoped verbs take a
|
|
294
349
|
`CardAddr` (`{ card? }`) first: `doc.getExt({ card: 2 })`, and the batch below.
|
|
295
350
|
|
|
@@ -309,7 +364,7 @@ writes go through the schema-bound writer while the quill-free opaque store sits
|
|
|
309
364
|
on `Document` itself (**store** = verbatim, **set** = typed):
|
|
310
365
|
|
|
311
366
|
- **`quill.writer(doc)`: the typed door whenever a quill is in hand.** Bind the
|
|
312
|
-
schema once and issue bare `set` / `setAll` / `
|
|
367
|
+
schema once and issue bare `set` / `setAll` / `reviseBody` / `reviseField` /
|
|
313
368
|
`addCard` / `card(i)`. Each resolves the field's schema `type`, coerces the
|
|
314
369
|
value to its canonical form (`"3"` → `3`, a markdown string → a richtext
|
|
315
370
|
content), and **fails now** on a mismatch instead of at render. A name the schema
|
|
@@ -362,16 +417,16 @@ write.
|
|
|
362
417
|
```ts
|
|
363
418
|
const v = quill.reader(doc);
|
|
364
419
|
v.get("subject"); // by declared type: richtext → markdown, plaintext → literal text
|
|
365
|
-
v.getContent("subject"); // the same read as a Content
|
|
366
|
-
v.
|
|
420
|
+
v.getContent("subject"); // the same read as a `Content`, whichever lane stored it
|
|
421
|
+
v.bodyMarkdown(); // the main body markdown (quill-free)
|
|
367
422
|
v.card(0).get("body"); // a card field, resolved by its $kind
|
|
368
423
|
```
|
|
369
424
|
|
|
370
|
-
`get` projects and `getContent` returns the
|
|
425
|
+
`get` projects and `getContent` returns the `Content`; both decode through the codec
|
|
371
426
|
the field's **declared type** names, which is why they bind the quill and the
|
|
372
427
|
verbatim `doc.getStored` does not. An undeclared name throws `UnknownField`, a
|
|
373
428
|
type that is not a content leaf throws `FieldNotContent`, and an undecodable
|
|
374
|
-
value throws `
|
|
429
|
+
value throws `FieldDecode`; an absent field reads back `undefined`.
|
|
375
430
|
|
|
376
431
|
### `engine.render(quill, parsed, opts?)` vs. `engine.open(quill, parsed)`
|
|
377
432
|
|
|
@@ -497,6 +552,14 @@ try {
|
|
|
497
552
|
}
|
|
498
553
|
```
|
|
499
554
|
|
|
555
|
+
**Delivery follows the function, not the failure.** A synchronous method throws;
|
|
556
|
+
a promise-returning one rejects. The promise-returning surface is `init` and the
|
|
557
|
+
four `Engine` verbs (`render`, `open`, `supportedFormats`, `supportsCanvas`), so
|
|
558
|
+
a programming error reached through one of them (a foreign handle, an
|
|
559
|
+
unregistered backend) rejects like any other failure. Nothing here both returns
|
|
560
|
+
a promise and throws, so a `.catch` on a promise-returning call is a whole
|
|
561
|
+
guard.
|
|
562
|
+
|
|
500
563
|
`QuillmarkError` is a **structural interface, not a class**: the WASM layer
|
|
501
564
|
throws a real `Error` and attaches the property, so there is no constructor to
|
|
502
565
|
`instanceof` against; narrow with `isQuillmarkError` (which also works on
|
|
@@ -515,7 +578,7 @@ compilation failures. The same shape applies to every throw site:
|
|
|
515
578
|
- `Document` mutators (`storeField`, the writer's `set`, etc.): mutator
|
|
516
579
|
failures carry a namespaced `edit::*` `code` on `diagnostics[0]`
|
|
517
580
|
(`edit::invalid_field_name`, `edit::unknown_field`, `edit::index_out_of_range`,
|
|
518
|
-
`edit::
|
|
581
|
+
`edit::field_coercion_failed`, …). Route on `diagnostics[0].code`, never on message
|
|
519
582
|
text.
|
|
520
583
|
- `engine.render` / `session.render`: backend compilation failures and
|
|
521
584
|
validation errors.
|
|
@@ -527,6 +590,9 @@ compilation failures. The same shape applies to every throw site:
|
|
|
527
590
|
|
|
528
591
|
### Lifecycle
|
|
529
592
|
|
|
593
|
+
Handles begin at [`init`](#initialization), which instantiates the core build;
|
|
594
|
+
`Engine` instantiates a backend on the first render against it.
|
|
595
|
+
|
|
530
596
|
The wasm bindings are built with `--weak-refs`, so dropped `Document`,
|
|
531
597
|
`Quill`, and `LiveSession` handles are reclaimed by `FinalizationRegistry`
|
|
532
598
|
without manual `.free()` discipline. `.free()` is still emitted as an eager
|