@quillmark/wasm 0.104.0 → 0.105.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 +106 -27
- package/README.md +72 -206
- package/backends/pdfform/wasm.d.ts +460 -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 +460 -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 +332 -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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,111 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.105.0 - 2026-08-14
|
|
4
|
+
|
|
5
|
+
- feat(core,wasm,python)!: a `Content` nested inside a composite field is
|
|
6
|
+
readable at its own codec. `TypedReader::get_content_at(name, path)` (and the
|
|
7
|
+
`CardReader` twin, `reader.getContentAt(addr, path)` in JS,
|
|
8
|
+
`reader.get_content_at(name, path)` in Python) walks a `PathSegment` path
|
|
9
|
+
through the field schema — `items` for an index, `properties` for a key — to
|
|
10
|
+
the leaf whose declared type names the codec, then decodes through the same
|
|
11
|
+
dispatch the whole-field read uses. So an `array<richtext>` element, an
|
|
12
|
+
`object`'s content property and a leaf under both each read back the same
|
|
13
|
+
`Content` whatever their resting form, where before every one of them
|
|
14
|
+
answered `FieldNotContent` and the consumer had to decide for itself what the
|
|
15
|
+
stored bytes meant (#1243). The empty path *is* `get_content`. A path naming
|
|
16
|
+
nothing in the stored value reads absent rather than throwing: an editor's
|
|
17
|
+
row index goes stale between derive and read, and that is the axis a repeater
|
|
18
|
+
mutates. `Addr` deliberately gains no element axis — the path is the read's
|
|
19
|
+
own argument, since `storeField` / `isFill` / `applyChange` could not answer
|
|
20
|
+
one. **Breaking**: `EditError::FieldDecode` and `EditError::FieldNotContent`
|
|
21
|
+
each gain an `at: Vec<PathSegment>` field carrying the in-field path, so the
|
|
22
|
+
diagnostic anchors at `main.paragraphs[1]` and parses back to those segments;
|
|
23
|
+
`field` stays a bare field name and `args` is unchanged. `FieldNotContent`
|
|
24
|
+
now names the type *reached*, so a `string[]` element reports `string` rather
|
|
25
|
+
than the field's `array`.
|
|
26
|
+
- feat(core,wasm)!: `must_fill:` on a field declares the **obligation** axis.
|
|
27
|
+
`default:` carried the fill value and the obligation signal on one bit, so
|
|
28
|
+
only that 2x2's diagonal was reachable; a safe value that still wants a
|
|
29
|
+
human's confirmation (`default: UNCLASSIFIED` with `must_fill: true`) and a
|
|
30
|
+
genuinely optional field with nothing to suggest (`must_fill: false`) now
|
|
31
|
+
each have a spelling. Left unset it derives `default.is_none()`, so no
|
|
32
|
+
existing quill's blueprint marker set changes. `Quill::validate` gains a
|
|
33
|
+
second trigger under the one `validation::must_fill` code, named by a
|
|
34
|
+
`trigger` arg: `marker` for a `!must_fill` tag the document carries, and
|
|
35
|
+
`unauthored` where the schema obliges a cell the document leaves absent or
|
|
36
|
+
present-null. The second closes a hole — `validate_fills` walked only the
|
|
37
|
+
payload, so a hand-written or programmatically built document drew no
|
|
38
|
+
completeness signal whatever `Quill.yaml` declared. **Breaking**: a merely
|
|
39
|
+
incomplete document no longer validates clean. Absence is still never
|
|
40
|
+
*malformed* and still never gates render, but a consumer reading "any
|
|
41
|
+
diagnostic ⇒ not done" now sees a warning per unauthored obliged cell on
|
|
42
|
+
documents that were silent. The obligation keys on cell presence rather than
|
|
43
|
+
the resolved source rung, so a must-fill leaf inside a container someone
|
|
44
|
+
touched still warns; a typed dict is never itself a cell and recurses to its
|
|
45
|
+
leaves, while an array is one cell, `[]` being a real answer. Authoring the
|
|
46
|
+
field's blank discharges it and `field: null` does not: null ≡ absent stays
|
|
47
|
+
unqualified on the value ladder, but obligation asks whether a human made a
|
|
48
|
+
call. Seeding stamps the marker on example-seeded obliged cells, so a fresh
|
|
49
|
+
seed and an empty document report the same cells, and the transform schema
|
|
50
|
+
carries `quillmark:must_fill` (#1255).
|
|
51
|
+
- fix(core,wasm,pdfform)!: a field's **blank** — its spelling of "explicitly
|
|
52
|
+
nothing" — is a property of the field rather than a member of its type's
|
|
53
|
+
domain, and an `enum`'s is `""`. The render floor for a defaultless enum
|
|
54
|
+
returned `values.first()`: a choice nobody made, indistinguishable at the
|
|
55
|
+
plate from a deliberate one and reachable from a cosmetic `values:` reorder.
|
|
56
|
+
An unanswered enum now renders `""`, so a reorder is render-safe for every
|
|
57
|
+
document and only removing or renaming a member breaks
|
|
58
|
+
([VERSIONING.md](prose/canon/VERSIONING.md)). **The accepted domain widens to
|
|
59
|
+
`values ∪ blank` for *every* enum**, defaulted ones included: `format: ""`
|
|
60
|
+
was a fatal `EnumViolation` and now coerces, validates and reaches the plate.
|
|
61
|
+
**A plate must therefore branch exhaustively over `values ∪ blank`** — an
|
|
62
|
+
`else` fallback re-opens exactly the fabrication the blank closes, and a
|
|
63
|
+
downstream package that asserts membership fails the compile outright. Note
|
|
64
|
+
`data.at(key, default: X)` is not a guard here: blank-filled render makes
|
|
65
|
+
every declared key present, so its `default:` is dead code and the blank
|
|
66
|
+
flows through. **Breaking**: `zero_value` → `blank`, `FieldSource::Zero` →
|
|
67
|
+
`Blank` and its wire token `"zero"` → `"blank"`; `""` declared in `values:`
|
|
68
|
+
is a load error (`quill::enum_blank_member`), the engine supplying the blank
|
|
69
|
+
instead; and `date: ""` renders blank rather than falling back to a
|
|
70
|
+
`default:`, settling a three-way disagreement between coercion, validation
|
|
71
|
+
and the floor. `default: ""` stays valid and keeps its meaning — `values:`
|
|
72
|
+
enumerates choices, `default:` is a value, and the blank is a legal value
|
|
73
|
+
that is never a choice. Additive: `ui.blank_title` labels an enum's blank and
|
|
74
|
+
rides the transform schema as `quillmark:blank_title`; that schema's `enum:`
|
|
75
|
+
leads with the blank, so a standard JSON-Schema validator accepts what the
|
|
76
|
+
engine accepts, and pdfform Choice widgets lead their options with it too. A
|
|
77
|
+
consumer's picker must keep the blank selectable and re-selectable — returning
|
|
78
|
+
to it is how an author clears a cell back to unset.
|
|
79
|
+
`integer`, `number` and `boolean` keep `0` / `false` as their blank,
|
|
80
|
+
indistinguishable from an authored zero — a permanent seam, since a wire
|
|
81
|
+
`none` would cost the totality the floor exists to buy. Full guide:
|
|
82
|
+
[0.104 → 0.105](docs/migrations/0.104-to-0.105.md) (#1254).
|
|
83
|
+
- fix(fixtures,docs): the three fixture plates that dispatch on `$kind` read it
|
|
84
|
+
with a bare `card.at("$kind")`, which panics on a kindless card, and guarded
|
|
85
|
+
declared fields against an absence blank-filled render makes impossible.
|
|
86
|
+
Plate authors copy the fixtures rather than `PLATE_DATA.md`, so the fixtures
|
|
87
|
+
were teaching both the unsafe metadata read and a dead presence check.
|
|
88
|
+
`classic_resume` carried the live consequence: `url` is declared with no
|
|
89
|
+
`default:`, so the floor delivers `""`, `default: none` never fires, and the
|
|
90
|
+
package's `url != none` test always passes — an empty Courier element where
|
|
91
|
+
the block should have been skipped. Its `subheading-*` guard cost an empty
|
|
92
|
+
grid row the same way. Declared fields now guard their *value*, and
|
|
93
|
+
`docs/quills/typst-backend.md` states the rule as a table over the three key
|
|
94
|
+
kinds. `fixture_quills_render_test` renders every fixture quill's seed
|
|
95
|
+
document — the net that was missing, since `classic_resume`'s plate had no
|
|
96
|
+
test reaching it (#1256, #1257).
|
|
97
|
+
- test(typst): `plaintext` reaches regions and navigation by inheritance — the
|
|
98
|
+
render floor coerces its resting literal to a content object, the backend
|
|
99
|
+
classifies that object by `contentMediaType` alone, and the shared lowering
|
|
100
|
+
emits it with a segment map — and every step was load-bearing and untested,
|
|
101
|
+
with the classification predicates named for richtext so the sharing read as
|
|
102
|
+
a coincidence. Pinned at engine altitude, because a test driving the backend
|
|
103
|
+
directly hand-builds the content object, bypassing the floor's coercion, and
|
|
104
|
+
would stay green through a regression that silently empties `regions()`. The
|
|
105
|
+
predicates are `is_content_field` / `is_content_array_field` /
|
|
106
|
+
`is_inline_content_field`, and `PREVIEW.md` names plaintext beside richtext
|
|
107
|
+
in its producer list (#1247, #1250).
|
|
108
|
+
|
|
3
109
|
## v0.104.0 - 2026-08-13
|
|
4
110
|
|
|
5
111
|
- feat(core,wasm): a quill declares, per body, the block constructs its plate
|
|
@@ -93,33 +199,6 @@
|
|
|
93
199
|
fixture suite reached neither path. A fourth carries a `!must_fill` tag on two
|
|
94
200
|
example-seeded cells through seed → store → load → conform. Refs #1234
|
|
95
201
|
|
|
96
|
-
<!-- seed: commits since v0.103.0, confirm the entries above cover them, then delete this comment
|
|
97
|
-
- Carry the thematic-break and unsupported-construct work into the guide
|
|
98
|
-
- Dense-prose pass over the new comments and docs
|
|
99
|
-
- Let a quill declare the constructs its plate does not typeset
|
|
100
|
-
- Drain the memo's heading buffer instead of dropping it
|
|
101
|
-
- Canonicalize a thematic break to `***`
|
|
102
|
-
- Close the 0.104 release gaps: the migration guide, and four changelog entries
|
|
103
|
-
- Retire the enum: modifier; project a field's domain by capability
|
|
104
|
-
- Pin the render floor's two type-domain edges and the seeded fill tag
|
|
105
|
-
- Raise the Node floor to 24
|
|
106
|
-
- dense-prose pass over the added prose
|
|
107
|
-
- docs: dense-prose pass on the text-type sections
|
|
108
|
-
- docs: give the text-type choice a decision procedure
|
|
109
|
-
- docs: state the backend seam's scope and reserve its namespaces
|
|
110
|
-
- docs: drop the private intra-doc link from pathFor
|
|
111
|
-
- fix(core): state the unspellable-tail conditions instead of linking a private fn
|
|
112
|
-
- chore(wasm): density pass, and gate a core-build dead function
|
|
113
|
-
- docs: dense-prose pass over the geometry-address translation
|
|
114
|
-
- docs: dense-prose pass over the pathFor / cardPath prose
|
|
115
|
-
- test(wasm): execute the static-export exemption instead of listing it
|
|
116
|
-
- feat(wasm): mint a DocPath from an Addr (#1225)
|
|
117
|
-
- fix(core)!: parse geometry addresses segment-wise (#1223)
|
|
118
|
-
- docs: carry the 0.103 CLI break, and the init gate into the quickstart (#1222)
|
|
119
|
-
- SCHEMAS.md: a declared type change rewrites stored values (#1221)
|
|
120
|
-
-->
|
|
121
|
-
|
|
122
|
-
|
|
123
202
|
## v0.103.0 - 2026-08-09
|
|
124
203
|
|
|
125
204
|
- docs: `docs/integration/operations.md`, carrying what the other integration
|
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)
|