card-yaml Blocks¶
Quillmark documents carry structured metadata in card-yaml blocks: explicitly delimited blocks that isolate YAML data from the surrounding Markdown prose. The first such block (the root block) names the quill that renders the document; later blocks are composable cards.
~~~
$quill: my_format
$kind: main
title: My Document
author: Jane Doe
date: 2025-01-15
tags: ["important", "draft"]
~~~
# Document content starts here
The rules a writer meets first:
- Open and close the block with a line of exactly
~~~, at column zero. Not---front matter, and not a backtick fence. $quill:names the quill that renders the document. The$is part of the key.- Every other line is
field: value. Quote a value that contains:, as insubject: "Re: budget". - A code block in the prose takes a backtick fence: a
~~~at column zero after a blank line opens a card-yaml block.
Prettier breaks cards
Prettier reprints every ~~~ fence as a backtick fence, which opens no
card, so a formatted document fails to parse. Keep Quillmark documents
out of prettier with a .prettierignore line naming their directory, such
as quills/ in a quiver.
Block Structure¶
A card-yaml block has three parts, in order:
- Opening fence: a bare
~~~(three tildes). No leading indentation. Any info string is accepted on input —~~~card-yamland~~~yamlamong them — and re-emits as a bare~~~. - YAML payload: a standard YAML mapping. The reserved keys
$quill,$kind,$ext, and$seedcarry system metadata (see below); every other key is a user-defined data field. The payload is one YAML value: text after a{...}mapping written as the whole payload, or after a...line, fails the parse asparse::yaml_error_with_locationat that text. Comments may follow it. A mapping names each key once: two keys of one text, quoted or not (1and"1"), fail the parse at the second. - Closing fence: a tilde run at least as long as the opener. The canonical opener and closer are both
~~~; a longer opener (e.g.~~~~) requires an equally long closer.
The unstructured Markdown body begins immediately after the closing ~~~
fence and runs to the next opening fence or the end of the document.
A blank line is required immediately above every ~~~ opener,
except when the opener is the very first line of the document. A
~~~ line without a blank line above it is not an opener: it is
treated as an ordinary code block.
Because every column-zero ~~~ block is a card-yaml block, writing a literal
fenced code block in prose requires the escape hatch: use a backtick fence.
Tildes offer no escape. Adding more does not help — a ~~~~ block is still a
card (its closer must just be at least as long) — and neither does a language
info string: ~~~rust opens a card whose payload is your Rust. Code that reads
as a YAML string or list fails at that fence's line and names the backtick
fence to write instead.
System Metadata ($)¶
The block's YAML payload may contain up to four reserved $-prefixed keys.
After parsing, these keys are extracted from the user field set and exposed
on the block's typed metadata.
$quill: <name>@<version>names the quill that renders the document. The root block (the first block, identified by position) must declare$quill: it is the only required$entry. If the root block is missing$quill, parsing fails.$kind: <kind>identifies a card's kind. The root block's kind ismainby position;$kind: mainmay be omitted or declared explicitly: any other value is a parse error. A composable card names its kind, matching[a-z_][a-z0-9_]*other thanmain, from the quill'scard_kinds. A block after the root with no$kindfails the parse (parse::missing_kind) at its fence. A card with a kind the quill does not declare still renders, but nothing checks its fields, and a plate shows only the kinds it knows.quill.validate(doc)warns on it (validation::unknown_card) and lists the declared kinds.$id: <card-id>names a composable card across edits, so a rewrite or a comment finds it wherever it moved. Leave it out of a new card and one is minted (c3kq9f); keep the line a card already has when you edit it. You may write your own: 1 to 64 letters, digits,_or-, opening with a letter or digit, unique in the document. One outside that, or one an earlier card holds, is replaced with a minted id and aparse::card_id_remintedwarning. The root block takes none, and$idnever reaches the plate JSON.$ext: <mapping>is an opaque YAML mapping reserved for out-of-band extension data: UI editor state, agent annotations, anything bespoke to a consumer that should not reach the rendered output. Round-trips through Markdown and the storage DTO; never appears in the plate JSON consumed by backends. The value must be a mapping (scalars and sequences are parse errors); an empty$ext: {}is preserved as a distinct, explicit declaration. Consumers namespace inside the map ($ext.editor,$ext.agent, …) to avoid collisions;$ext.editor.titleis the canonical key for a per-card display name (an editor-side rename).$seed: <mapping>is a root-only mapping of per-kind seed overlays, keyed by card-kind. Like$extit round-trips through Markdown and storage but never appears in the plate JSON; the seeding layer interprets it (see CARDS.md "Per-kind Seed Overlays"). A composable card carrying$seedis rejected.
$ metadata entries may appear anywhere in the block's payload (the
canonical emission puts them first, in the order $quill, $kind, $id,
$ext, $seed). Any other $-prefixed key is a parse error: the set
is closed.
Version Selectors¶
Pin a specific version with @version syntax on the $quill line:
A bare name matches any version, @2.1 any 2.1.x, and @2.1.0 that version
alone. A lone major, @2, fails to parse, and there is no @latest: write the
bare name. The Quill Versioning
page owns the full selector semantics.
Quill names are [a-z][a-z0-9_]*, at most 64 characters. A $quill name may
carry the collection the quill comes from, $quill: borb.usaf_memo@1.2, under
the same rule; the host resolves the collection, and Quillmark pairs the
document with its quill by the name after the ..
Payload Data Types¶
The data payload (everything in the YAML mapping except the $-prefixed
metadata keys) is standard YAML.
YAML reads an unquoted value by its shape before the schema sees it, so a
text field takes what YAML read, not what you wrote. Quote these:
| Written | Reaches the page | Write instead |
|---|---|---|
zip: 02134, a leading zero |
2134.0 |
zip: "02134" |
ver: 1.10, a version-like number |
1.1 |
ver: "1.10" |
batch: 2024_07, an _ between digits |
202407 |
batch: "2024_07" |
code: 0x1F, a 0x, 0o or 0b prefix |
31 |
code: "0x1F" |
room: 1E3, an exponent |
1000.0 |
room: "1E3" |
phone: +15551234567, a leading + |
15551234567 |
phone: "+15551234567" |
id: 18446744073709551616, an integer past 18446744073709551615 |
1.8446744073709552e+19 |
id: "18446744073709551616" |
ratio: .5 or 5., a . with no digit on one side |
0.5, 5.0 |
ratio: ".5" |
ext: .inf or .nan, in any letter case |
nothing: the parse fails | ext: ".inf" |
id: 1e999, a number too large to hold |
nothing: the parse fails | id: "1e999" |
text: Approve Item #12, a space or tab then # |
Approve Item: the rest is a comment |
text: "Approve Item #12" |
quillmark check reports only the rows that fail the parse; the rest are
well-formed YAML, read as a number or cut at a comment. A word needs no quotes
unless it is true, false or null, in any letter case, or ~: yes,
no, on, off, y and n stay text.
Strings:
title: Simple String
quoted: "String with special chars: $%^"
multiline: |
This is a
multiline string
Numbers:
Booleans:
Arrays:
Objects:
Object-valued fields must be schematized in Quill.yaml with type: object
and a properties: map; array-valued fields with type: array and an
items: element schema (e.g. items: { type: text }, or items: { type:
object, properties: … } for a list of objects). See
Quill.yaml Reference: Field Types.
A Quill.yaml declares field names as [a-z][a-z0-9_]*, so every schema
field is lowercase. The document parser is wider: it accepts any
[A-Za-z_][A-Za-z0-9_]* key and preserves case, so an uppercase or
underscore-led key parses but is always undeclared. An undeclared key stays in
the document, and quill.validate(doc) warns on it (validation::unknown_field),
naming the declared field it most likely meant where one is close. Only $-prefixed keys are
reserved for system metadata.
Comments¶
YAML comments are supported in the payload and round-trip through
toMarkdown, both own-line comments and inline comments:
Comments adjacent to $ metadata keys (own-line or inline) round-trip
identically to comments on data fields.
YAML Tags¶
Custom YAML tags (!include, !env, …) are not supported. A tag is dropped
with a parse::unsupported_yaml_tag warning at the tagged node's path and its
value kept, wherever it sits: on a key's value, a - element, a key, or a
node inside a flow collection.
A core tag (!!str 5) takes effect as the value is read, so k: !!str 5 keeps
"5", a string; the tag itself drops and warns the same way.
Card Blocks¶
Every block after the root is a card: a composable, repeatable record. A card
declares $kind: <kind> (matching [a-z_][a-z0-9_]*, never main) alongside its
data fields; the Markdown after its closing ~~~ fence is the card's body. A
card of a kind declaring body.enabled: false renders without its body, and
quill.validate(doc) warns (validation::body_disabled).
~~~
$quill: my_quill@1.0
$kind: main
title: Main Document
~~~
# Introduction
Some content here.
~~~
$kind: products
name: Widget
price: 19.99
~~~
Widget description.
~~~
$kind: products
name: Gadget
price: 29.99
~~~
Gadget description.
Each card is collected into the plate JSON's $cards array. Its body Markdown:
everything between that card's closing ~~~ fence and the next block's opener
(or document end): is carried as the card's $body value.
Card kinds and their field schemas are declared in Quill.yaml under
card_kinds; see the
Quill.yaml Reference.
Emission¶
toMarkdown always emits the canonical block form: a bare ~~~
opener, the $ metadata lines in the canonical order $quill, $kind,
$ext, $seed, the remaining data fields, and a ~~~ closer. The root
block emits $quill and $kind: main plus any $ext / $seed it
declared ($seed is root-only); composable cards emit $kind: <kind> plus
any $ext they declared. Fence markers,
key ordering, and YAML quoting are normalised; YAML comments (own-line and
inline trailing, including those adjacent to $ lines) survive the
round-trip.
The payload is coerced and validated against the schema declared in the
quill's Quill.yaml (main.fields). See the
Quill.yaml Reference for field
types and constraints.