0.120 → 0.121 — a matrix member is held by being present, a matrix reaches the plate as the members it holds, and an open matrix admits added items¶
The break that needs the most care is the one nothing reports: a matrix member
spelled as a mapping naming no held flips from unticked to ticked. The value
is well-typed under both readings, so no type checker, no load error and no
diagnostic fires — the page prints a ticked box, and that member's columns with
their answers, where it printed an unticked one. The same flip reaches a
Quill.yaml seed: or array default: that spells a member that way.
The loud break is wider: a member mapping storing held at all fails
validation as validation::held_stored, and the render with it. 0.120's strict
write stored every bare tick as {held: true} — writer.set on any matrix, and
quill.parse / conform and card seeding on a matrix with a content column — and
the editor spelled held on every member it touched, so most stored documents
holding a tick carry one. A Quill.yaml literal storing held fails to load:
quill::seed_held_stored in a seed:, quill::default_held_stored in a
default:. The section below says
how to find both.
Every plate reading a matrix breaks, loudly. A matrix reaches the plate as
the members its document holds, each its columns alone: no member carries
held or a roster title, and an unheld member is absent. A Typst plate
reading m.held or m.title fails to compile, and a member's tick is its own
address, so a claim on <field>.<member>.held fails the compile and an
acroform form.json binding on it fails to load. The plate
section has the rewrite.
| Break | Surface | Action |
|---|---|---|
A matrix member spelled { detail: X } — a mapping naming no held — is held, where it was unheld |
document YAML, Quill.yaml seed: and default:, rendered output |
Remove every such member meant to be unticked. Nothing reports this one: find it as below |
A matrix member mapping storing held fails validation as validation::held_stored, and the render with it |
document YAML, stored documents, diagnostics | Delete held: true; remove a member stored held: false, whose answers go with it |
A Quill.yaml seed: or default: storing held fails to load as quill::seed_held_stored / quill::default_held_stored |
Quill.yaml |
The same edit, in the literal |
| An unticked member keeps no answers in the document | editors | Hold a member's answers across an untick in the editor's own state, if a retick should restore them. Remove the member to untick it; clearing a held member's columns keeps it held |
The strict write rests a bare tick as true and an empty mapping as true, where it wrote {held: true} |
stored documents | None: the new rest is what the previous row asks for. Re-baseline a byte snapshot of a conformed document |
A matrix's blueprint # e.g. line spells its member without held: true, and an open matrix's shows a new_item added item |
blueprint() output |
Re-baseline anything asserting on blueprint bytes |
A matrix reaches the plate as the members it holds, each its columns: no held, no roster title, and an unheld member absent |
Typst plates | A plate reading m.held or m.title fails to compile: print through roster(data, "<field>") |
A member's tick is its own address, <field>.<member>; <field>.<member>.held is no address |
Typst field-region and form-field, acroform form.json |
A claim on .held fails the compile, and a binding on it fails to load as acroform::dangling_binding: drop the .held segment |
A bare tick the boolean coercion refuses is validation::type_mismatch at <field>.<member>, where it was <field>.<member>.held |
diagnostics | Match the new path |
resolve() reports a matrix as the members it holds, where it listed every member with held |
editors reading resolve() |
Read the roster from schema(), and a member's tick from its presence |
A dictionary declaring a matrix carries $path though none of its fields has ink |
Typst plates comparing, spreading or listing that dictionary's keys | Skip $path as you skip $ink |
The transform schema carries a matrix's roster as quillmark:roster, [id, title] pairs, where it carried quillmark:order, and a member's node holds its columns alone |
transform schema consumers; Rust QUILLMARK_ORDER_KEY is QUILLMARK_ROSTER_KEY |
Read the ids from the pairs |
FieldType::Matrix carries open: bool; FieldSchema::members, one object per id, is FieldSchema::member, one shared, beside a new added_item; namespace_props answers None for a matrix |
Rust | A FieldType::Matrix { roster } pattern takes ..; build a FieldSchema through FieldSchema::new; resolve a member through FieldSchema::matrix_member or matrix_member_at |
ValidationError::EnumViolation carries open, and ValidationError::HeldStored is new |
Rust | Spell open: false where you build the variant; an exhaustive match gains an arm |
A matrix member is held by being present¶
A document ticks a member by writing it. The rule has one carrier, the member's
presence, and the document never stores held:
qualifications:
flight_cc: true # held, unchanged
cyber_200: {} # was unheld; now held
dodin_ops: { detail: "2024" } # was unheld; now held
dco: { held: true, detail: X } # was held; now validation::held_stored
sq_cc_candidate: false # not held, unchanged
0.115 retired the reading that ticked a mapping naming no held, because it
overrode the ladder for one key: { detail: X } and { held: false, detail: X }
were two mappings one key apart meaning opposite ticks. Presence has no second
key to disagree with it. A stored held is refused whichever value it holds:
false under a present member contradicts the presence that holds it, and
reading it either way is a guess. A held: null is absent, as at every type.
What moves with it:
- An unticked member keeps nothing. Unticking removes the member, and its columns go with it. An editor that wants a retick to restore them keeps them itself.
- An empty mapping ticks.
{}is a member present with nothing answered. An editor clearing a held member's last column leaves it held; the strict write rests it astrue. - The blueprint teaches less. Its
# e.g.line no longer spells theheld: truea mapping needed to tick.
Two finds. The loud one: before upgrading, every stored held: under a matrix
member is the edit; after, quill.validate(doc) names each as
validation::held_stored at its path, and a quill storing one names it at load.
Delete held: true, and remove each member stored held: false.
The silent one is a mapping naming no held that was meant to be unticked,
which only a hand or a model writes, in a document or in a quill's seed: or
default:. Inline spellings narrow from:
A block mapping under a member id needs the roster: for each matrix field in your quills, search its member ids and read the lines under each hit.
A matrix reaches the plate as the members it holds¶
A matrix is a cell whose value is the set of held members, the way an array's value is its elements, so the plate carries what the document ticks and no more:
where 0.120 carried every member with held and the roster's title. The
vocabulary to print, held or not, comes from roster(dict, key), a helper that
reads the roster the backend generates, so a plate keeps no copy of it:
// 0.120
#for (id, m) in data.qualifications {
[#(if m.held { "[x]" } else { "[ ]" }) #m.title — #m.detail]
}
// 0.121
#import "@local/quillmark-helper:0.1.0": data, ink, roster
#for row in roster(data, "qualifications") {
[#(if row.held { "[x]" } else { "[ ]" }) #row.title]
if row.held [ — #ink(row.value).detail]
}
A row is (id, title, held, value, path): value is the member's columns,
none where unheld, and path its address. An unheld member has no columns to
print, where 0.120 printed their blanks. Pass the dictionary that declares the
matrix: roster(card, "qualifications") on a card, roster(row, "quals") in a
table row.
A member's tick is its own address. A region or widget on
qualifications.flight_cc.held moves to qualifications.flight_cc, and on
acroform that address binds a checkbox its presence checks; columns keep their
addresses. field-region(row.path)[…] claims a whole row, so an unticked box is
clickable in an editor preview too.
Being a cell, a matrix now takes a default:, the set a document that leaves
the field out holds, and type: matrix?, which renders an unanswered one
none. Both failed to load in 0.120.
An open matrix admits items a document adds¶
open: true on a matrix lets a document add items beside the roster, each keyed
by a snake_case id and carrying a title:
Nothing changes for a closed matrix. On an open one:
- The plate receives the held roster members in roster order, then each added
item sorted by id,
{title, …columns}, androsterreturns them in that order. An added item'stitleis a cell with an address, soink(row.value).titleprints it click-to-edit. - A key outside the roster with no
title, or one no snake_case id spells, isvalidation::enum_violationwithopen: true. - The transform schema carries the added item as the matrix node's
additionalProperties, andschema()emitsopen: true(WASMQuillFieldSchema.open). - An acroform quill declaring one fails to load as
quill::open_matrix_unsupported.