0.82.0 → 0.83.0 — $-prefixed plate JSON wire format¶
0.83.0 retires the legacy uppercase reserved keys from the plate JSON
that backends and Typst plates consume. The same conceptual data is now
exposed under $-prefixed metadata keys, alongside flat user payload
fields. This is a hard cutover: there is no compatibility shim — plates
that read the old keys (QUILL, BODY, CARDS, CARD) will fail to
compile.
Wire-format change¶
| Key (0.82) | Key (0.83) | Access in Typst plates |
|---|---|---|
data.QUILL |
data.$quill |
data.at("$quill") |
data.BODY |
data.$body |
data.at("$body") |
data.CARDS |
data.$cards |
data.at("$cards") |
card.CARD |
card.$kind |
card.at("$kind") |
card.BODY |
card.$body |
card.at("$body") |
User payload fields stay flat at the root of the plate JSON, alongside
the $ metadata. They cannot collide with the $ keys because field
names match [a-z_][a-z0-9_]* — the $ sigil is excluded from valid
identifiers.
- #mainmatter[
- #data.BODY
- ]
+ #mainmatter[
+ #data.at("$body")
+ ]
- #for card in data.CARDS {
- if card.CARD == "indorsement" [
- #card.BODY
- ]
- }
+ #for card in data.at("$cards") {
+ if card.at("$kind") == "indorsement" [
+ #card.at("$body")
+ ]
+ }
Why
.at("$...")? Typst identifiers do not allow the$character, sodata.$bodyis a parse error. Dictionary access via.at("$body")is the supported form. Lowercase user fields (e.g.data.title,card.from) keep their familiar.fieldaccess.
Removed APIs and validation¶
quillmark_core::document::edit::RESERVED_NAMES,is_reserved_name, andEditError::ReservedNameare removed. The editor surface (Card::set_field,remove_field,set_fill) now returnsEditError::InvalidFieldNamefor any name that does not match[a-z_][a-z0-9_]*— including the legacy uppercase sentinels and any$-prefixed name. WASM and Python bindings track the variant change inedit_error_to_jsandconvert_edit_error.- The markdown parser no longer rejects uppercase YAML keys like
BODY:. They are simply parsed as user payload fields with invalid names; the editor surface refuses to mutate them, butfrom_markdownno longer errors on parse. The new wire format makes this safe — the user field cannot shadow$bodyregardless. QuillConfig::schema()no longer prepends syntheticQUILL/CARDdiscriminator fields tomain.fields/card_kinds.<name>.fields. The schema describes only the user-fillable fields. To construct a document:- The root
$quillis${meta.name}@${meta.version}(read fromquill.metadata). - Each card's
$kindis the key under which it is declared incard_kinds. - The
quillmark::compile_dataJSON output uses the new shape end-to- end. Any host code that inspecteddata["QUILL"],data["BODY"],data["CARDS"], or a card'sCARD/BODYkeys must switch to the$-prefixed equivalents.
Updating a quill bundle¶
- Edit every
plate.typ(or other backend template) in the bundle. Replacedata.BODY→data.at("$body"),data.CARDS→data.at("$cards"),card.CARD→card.at("$kind"), andcard.BODY→card.at("$body"). - Regenerate any stored
schema.yamlgolden files — the discriminator entries (QUILL,CARD) no longer appear. - If host code reads the plate JSON directly (form builders, custom
exporters), switch every reserved-name lookup to the
$-prefixed equivalent.
Editor-surface migration¶
Code that catches EditError::ReservedName should fold that branch
into EditError::InvalidFieldName:
match doc.set_field(name, value) {
- Err(EditError::ReservedName(n)) => handle_invalid(n),
Err(EditError::InvalidFieldName(n)) => handle_invalid(n),
...
}
In WASM / Python bindings the thrown / raised error string changes from
[EditError::ReservedName] ... to [EditError::InvalidFieldName] ...
for the legacy uppercase names. Any tests that asserted on the
ReservedName substring need updating.
New: $ext system metadata¶
0.83.0 adds $ext as a fourth $-prefixed system-metadata key
in the closed set, alongside $quill, $kind, and $id. Its value is
an opaque YAML mapping reserved for out-of-band extension data —
per-card UI editor state (display renames, collapse flags), agent
annotations, or anything bespoke to a consumer that should not reach
the rendered output. The map round-trips through Markdown and the
storage DTO, and is stripped from Document::to_plate_json() so
backends never see it.
~~~card-yaml
$quill: my_quill@0.1.0
$kind: main
$ext:
presentation:
title: "Greeting Card"
agent:
last_visited: 2025-03-14
title: Hi
~~~
Rules at a glance — see markdown-spec.md §3.3 for the full specification:
- Value must be a YAML mapping; scalars and sequences are parse errors.
!fillon$extis rejected (the existing$-prefix guard covers it).- An empty
$ext: {}is preserved as a distinct, explicit declaration and emits as$ext: {}. - Unknown sub-keys inside
$extare not checked — the map is opaque. Consumers namespace inside it ($ext.presentation,$ext.agent, …) to avoid collisions when more than one tool carries state on the same card.
API surface¶
| Surface | Read | Write |
|---|---|---|
| Rust | Card::ext() → Option<&Map<String, Value>> |
Payload::set_ext(map), take_ext() |
| WASM | card.ext (Record<string, unknown> or undefined) |
round-trip via Document.toJson() / fromJson() |
| Python | card["ext"] (dict or None) |
round-trip via Document.to_json() / from_json() |
The $ext map is stripped from the binding-level payloadItems list,
just like the other $ entries. There is no in-place mutator on the
WASM or Python Card object in v1; bespoke consumers mutate the DTO
JSON and re-instantiate via fromJson/from_json.
Storage DTO¶
PayloadItemV0_82_0 gains a new variant under the existing
quillmark/document@0.82.0 schema tag:
This is an additive change to the wire format — documents without
$ext are byte-identical to the 0.82.0 form. Because 0.82.0 is
yanked, no external consumer reads the old shape, so the DTO is
extended in place rather than versioned to @0.83.0.
Migrating legacy PRESENTATION_METADATA-style state¶
Some pre-0.82 UI consumers stashed editor-only state under an
all-uppercase frontmatter key like PRESENTATION_METADATA. The
convention worked because pre-0.82 field names were also expected to
be lowercase, so an uppercase key did not collide with user data and
renderers ignored it. Under card-yaml that uppercase key parses as an
ordinary user field and now reaches the plate JSON as flat root-level
data — leaking editor state into renders.
Lift it into $ext:
~~~card-yaml
$kind: indorsement
- PRESENTATION_METADATA:
- title: "Cmdr's response"
+ $ext:
+ presentation:
+ title: "Cmdr's response"
from: ORG/SYMBOL
~~~
The $ext.presentation sub-namespace is a convention, not a spec
requirement — pick a sub-key that identifies your consumer.