@quillmark/wasm 0.114.0 → 0.116.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 +377 -0
- package/README.md +50 -49
- package/core/wasm.d.ts +93 -118
- package/core/wasm.js +114 -102
- package/core/wasm_bg.wasm +0 -0
- package/core/wasm_bg.wasm.d.ts +5 -5
- package/package.json +1 -1
- package/render/wasm.d.ts +119 -185
- package/render/wasm.js +138 -144
- package/render/wasm_bg.wasm +0 -0
- package/render/wasm_bg.wasm.d.ts +7 -7
- package/runtime/runtime.d.ts +52 -66
- package/runtime/runtime.js +25 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,382 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.116.0 - 2026-09-26
|
|
4
|
+
|
|
5
|
+
Upgrade path: [0.115 → 0.116](docs/migrations/0.115-to-0.116.md).
|
|
6
|
+
|
|
7
|
+
### What blocks a render
|
|
8
|
+
|
|
9
|
+
- feat(core)!: **a card no kind claims, and a body under `body.enabled: false`,
|
|
10
|
+
render and warn.** A render fails only where the engine would have to invent
|
|
11
|
+
what the author wrote: a value that will not read as its declared cell's
|
|
12
|
+
type, or markup the grammar cannot read. Input no declaration claims
|
|
13
|
+
displaces nothing, so it renders and warns; SCHEMAS.md § "What blocks a
|
|
14
|
+
render" sorts every validation code into incomplete, malformed and
|
|
15
|
+
unclaimed. `validation::unknown_card` and `validation::body_disabled` are
|
|
16
|
+
`Severity::Warning` from `Quill::validate`, `unknown_card` carries
|
|
17
|
+
`allowed`, and an unclaimed card keeps its place in `$cards` with its
|
|
18
|
+
fields verbatim. `ValidationError::UnknownCard` and `BodyDisabled` are
|
|
19
|
+
deleted. This is the release's silent break: a pipeline that relied on the
|
|
20
|
+
render failing catches nothing. (#1877)
|
|
21
|
+
- feat(core)!: **an undeclared key warns `validation::unknown_field`**, at
|
|
22
|
+
every depth of a claimed card: a card field, a typed dictionary's property,
|
|
23
|
+
an array element's, a matrix member's column, and a cell inside a variant
|
|
24
|
+
container no world declares. `args` carry `suggestion`, the closest declared
|
|
25
|
+
field the document leaves unwritten, or `container` and `variant` for a
|
|
26
|
+
variant cell written beside its discriminant rather than under it. (#1871)
|
|
27
|
+
- feat(quillmark,cli)!: **a one-shot render carries validate's warnings.**
|
|
28
|
+
`Quillmark::render`, WASM `engine.render` and Python `render` put every
|
|
29
|
+
`Quill::validate` warning on the result's `warnings` ahead of the compile's,
|
|
30
|
+
so the unclaimed input the page left out is on the result a pipeline reads.
|
|
31
|
+
A session's warnings stay its current compile's. (#1931)
|
|
32
|
+
- feat(core)!: **a card-yaml block with no `$kind` fails the parse** as
|
|
33
|
+
`parse::missing_kind`, located at its opener, with a hint naming both fixes:
|
|
34
|
+
a `$kind:` line, or a backtick fence. Such a block was a card missing its
|
|
35
|
+
kind line or tilde-fenced code, and reading it as either was a guess. Every
|
|
36
|
+
card now carries `$kind`: `ResolvedCard.kind` is `String` (`string` in
|
|
37
|
+
TypeScript), and a stored card with no `$kind` folds into the body above it
|
|
38
|
+
on load, its payload as a code block, then its body. (#1905)
|
|
39
|
+
- feat(core)!: **a card-yaml payload that is not a mapping fails at its
|
|
40
|
+
fence** as `parse::payload_not_mapping`, located at the opener, with `actual`
|
|
41
|
+
and `info` args and a hint naming the backtick fence, where it was an
|
|
42
|
+
unlocated `parse::invalid_structure` on a card and `parse::missing_quill` on
|
|
43
|
+
the root. (#1873)
|
|
44
|
+
|
|
45
|
+
### `!must_fill` is removed
|
|
46
|
+
|
|
47
|
+
- refactor(core)!: **`!must_fill` and `validation::must_fill` are gone.** The
|
|
48
|
+
marker said a cell held a placeholder rather than an answer, and the schema
|
|
49
|
+
half of the warning read `default:`'s absence as an obligation. `default:`
|
|
50
|
+
is the value an unanswered cell renders, `?` moves the render floor, and
|
|
51
|
+
null ≡ absent holds on every surface. A value under the marker keeps its
|
|
52
|
+
value: in markdown the tag drops as any unknown tag does, warned
|
|
53
|
+
`parse::unsupported_yaml_tag` at the tagged node's rooted path (which that
|
|
54
|
+
code now carries), and a stored row of any earlier tag is read past its
|
|
55
|
+
`fill` keys. Deleted: `store_fill`,
|
|
56
|
+
`is_fill`, the `QuillValue` fill set, `FieldSchema::{must_fill,
|
|
57
|
+
example_content}`, `edit::fill_on_mapping`,
|
|
58
|
+
`parse::fill_marker_unsupported_position`, WASM `storeFill` / `isFill`, and
|
|
59
|
+
the card wire's `fill` / `nestedFills`. (#1906, #1933)
|
|
60
|
+
- refactor(core)!: **the storage tag is `quillmark/document@0.116.0`.** A
|
|
61
|
+
field item carries no `fill` or `nested_fills`, and a row carrying either,
|
|
62
|
+
or any key its type does not name at the document, card, payload or item
|
|
63
|
+
level, fails the load, as does a nested comment spanning lines, which
|
|
64
|
+
`to_markdown` would have written as bare YAML. Every earlier tag still
|
|
65
|
+
reads. A row's bytes move on its next save, so content hashes
|
|
66
|
+
recompute and a 0.115 build refuses the row from then on.
|
|
67
|
+
- feat(core,wasm,python)!: **a card-wire payload item refuses an unknown
|
|
68
|
+
key.** A 0.115 integration still sending `{value, fill: true}` committed the
|
|
69
|
+
placeholder as an answer, and an item typo vanished; `insertCard` throws and
|
|
70
|
+
`insert_card` raises `ValueError` instead. (#1933)
|
|
71
|
+
- feat(core): **a `?` on a cell's type renders it `none` when unanswered**,
|
|
72
|
+
so a plate can tell "0 votes against" from "nobody entered a tally". `?` and
|
|
73
|
+
`default:` are exclusive (`quill::optional_default`), a namespace takes no
|
|
74
|
+
`?` (`quill::optional_namespace`), and the transform schema projects
|
|
75
|
+
`type: [t, "null"]`. (#1880)
|
|
76
|
+
|
|
77
|
+
### The blueprint and the seed
|
|
78
|
+
|
|
79
|
+
- feat(core)!: **seeding commits no `example:`.** An example documents a
|
|
80
|
+
cell's shape rather than answering it, and `body.example` is guide text
|
|
81
|
+
quills write as instructions. `seed_document`, `seed_main` and `seed_card`
|
|
82
|
+
commit the `$seed` overlay alone; every other body is empty and every other
|
|
83
|
+
field absent. (#1923)
|
|
84
|
+
- feat(core,wasm,python)!: **the blueprint answers nothing on the author's
|
|
85
|
+
behalf.** A defaultless cell is empty (`title: # string`) with its
|
|
86
|
+
`example:` on a `# e.g.` line above it; every body is empty, a
|
|
87
|
+
`body.example` riding a `# body e.g.` line that closes the payload and a
|
|
88
|
+
kind under `body.enabled: false` closing with `# no body`; a card's
|
|
89
|
+
description rides its `$kind` line; a `default: []` table is led by the
|
|
90
|
+
whole field commented out holding its row; and a matrix names its columns
|
|
91
|
+
in an `# e.g.` line, a column with no example showing its
|
|
92
|
+
`<type>[<format>]`. (#1874, #1942)
|
|
93
|
+
- feat(core,wasm,python)!: **`FORMAT_RULES` is `MARKDOWN_RULES`**, headed
|
|
94
|
+
`Quillmark Markdown rules:` (`markdownRules` / `markdown_rules`), and keeps
|
|
95
|
+
only what departs from plain Markdown and YAML. `blueprint_instruction` is
|
|
96
|
+
deleted in every binding: a prompt is the consumer's to write. (#1942)
|
|
97
|
+
- feat(core)!: **`to_markdown` writes a multi-line string as a `|` literal
|
|
98
|
+
block** (`|-` without a trailing newline), at every depth, where it wrote
|
|
99
|
+
one double-quoted line. A string a block would not hold verbatim stays
|
|
100
|
+
quoted. The value round-trips unchanged; the markdown bytes move. (#1883)
|
|
101
|
+
|
|
102
|
+
### The quill authoring contract
|
|
103
|
+
|
|
104
|
+
- feat(core)!: **a field's and a card kind's label is a top-level `title`**,
|
|
105
|
+
beside `description`, which `schema()` emits and the blueprint prints as
|
|
106
|
+
`# <title> — <description>`. `ui.title` fails the load, a `{field}` token
|
|
107
|
+
in a `title` is `quill::title_template`, and a `title` on an array's `items`
|
|
108
|
+
is `quill::title_on_items`. (#1922)
|
|
109
|
+
- refactor(core,wasm)!: **`body.unsupported` is removed**, with
|
|
110
|
+
`plate::unsupported_construct`, `Quill::unsupported_constructs` and the WASM
|
|
111
|
+
`QuillBlockConstruct`. Nothing verified a declaration against the plate and
|
|
112
|
+
no editor read it; a `Quill.yaml` still declaring it fails
|
|
113
|
+
`quill::invalid_body`.
|
|
114
|
+
- fix(core)!: **an unquoted `quill.version` reads as written**: `version:
|
|
115
|
+
1.10` is `1.10`, where it loaded as `1.1`, and `1.1e1` fails
|
|
116
|
+
`quill::invalid_version`.
|
|
117
|
+
- feat(core)!: **a `plaintext(inline)` value ending in a newline names `|-`
|
|
118
|
+
as the fix.** `validation::not_inline`, `edit::field_not_inline` and
|
|
119
|
+
`conform::field_not_inline` carry `trailingNewline: true`, and
|
|
120
|
+
`ValidationError::NotInline` / `EditError::FieldNotInline` gain
|
|
121
|
+
`trailing_newline`. (#1918)
|
|
122
|
+
- fix(core): `quill::invalid_name` suggests a name the loader accepts.
|
|
123
|
+
|
|
124
|
+
### The render date
|
|
125
|
+
|
|
126
|
+
- feat(core)!: **the host supplies the render date; a `date` field may hold
|
|
127
|
+
`today`.** The engine reads no clock. `Quillmark::open` / `render`,
|
|
128
|
+
`Quill::compile_data` / `compile_checked` / `resolve`,
|
|
129
|
+
`QuillConfig::compile_data` / `compile_checked`, `TypedReader::resolve` and
|
|
130
|
+
`Backend::open` take a required `today: CalendarDate`; a plate's
|
|
131
|
+
`datetime.today()` returns it whatever its `offset:`. The CLI (`--today`)
|
|
132
|
+
and the WASM and Python bindings default to the local date. A `date` value
|
|
133
|
+
is `YYYY-MM-DD` or `today`, and the blueprint annotates `date<YYYY-MM-DD |
|
|
134
|
+
today>`. `CalendarDate`'s parse fails with `ParseDateError`, which tells a
|
|
135
|
+
day the calendar lacks from a string that is no date. (#1900, #1930)
|
|
136
|
+
- feat(fixtures)!: `usaf_memo`'s blank `date` renders a fill-in field where it
|
|
137
|
+
rendered today's date.
|
|
138
|
+
|
|
139
|
+
### Plates and the Typst backend
|
|
140
|
+
|
|
141
|
+
- feat(typst)!: **a dictionary carries the `$ink` twin its fields print
|
|
142
|
+
through.** A scalar printed off `data` kept its click target only as a
|
|
143
|
+
direct read in the plate. `data`, each card, and each typed dictionary or
|
|
144
|
+
table row carry `$ink` and `$path` wherever a field has ink: compute with the
|
|
145
|
+
dictionary, print its ink (`ink(row).org`). A date's ink prints its default
|
|
146
|
+
display, and `display(row, "due", "[year]")` formats one by dictionary and
|
|
147
|
+
key. The keys are visible to `keys()`, `pairs()`, `len()`, `==` and a
|
|
148
|
+
spread. (#1868)
|
|
149
|
+
- feat(typst)!: **a plate imports the quill's `.typ` files by Typst's path
|
|
150
|
+
rules.** The plate loads at its `plate_file` path and every `.typ` file
|
|
151
|
+
outside `packages/` beside it, so a bare path resolves from the importing
|
|
152
|
+
file and a `/`-rooted one from the quill root. Plate diagnostics name the
|
|
153
|
+
declared `plate_file`, where they named `main.typ`, and a vendored package's
|
|
154
|
+
file under its spec (`@local/p:0.1.0/lib.typ`), apart from a quill module
|
|
155
|
+
of the same path. (#1869)
|
|
156
|
+
- feat(typst): **a `typst:` key the backend does not read warns
|
|
157
|
+
`typst::unknown_key`**, `typst.packages` included; a package is vendored
|
|
158
|
+
under `packages/`, never fetched. A package manifest without `version` or
|
|
159
|
+
`entrypoint` is skipped at load.
|
|
160
|
+
- refactor(typst): the `when` / `value-or` plate helpers are dropped; a plate
|
|
161
|
+
reads an optional cell with `!= none`.
|
|
162
|
+
- fix(typst): a negative scalar's ink lexes, and region queries stay linear.
|
|
163
|
+
|
|
164
|
+
### Rendering, the CLI and the bindings
|
|
165
|
+
|
|
166
|
+
- feat(core)!: **one raster ceiling, 16384 px a side**, `MAX_RASTER_SIDE`,
|
|
167
|
+
owned by core, where core refused past 16384² px of area and the WASM
|
|
168
|
+
painter clamped by side on its own. A PNG export long on one axis that fit
|
|
169
|
+
by area now fails `backend::invalid_raster_scale`.
|
|
170
|
+
- feat(wasm)!: **`paint(ctx, page, scale)` takes one scale and returns
|
|
171
|
+
nothing**; `PaintOptions` and `PaintResult` are deleted, and a missing or
|
|
172
|
+
non-finite scale throws `backend::invalid_raster_scale`.
|
|
173
|
+
- feat(cli): **`quillmark workspace` exports a plate for Typst's own
|
|
174
|
+
tooling**: the generated helper for one document, every vendored package
|
|
175
|
+
and the backend's fonts, with the `typst watch` command over them. A rerun
|
|
176
|
+
replaces the `packages/` and `fonts/` an earlier export wrote. The facade
|
|
177
|
+
re-exports it as `quillmark::typst_workspace`. (#1869)
|
|
178
|
+
- feat(cli): **`quillmark check` prints a document's every diagnostic**, and
|
|
179
|
+
`--strict` exits `1` on a warning, for a CI gate over a repository of
|
|
180
|
+
documents; `render` prints the warnings for what the page leaves out.
|
|
181
|
+
(#1872)
|
|
182
|
+
- fix(cli)!: `render` takes its format from an `-o` extension naming one, and
|
|
183
|
+
a `-f` the extension contradicts exits `1` before writing.
|
|
184
|
+
- feat(core,wasm,python): `Quill.emptyDocument()` / `Quill.empty_document()`
|
|
185
|
+
reach the bindings, and WASM `isInline` / `isPlain` export the content
|
|
186
|
+
predicates. (#1919)
|
|
187
|
+
|
|
188
|
+
### Fixes
|
|
189
|
+
|
|
190
|
+
- fix(core): the YAML prescan keeps comments, tags and anchors with the value
|
|
191
|
+
they sit on across continuation lines, dash lines, block scalars, multi-line
|
|
192
|
+
flow collections, a value below its key, nested keys with an inner `:` or a
|
|
193
|
+
leading `-`/`?`, and a removed item's trailing comment; a `$seed` overlay edit keeps the comment on
|
|
194
|
+
its `$seed` line.
|
|
195
|
+
- fix(core): `to_markdown` projects a content object at every depth of a
|
|
196
|
+
field's value, and a container under a sequence item's first key
|
|
197
|
+
round-trips.
|
|
198
|
+
- fix(core): a folded island past `isl-{u64::MAX}` mints a fresh id, and a
|
|
199
|
+
folded card's `$ext` stays out of the body.
|
|
200
|
+
- fix(core): the unknown-key walk stays linear in a mapping's keys.
|
|
201
|
+
- fix(core): a `$seed.<kind>.$body` for a kind under `body.enabled: false`
|
|
202
|
+
warns `validation::seed_unknown_field`, where the seed dropped it silently.
|
|
203
|
+
- fix(cli): `validate -v` reports a backend load warning once, where each of
|
|
204
|
+
the three canonical renders repeated it.
|
|
205
|
+
- fix(quillmark): the facade re-exports `ParseDateError`.
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
## v0.115.0 - 2026-09-22
|
|
209
|
+
|
|
210
|
+
Upgrade path: [0.114 → 0.115](docs/migrations/0.114-to-0.115.md).
|
|
211
|
+
|
|
212
|
+
### The quill authoring contract
|
|
213
|
+
|
|
214
|
+
- feat(core)!: **a `matrix` member mapping takes the ordinary ladder.** Two
|
|
215
|
+
rules decided what a stored member spelling meant — a bare scalar is the tick,
|
|
216
|
+
and a mapping naming no `held` is held — and the second retires. A member is
|
|
217
|
+
an `object` of a synthesized `held: {type: boolean, default: false}` beside
|
|
218
|
+
its columns, so a mapping is the member object already and the tick it names
|
|
219
|
+
no key for resolves as every other absent cell does, to its `default:`. The
|
|
220
|
+
old rule overrode that ladder for one key in one type, and bought a spelling
|
|
221
|
+
convenience for it: two mappings one key apart meant opposite ticks, the
|
|
222
|
+
shorter one ticked, and any producer building the member object without
|
|
223
|
+
conforming — an editor writing a column before the tick, a serde-built
|
|
224
|
+
payload, `{}` — set a tick it never wrote. `{ detail: X }` is now unheld with
|
|
225
|
+
its detail retained, which is what `{ held: false, detail: X }` already meant;
|
|
226
|
+
the bare scalar and every mapping naming `held` are unchanged. What remains is
|
|
227
|
+
exactly `conform_variant`'s bare-scalar rule, so `SCHEMAS.md` §Matrix lists
|
|
228
|
+
three things as the type's own rather than four. The flip is silent — the
|
|
229
|
+
value is well-typed under both readings, and a defaultless column's
|
|
230
|
+
`validation::must_fill` leaves with the tick it was gated on — so the upgrade
|
|
231
|
+
guide leads with it. (#1860)
|
|
232
|
+
- feat(core): **a typed dictionary's property opens a variant world.**
|
|
233
|
+
`variants:` was card-level, so a conditional block had to sit at the top of a
|
|
234
|
+
card even where the fields it governs belong to a dictionary. The rule was
|
|
235
|
+
never about depth: what stays one level deep is the gap between a field's
|
|
236
|
+
*declared* shape and its *live* one, and a dictionary's own shape is the
|
|
237
|
+
schema's — so a world opened there deepens the address and leaves the gap
|
|
238
|
+
alone. `header.classification` reads, binds, branches and strands exactly as
|
|
239
|
+
`classification` does, one step down. Every other position would deepen the
|
|
240
|
+
gap and still refuses one: an array element, whose liveness would vary per
|
|
241
|
+
index against a form bound once; a matrix column, inside a grid the page
|
|
242
|
+
prints in full; and another variant's cell, which would make the plate's one
|
|
243
|
+
branch a tree. `quill::variant_placement` now names the position carrying the
|
|
244
|
+
ban rather than restating the old rule, and the ban is **sticky** — an object
|
|
245
|
+
inside any of them inherits it instead of laundering a world into a position
|
|
246
|
+
that cannot hold one. The surfaces that dispatch on the container before
|
|
247
|
+
walking the type needed nothing (coercion, validation, the render floor,
|
|
248
|
+
conditional `must_fill`, the transform schema, `acroform` binding); the three
|
|
249
|
+
that scanned card fields now descend, so `validation::out_of_variant` finds a
|
|
250
|
+
strand at `main.header.classification.controlled_by`, the blueprint seats a
|
|
251
|
+
world's cells — live or commented — at the dictionary's indent, and the seed
|
|
252
|
+
resolves a nested discriminant before walking its field set.
|
|
253
|
+
- feat(core)!: **`ui.layout: table` contracts its columns.** The key was checked
|
|
254
|
+
for its outer shape alone — `type: array` whose `items` is an `object` — so a
|
|
255
|
+
row holding a container loaded clean and no editor could draw it: the author
|
|
256
|
+
wrote a key, the loader took it, and a surface ignored it with no signal at
|
|
257
|
+
either end. A column that is not a leaf is now
|
|
258
|
+
`quill::table_column_not_flat`, naming the column (`appendices[].entries`)
|
|
259
|
+
rather than the field. This splits the key along a seam it always had: a
|
|
260
|
+
**contract** that every column is a leaf, settled at load, and a **request**
|
|
261
|
+
to draw a grid, which an editor still declines for a width or a cell renderer
|
|
262
|
+
it does not have. Shape leaves the decline list, since shape was never a
|
|
263
|
+
consumer's judgement to make. The boundary is containment, not height — prose
|
|
264
|
+
is a column whatever its `inline`, because how tall a cell renders is the
|
|
265
|
+
editor's call and what it contains is not. `FieldType::is_leaf` is the one
|
|
266
|
+
definition, an exhaustive match a new type cannot skip, and `acroform`'s
|
|
267
|
+
`is_scalar_or_prose` is gone in favor of it: the same question had two
|
|
268
|
+
answers in two crates. Part of #1859.
|
|
269
|
+
- feat(core,wasm,python): **the empty document has a constructor.**
|
|
270
|
+
`Quill::empty_document()` is `Document::new` under the quill's own reference,
|
|
271
|
+
so the three canonical documents are three constructors beside
|
|
272
|
+
`QuillConfig::blueprint()` and `Quill::seed_document()`, and no caller spells
|
|
273
|
+
`name@version` to reach the one a plate is bound to render. `quiver_test.rs`
|
|
274
|
+
reads it rather than formatting the two-line markdown itself. The bindings
|
|
275
|
+
carry it as `Quill.emptyDocument()` / `Quill.empty_document()`.
|
|
276
|
+
- feat(cli)!: **`quillmark validate` compiles the plate.** It renders the empty
|
|
277
|
+
document, the blueprint and the seed through the quill's backend at the
|
|
278
|
+
backend's first declared format, and reports each failure as a
|
|
279
|
+
`cli::canonical_document_failed` error naming which of the three it is,
|
|
280
|
+
followed by the backend's own diagnostics. The contract that a plate renders
|
|
281
|
+
the empty document was checked for the fixtures quiver alone, and `render`
|
|
282
|
+
with no input file renders the seed, the fullest of the three: a plate that
|
|
283
|
+
reads a blank badly passed both. A quill whose plate does not compile now
|
|
284
|
+
exits `1` where it exited `0`, and a backend that does not resolve is
|
|
285
|
+
`cli::backend_unresolved` followed by the engine's own diagnostic. A render's
|
|
286
|
+
warnings join the `-v` list, and `validate --no-render` is the configuration
|
|
287
|
+
read on its own. Closes #1844.
|
|
288
|
+
|
|
289
|
+
### The content model
|
|
290
|
+
|
|
291
|
+
- refactor(content)!: **an island stores its value, not its provenance.** `Loss`
|
|
292
|
+
had one minting rule — a table cell dropping an inline image's url — and no
|
|
293
|
+
reader: export dispatches on an island's `type`, the Typst emitter and the
|
|
294
|
+
acroform binder never looked, and an `islandOps` `set` stored whatever class
|
|
295
|
+
the op named. Two cells written `| a cat |` and `|  |` mint
|
|
296
|
+
equal `props` and differ only in the markdown behind them, which the
|
|
297
|
+
projection has no syntax to carry back out, so the class survived neither a
|
|
298
|
+
`to_markdown` → `from_markdown` hop nor re-derivation from the row holding it
|
|
299
|
+
— which is why the content fixed point had to be stated modulo it. An
|
|
300
|
+
island's canonical form is `{id, type, props}`, the fixed point is exact, and
|
|
301
|
+
the closed vocabularies cover the four discriminators a projection dispatches
|
|
302
|
+
on. `Loss`, `Island::loss`, `Island::with_loss`, `IslandType::default_loss`,
|
|
303
|
+
`ContentIsland.loss` and `ContentLossClass` are deleted; a stored row spelling
|
|
304
|
+
`loss` opens with the key ignored and absent from what comes back, and a
|
|
305
|
+
`loss` on an authored island or an `islandOps` entry is ignored rather than
|
|
306
|
+
refused. A host that surfaced the class surfaces the drop at the import
|
|
307
|
+
instead, where the markdown it is about is still in hand. Closes #1757.
|
|
308
|
+
- refactor(content,core,wasm,python)!: **a block island's line is a `para`, and
|
|
309
|
+
`LineKind::Island` is gone.** The kind was a second copy of a fact the island
|
|
310
|
+
beside it already carried: markdown writes a `table` as a block and an `image`
|
|
311
|
+
inline, so `IslandType::block_only` settled which line a lone slot sat on and
|
|
312
|
+
`normalize` wrote that answer back over whatever was stored. Every read site
|
|
313
|
+
now asks the island — export already dispatched on the type, the Typst emitter
|
|
314
|
+
already lowered `Island` as `Para`, and the one fact the kind alone still
|
|
315
|
+
answered, "this block renders one line", moves to the new public
|
|
316
|
+
`Content::block_island_at`, which `normalize` reads beside
|
|
317
|
+
`takes_continuations` when it clears a stranded `continues`. Landing a block
|
|
318
|
+
island is two channels rather than three: the delta opens the line, the island
|
|
319
|
+
op fills it, and the `setKind` goes. The TS `ContentLineKind` loses its
|
|
320
|
+
`{ kind: "island" }` arm, so a host branching on it typechecks red; a `setKind`
|
|
321
|
+
still naming `island` lands as the `para` it would have settled to.
|
|
322
|
+
- refactor(core)!: **the storage tag is `quillmark/document@0.115.0`.** The two
|
|
323
|
+
lines above move canonical bytes — the `loss` key for every document holding
|
|
324
|
+
an island, the line kind for every document holding a table — so content
|
|
325
|
+
hashes recompute once on those rows. `DocumentV0_112_0` freezes with a raw
|
|
326
|
+
`body` and the new `DocumentV0_115_0` carries the `CanonicalContent`; the hop
|
|
327
|
+
between them is that body's decode, and `V0_93_0 → V0_112_0` becomes a retag
|
|
328
|
+
now that both trees spell `body` raw. Stored rows need no migration: the
|
|
329
|
+
content decoder reads `island` as `para` wherever it meets it, permanently,
|
|
330
|
+
since the same spelling rests untagged inside every `richtext` field and
|
|
331
|
+
`$seed` overlay, where no hop reaches it. Closes #1647.
|
|
332
|
+
|
|
333
|
+
### Schema, validation and the resolved view
|
|
334
|
+
|
|
335
|
+
- refactor(core)!: **a schema is loaded, not deserialized.** `QuillConfig`,
|
|
336
|
+
`CardSchema` and `FieldSchema` serialize and no longer implement
|
|
337
|
+
`Deserialize`, and `FieldSchema::from_quill_value` is crate-internal. A field
|
|
338
|
+
arrives in two steps — the parse, which folds the sibling keys (`inline:`,
|
|
339
|
+
`values:`, `members:`, `max:`) into the type payload, and the shape walk,
|
|
340
|
+
which rejects an `object` without `properties`, an `array` without `items`,
|
|
341
|
+
an empty `properties` map, and the placements — and only `parse_fields` ran
|
|
342
|
+
both, so a config assembled through serde carried states no `Quill.yaml` can
|
|
343
|
+
reach, absorbed downstream without a diagnostic. Gating the serde path
|
|
344
|
+
instead would have duplicated the walk and collapsed its diagnostic codes
|
|
345
|
+
into `quill::field_parse_error` at that boundary. The way to a config is
|
|
346
|
+
`QuillConfig::from_yaml_with_warnings`, or a `Quill` read off a file tree;
|
|
347
|
+
serialization is untouched, so `schema()`, `schema_yaml()` and every emission
|
|
348
|
+
read the same loaded form they did. Closes #1836.
|
|
349
|
+
- refactor(core,wasm)!: **a matrix roster is one flat mapping; group headings
|
|
350
|
+
are gone.** A block's `group:` was declared in the schema and absent from the
|
|
351
|
+
value, the address and the vocabulary — the blueprint emits
|
|
352
|
+
`matrix<a | b | c>` off one flat key space, so the partition reached the
|
|
353
|
+
visual editor and nowhere else, buying one shared `properties` map for a
|
|
354
|
+
schema concept, its load rules and a reserved key. `members:` is now
|
|
355
|
+
`{id: Title}` directly, `MatrixGroup`, `MATRIX_GROUP_KEY` and the TypeScript
|
|
356
|
+
`QuillMatrixGroup` are deleted, and `FieldType::Matrix` carries an
|
|
357
|
+
`IndexMap<String, String>` read through the new `FieldType::matrix_roster`,
|
|
358
|
+
which replaces `matrix_members`. The projection stops writing `group` onto
|
|
359
|
+
every member, which returns `group` to the column names a quill may declare,
|
|
360
|
+
and `quill::duplicate_matrix_member` retires: the roster is a mapping, so it
|
|
361
|
+
has one slot per id exactly as the stored value does and a duplicate is
|
|
362
|
+
unspellable on both sides. Closes #1851.
|
|
363
|
+
|
|
364
|
+
### The blueprint
|
|
365
|
+
|
|
366
|
+
- feat(core)!: **a dormant variant world is its own cells, commented out.** A
|
|
367
|
+
variant-bearing enum's blueprint named every world it could not show, one
|
|
368
|
+
`# when CUI: controlled_by, poc, category` line each — the one place on the
|
|
369
|
+
surface where a cell was named rather than shown, dropping the position,
|
|
370
|
+
obligation, type, description and example a cell carries. Every world now sits
|
|
371
|
+
under a `# when <MEMBER>:` header in declaration order: the selected world's
|
|
372
|
+
cells live, every other world's the same cells with a `# ` in front, at the
|
|
373
|
+
slot they would occupy live. A reader activates a world by setting the
|
|
374
|
+
discriminant and deleting `# `. One builder and one emitter serve both, so a
|
|
375
|
+
dormant line is byte-for-byte the live line, and the block round-trips through
|
|
376
|
+
`Document::parse` as the comments it is, reaching neither the validator nor
|
|
377
|
+
the render floor. Closes #1846.
|
|
378
|
+
|
|
379
|
+
|
|
3
380
|
## v0.114.0 - 2026-09-18
|
|
4
381
|
|
|
5
382
|
### Schema, validation and the resolved view
|
package/README.md
CHANGED
|
@@ -150,8 +150,7 @@ document was built. Parse warnings and the `conform::*` warnings both ride
|
|
|
150
150
|
other way (`fromStored`, a stored row), returning the `conform::*` `Diagnostic[]`
|
|
151
151
|
(`[]` when everything rested). It is idempotent and a byte no-op on an
|
|
152
152
|
already-canonical document, YAML comments included, so calling it on every load
|
|
153
|
-
is safe. A
|
|
154
|
-
value the strict write refuses stays as authored under a warning. Both throw
|
|
153
|
+
is safe. A value the strict write refuses stays as authored under a warning. Both throw
|
|
155
154
|
when the document declares a `$quill` this quill does not answer to, before any
|
|
156
155
|
mutation.
|
|
157
156
|
|
|
@@ -175,9 +174,9 @@ const doc = Document.storageVersionOf(content)
|
|
|
175
174
|
: Document.fromMarkdown(content);
|
|
176
175
|
```
|
|
177
176
|
|
|
178
|
-
The `schema` value (`quillmark/document@0.
|
|
177
|
+
The `schema` value (`quillmark/document@0.116.0`) is the **model version**,
|
|
179
178
|
not the running crate version. It is a hand-set constant, bumped only when
|
|
180
|
-
the `Document` model itself changes, so every `0.
|
|
179
|
+
the `Document` model itself changes, so every `0.116.x` patch release reads
|
|
181
180
|
and writes that same value.
|
|
182
181
|
|
|
183
182
|
- **Upgrading is safe.** A newer build reads documents an older build's
|
|
@@ -187,8 +186,10 @@ and writes that same value.
|
|
|
187
186
|
canonical on-disk format keeps loading across crate upgrades: there is no
|
|
188
187
|
need to pin old wasm to read old data. The exception is a row a host
|
|
189
188
|
authored a content construct of its own into — a line `kind`, container,
|
|
190
|
-
mark `type
|
|
191
|
-
|
|
189
|
+
mark `type` or island `type` outside the vocabulary. Those are refused from
|
|
190
|
+
0.113 on; see that release's migration guide. An island `loss` is not one of
|
|
191
|
+
them: 0.115 drops the key, so a row spelling one opens whatever the class and
|
|
192
|
+
comes back without it.
|
|
192
193
|
- **Downgrading is not.** `fromStored` rejects an *unknown* (i.e. newer)
|
|
193
194
|
`schema` version rather than guessing at a format it predates. Don't feed
|
|
194
195
|
documents written by a newer build back into an older one.
|
|
@@ -218,8 +219,9 @@ To render a form editor, read field definitions from `quill.schema` (walk
|
|
|
218
219
|
values from the `Document` payload: there is no separate form-view projection.
|
|
219
220
|
`quill.validate(doc)` scores it without invoking the backend.
|
|
220
221
|
|
|
221
|
-
`quill.seedDocument()` returns a starter document
|
|
222
|
-
|
|
222
|
+
`quill.seedDocument()` returns a starter document: one card per kind, each body
|
|
223
|
+
empty, every field absent (no `example:` is committed);
|
|
224
|
+
`quill.seedMain()` and `quill.seedCard(kind)` seed one card. All
|
|
223
225
|
return the read `Card` shape of `doc.main` / `doc.cards`, which `doc.insertCard`
|
|
224
226
|
accepts directly:
|
|
225
227
|
|
|
@@ -246,7 +248,7 @@ card is an object literal: one `{ type: "field", key, value }` per field in
|
|
|
246
248
|
`{ card?, field? }`, absent `card` = main, absent `field` = body, and a bare
|
|
247
249
|
string is shorthand for `{ field }`. So `doc.storeField("qty", 3)` targets the
|
|
248
250
|
main card's `qty`, `doc.storeField({ card: 2, field: "qty" }, 3)` a composable
|
|
249
|
-
card's. Reads are total over the field axis (`getStored` → `undefined
|
|
251
|
+
card's. Reads are total over the field axis (`getStored` → `undefined` for
|
|
250
252
|
an absent field; only an out-of-range card throws); field writes throw on a body
|
|
251
253
|
address. `getStored` is the verbatim transport read, distinct from the interpreted
|
|
252
254
|
`quill.reader(doc).get`; `bodyMarkdown` is the body markdown read (a `CardAddr`; a field's
|
|
@@ -287,7 +289,7 @@ on `Document` itself (**store** = verbatim, **set** = typed):
|
|
|
287
289
|
`_reviseField` ABI the writer delegates to, hidden from the `.d.ts`.)
|
|
288
290
|
|
|
289
291
|
- **`store*`: the deliberate quill-free primitive.** `doc.storeField(addr, value)`
|
|
290
|
-
/ `doc.storeFields(cardAddr, {...})`
|
|
292
|
+
/ `doc.storeFields(cardAddr, {...})` validate only the field
|
|
291
293
|
name/depth/kind and store the value verbatim, no quill required. Reach for it
|
|
292
294
|
on purpose when you *want* the opaque store: quill-agnostic storage/migration
|
|
293
295
|
infra that has no bundle and must write regardless of a drifted schema;
|
|
@@ -352,43 +354,47 @@ tells you which pages to repaint (`dirty ∩ visible`). `update` is transactiona
|
|
|
352
354
|
on throw, every read keeps serving the last-good compile. Don't open a session
|
|
353
355
|
per export, and don't re-open per edit: `update` instead.
|
|
354
356
|
|
|
357
|
+
Both take the render date as an optional last argument (`YYYY-MM-DD`, default
|
|
358
|
+
the local date): what a `today` date field and a plate's `datetime.today()`
|
|
359
|
+
render as. A session keeps the date it opened with, so a preview left open past
|
|
360
|
+
midnight renders yesterday's until it is reopened.
|
|
361
|
+
|
|
355
362
|
A document that compiles to zero pages still produces a valid session
|
|
356
|
-
(`pageCount === 0`); `paint(ctx, 0)` and `pageSize(0)` then throw. Branch on
|
|
363
|
+
(`pageCount === 0`); `paint(ctx, 0, scale)` and `pageSize(0)` then throw. Branch on
|
|
357
364
|
`pageCount === 0` to render a "no pages to preview" UI rather than relying on
|
|
358
365
|
the throw.
|
|
359
366
|
|
|
360
367
|
Their `warnings` differ in reach. `engine.render` returns one list for the
|
|
361
|
-
whole pipeline: `doc.warnings` (parse
|
|
362
|
-
`
|
|
368
|
+
whole pipeline: `doc.warnings` (parse and `conform::*`), then every
|
|
369
|
+
`quill.validate(doc)` warning, then the compile's own. A session outlives
|
|
363
370
|
the document it opened from, so `session.render` and `session.warnings` carry
|
|
364
|
-
the compile
|
|
371
|
+
the compile's alone — read `doc.warnings` and `quill.validate(doc)` beside them.
|
|
365
372
|
|
|
366
373
|
### Canvas Preview
|
|
367
374
|
|
|
368
|
-
`session.paint(ctx, page,
|
|
375
|
+
`session.paint(ctx, page, scale)` rasterizes a page directly into a
|
|
369
376
|
`CanvasRenderingContext2D` (main thread) or
|
|
370
377
|
`OffscreenCanvasRenderingContext2D` (Worker), skipping PNG/SVG byte
|
|
371
378
|
round-trips.
|
|
372
379
|
|
|
373
|
-
The painter owns `canvas.width` /
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
380
|
+
`scale` is backing-store pixels per point. The painter owns `canvas.width` /
|
|
381
|
+
`canvas.height`, reducing `scale` where it must so neither exceeds 16384 px;
|
|
382
|
+
consumers own `canvas.style.*`. A canvas styled to fill its page box
|
|
383
|
+
needs nothing back from the paint:
|
|
377
384
|
|
|
378
385
|
```ts
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
canvas.style.width = `${result.layoutWidth}px`;
|
|
385
|
-
canvas.style.height = `${result.layoutHeight}px`;
|
|
386
|
+
canvas.style.width = "100%"; // the box sets the display size
|
|
387
|
+
const cssPxPerPt = canvas.clientWidth / session.pageSize(0).widthPt;
|
|
388
|
+
if (cssPxPerPt > 0) { // 0 while the canvas has no layout box
|
|
389
|
+
session.paint(canvas.getContext("2d")!, 0, cssPxPerPt * window.devicePixelRatio);
|
|
390
|
+
}
|
|
386
391
|
```
|
|
387
392
|
|
|
388
|
-
- `
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
393
|
+
- Fold `devicePixelRatio`, in-app zoom, and `visualViewport.scale` into `scale`.
|
|
394
|
+
- A canvas with no layout box (`display: none` on it or an ancestor, detached,
|
|
395
|
+
in a zero-width container) has `clientWidth` 0, and a 0 scale throws
|
|
396
|
+
`backend::invalid_raster_scale`: skip the paint, and paint when a
|
|
397
|
+
`ResizeObserver` reports a width.
|
|
392
398
|
- `paint` writes the whole backing store with `putImageData`, which ignores the
|
|
393
399
|
2D context transform, `globalAlpha`, and clip. Give each visible page its own
|
|
394
400
|
`<canvas>`: no compositing, sub-rect, or transform reaches through `paint`.
|
|
@@ -400,33 +406,28 @@ canvas.style.height = `${result.layoutHeight}px`;
|
|
|
400
406
|
cache them between committed `update`s only. After one, the count is
|
|
401
407
|
`ChangeSet.pageCount`, and every page in `ChangeSet.dirtyPages` needs its
|
|
402
408
|
`pageSize` re-read.
|
|
403
|
-
- In a Worker, pass an `OffscreenCanvasRenderingContext2D
|
|
404
|
-
|
|
405
|
-
is the host's
|
|
409
|
+
- In a Worker, pass an `OffscreenCanvasRenderingContext2D`. An
|
|
410
|
+
`OffscreenCanvas` has no layout box to measure, so the main thread posts the
|
|
411
|
+
scale. Loading the WASM module inside the Worker is the host's
|
|
412
|
+
responsibility.
|
|
406
413
|
- `paint` / `pageSize` throw on a page the compile does not have, a zero-page
|
|
407
414
|
compile included, naming the index and the `pageCount` that excludes it. That
|
|
408
415
|
throw is the whole contract: open the session and handle it.
|
|
409
416
|
|
|
410
417
|
### Schema model
|
|
411
418
|
|
|
412
|
-
A field
|
|
419
|
+
A field declares no `required` key: nothing is required.
|
|
413
420
|
|
|
414
|
-
|
|
415
|
-
that value under a type-only `# <type>` annotation and the render path
|
|
416
|
-
when the document omits the field. Without one,
|
|
417
|
-
|
|
421
|
+
**`default:`** — what an unanswered field renders. With one, `quill.blueprint`
|
|
422
|
+
renders that value under a type-only `# <type>` annotation and the render path
|
|
423
|
+
uses it when the document omits the field. Without one, the blueprint leaves the
|
|
424
|
+
cell empty and an absent field blank-fills.
|
|
418
425
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
`quill.validate(doc)` emits the non-fatal `validation::must_fill` warning while
|
|
423
|
-
the document leaves it unauthored — from either of two triggers, named by the
|
|
424
|
-
diagnostic's `trigger` arg: `marker` for a marker the document still carries,
|
|
425
|
-
`unauthored` for a cell the schema obliges and the document never filled.
|
|
426
|
-
Authoring the field's blank discharges the obligation; clearing the key does
|
|
427
|
-
not.
|
|
426
|
+
**`example:`** — the schema's illustration of the field's shape.
|
|
427
|
+
`quill.blueprint` shows it on a `# e.g.` line above the field; it never takes a
|
|
428
|
+
cell and never renders.
|
|
428
429
|
|
|
429
|
-
|
|
430
|
+
An unanswered field draws no diagnostic. Partial documents are accepted, and
|
|
430
431
|
`engine.render(quill, doc)` throws only for malformed input.
|
|
431
432
|
|
|
432
433
|
### Errors
|
|
@@ -514,7 +515,7 @@ in a browser that hasn't shipped it:
|
|
|
514
515
|
const session = await engine.open(quill, doc);
|
|
515
516
|
try {
|
|
516
517
|
for (let p = 0; p < session.pageCount; p++) {
|
|
517
|
-
session.paint(
|
|
518
|
+
session.paint(canvases[p].getContext("2d")!, p, scale);
|
|
518
519
|
}
|
|
519
520
|
} finally {
|
|
520
521
|
session.free();
|