0.90.0 → 0.91.0 — column-zero card fences, hardened input paths¶
A correctness/security release driven by a full parsing-and-conversion audit
(see VULNERABILITIES.md on the repo root). One syntax rule is tightened and
several input boundaries now reject what they previously accepted; each
breaking change below states its migration. Round-trip and output-fidelity
bug fixes that require no action are summarized at the end.
Markdown syntax: the closing ~~~ must be at column zero¶
Previously the closing fence of a card-yaml block could carry 1–3 leading spaces (inherited from CommonMark's closing-fence rule). It must now be at column zero, exactly like the opener.
Why: the payload between the fences is YAML, where indentation is structural. Under the old rule, a tilde code fence embedded in a block-scalar value —
— was mistaken for the block's closer, silently truncating snippet and
swallowing the rest of the payload into the body. Under the new rule the
indented ~~~ lines are payload and the block parses intact. A column-zero
~~~ can never be block-scalar content (YAML requires scalar content to be
indented past its key), so the closer is unambiguous.
Migration: if a document's only closer was indented, the opener now falls
through to CommonMark as an unclosed code block (with the standard
parse::unclosed_code_block warning) and parsing fails with MissingQuill
instead of silently mis-splitting. De-indent the closing ~~~ to column zero.
Documents emitted by toMarkdown are unaffected — the emitter has only ever
produced column-zero fences.
Parsing: data-field names are validated everywhere¶
Spec §3.4/§10 restricts data-field names to [a-z_][a-z0-9_]*. This was
previously enforced only by the typed mutators; the markdown parse, storage
(Document.fromJson), and wire (pushCard / insertCard) paths accepted any
key. All paths now reject a non-conforming name with an error naming the key.
Unicode and arbitrary characters remain fully supported in values and in
nested map keys — only top-level field names are restricted (they always
were, per spec; the parser now agrees).
Parsing: the §8 nesting limit holds on every input path¶
Values deeper than 100 nesting levels were rejected on the markdown parse path
but accepted (until eventual stack exhaustion) via storage DTOs, the card wire
shape, the mutators, and the Python value converter. All paths now reject them
with a clean error. As part of this, Card.set_ext / set_ext_namespace
(Rust) now return Result — $ext carries the same depth bound.
Quill loading¶
- Symlinks inside a quill directory are skipped, not followed.
- A single file in a quill directory may not exceed 50 MiB.
quillmark validaterejects aplate_filecontaining..or absolute components instead of probing the host filesystem.
Python binding¶
float('nan')/float('inf')field values now raiseValueErrorinstead of being silently stored asnull.- Integers above
i64::MAXnow convert losslessly throughu64; integers outside 64-bit range raiseValueErrorinstead of leaking a rawOverflowError.
Also in this release (no action required)¶
Bug fixes that change output for the better but break nothing:
- Trailing-comment detection follows YAML 1.2 —
x: it's fine # noteno longer loses its comment on round-trip (values were always parsed correctly; only comment preservation changed). renders as#image("src", alt: "…"), carrying alt text into the PDF as accessibility alternate text.- Nested map keys needing YAML quoting (
:,#, leading indicators, edge whitespace,n/true/123) now emit quoted instead of producing YAML that failed to re-parse. - WASM getters throw a named
QuillmarkErrorif internal serialization fails, instead of silently yieldingundefined(the success path is byte-identical). - Typst diagnostics originating in helper or vendored packages report their
own file/line/column instead of
main.typcoordinates.