Skip to content

0.125 → 0.126 — a quill's font collections load, status_report stores enum ids, a $quill name may carry a collection, an acroform quill's field spec is form.yaml, a Typst quill's plate is plate.typ by name and its typst: section is strict, a card groups fields under a top-level object where it declared sections:, a quill's version is MAJOR.MINOR.PATCH and a $quill selector @MAJOR.MINOR or exact, a blueprint prints a blank default: as no default, a text field is text or richtext, one line unless it declares multiline: true, and script links, runaway YAML expansion and bodies nested past 12 levels are refused

The first break nothing reports: a Typst quill holding plate.typ at its root and declaring no typst.plate_file renders that plate, where it rendered a blank page. The backend finds the plate by name, so a plate.typ the quill carried without wiring it up now compiles. Every render of such a quill changes, and no diagnostic says so. A plate.typ there that does not compile, a stale draft or one importing a file the quill no longer holds, fails open and quillmark validate. Delete or fix it, or declare the plate the quill means as typst.plate_file.

The second break nothing reports: a .ttc or .otc under assets/fonts/ or inside a vendored package loads every face it holds, up to 256, where the backend skipped it. One declaring more loads none and warns typst::font_collection_skipped. A plate naming a collection's family renders in it, where it rendered in a fallback face and warned unknown font family. A quill whose only fonts are collections renders in them, where it rendered in the embedded Roboto, so a plate there naming Roboto warns unknown font family. A collection's faces also join the glyph fallback for a character the plate's fonts lack, even one inside a vendored package the plate never imports.

The third break: the status_report fixture quill's state stores on_track, at_risk and blocked, where it stored on track, at risk and blocked. The quill is version: 0.2.0, where it was 0.1.0, so a document pinned status_report@0.1 fails quill::version_mismatch. A document holding state: at risk fails validation::enum_violation and does not render. Rewrite on track to on_track and at risk to at_risk, and the pin to status_report@0.2; the banner prints the same text.

A text field is text or richtext, and either is one line unless it declares multiline: true. type: plaintext is type: text, type: string is retired in favor of text, and inline: and ui.multiline give way to the one key multiline:. Each retired spelling fails the load as quill::field_parse_error, its message naming the replacement, and a richtext must spell multiline: either way for this release, so none of this migrates silently. Rewrite each declaration:

Was Now
type: plaintext with inline: true type: text
type: plaintext type: text with multiline: true
type: string type: text, plus multiline: true where a value takes more than one line
type: richtext with inline: true type: richtext with multiline: false
type: richtext type: richtext with multiline: true
ui: { multiline: true } multiline: true on a text or richtext; delete it elsewhere
string?, items: { type: string } text?, items: { type: text }

No Quill.yaml declaring a text field loads on both releases. 0.125 refuses type: text and a field-level multiline:, and 0.126 refuses string, plaintext and a richtext without multiline:. A host upgrades a quill and the engine together, cutting both over in one deploy.

A stored former string value holding an interior line break renders on one line. string, plaintext and text all rest as the verbatim string, so no stored row is rewritten, but string admitted any string and text is one line unless it declares multiline: true. A one-line value ignores trailing whitespace, so a YAML | block's trailing newline passes; a line break inside the value renders as a space and warns validation::line_break_folded, and a write refuses it as edit::field_not_single_line. On 0.126.0 the render failed instead, with validation::coercion_failed, target text(single_line). To find these values, run quillmark check --strict with the rewritten quill over the stored documents, or validate in a binding. Declare multiline: true on a former string field whose breaks carry meaning, such as an address.

The blueprint prints each text type by its new name and line shape:

0.125 annotation 0.126 annotation
string text<plain>, or text(multiline)<plain> where the field declares multiline: true
plaintext(inline)<plain> text<plain>
plaintext<plain> text(multiline)<plain>
richtext(inline)<markdown> richtext<markdown>
richtext<markdown> richtext(multiline)<markdown>

richtext<markdown> names a one-line field, where it named a block one, and neither (inline) annotation is printed.

A former string value reaches a Typst plate as content, where it was a str, so it prints with nav and click-to-edit. A plate that computes with it reads the value through the new qm.str(..), which returns its string (qm.data.memo_for.map(qm.str) for an array). A split, a len, set document(author: ..) or an image(..) path fails to compile until it does. Printing it, #qm.data.title, needs no change. A matrix added item's title is a one-line text cell: a stored document whose added item holds a multi-line title fails validation::not_single_line. A qm.roster row's title stays a str.

Four changes nothing reports:

  • A comparison or upper on a former string value runs without qm.str and answers wrong. qm.data.status == "final" is false for every value, so the plate takes the other branch, and upper(qm.data.office) returns content. Search the plate for ==, !=, in and upper( on a text field and wrap the value in qm.str(..).

  • schema() in WASM and Python, and quillmark schema, carry multiline where a field carried inline: true. The echo is the declaration: string and plaintext are text, every richtext carries multiline: true or false, a text carries multiline: true where it admits more than one line, and the inline key is gone. The polarity inverts: a consumer that read the absence of inline as "block" now reads every one-line field as a block. Branch on multiline instead. WASM QuillFieldUi has no multiline; read the field's own.

  • The transform schema a Typst plate's helper reads, build_transform_schema in Rust, emits quillmark:multiline: true on a field that admits more than one line and on every $body, and no quillmark:inline. A former string field there is {type: object, contentMediaType: application/quillmark-content+json, quillmark:plain: true}, where it was {type: string}. QUILLMARK_INLINE_KEY is QUILLMARK_MULTILINE_KEY.
  • An acroform text widget is multiline exactly where its field declares multiline: true, where it read ui.multiline. A block richtext without that hint bound a one-line widget, which a value holding a newline widened at render (#2217); it now binds multiline.

validation::not_inline, edit::field_not_inline, conform::field_not_inline and quill::seed_not_inline are validation::not_single_line, edit::field_not_single_line, conform::field_not_single_line and quill::seed_not_single_line, and their codec argument names text where it named plaintext.

A link whose target's scheme is javascript:, vbscript:, data: or file: renders as its text alone under Typst, where it rendered a live link in PDF and SVG. A card-yaml block whose expanded aliases and tags pass twice the block's bytes plus 4 KiB of scalars, or the block's bytes plus 4 KiB of nodes, fails its YAML parse, where it parsed.

A body nests at most 12 levels deep, where it nested 100. The list items, quotes and elements open at one point of a body count toward the bound, and its marks count nothing (markdown spec §8). A body past it fails parse::body_import at the body's path, main.body or cards.<kind>[i].body, where it imported and failed to render once Typst's show-rule depth ran out, by 16 quotes or 32 lists. Flatten the deepest levels. A stored row holding such a body fails to load, uncoded, its message naming NestingTooDeep: open it with a 0.125 build and flatten it before upgrading. MAX_NESTING_DEPTH is 12, and ParseError::BodyImport holds { path, message }.

A quill declaring a field named body, on main or a card kind, fails to load with quill::reserved_field_name, where it loaded and the field shared its path, main.body or cards.<kind>[i].body, with the card's body. Rename the field, and its key in every document.

An acroform quill's field spec is form.yaml, where it was form.json. A quill holding form.json, alone or beside form.yaml, fails to open as acroform::legacy_form_json. Rename the file: JSON is YAML 1.2, so its contents load unchanged, and the schema tag stays quillmark/form@0.2.0. A generator may keep writing JSON, under the new name. Three contents fail as acroform::invalid_form_spec where form.json loaded:

  • A string holding DEL, a C1 control other than U+0085, U+FFFE or U+FFFF written raw, which the parser refuses. Escape it, as \u007f.
  • A key written twice in one object, which form.json read as its last value when the key was one the spec does not know. Delete the duplicate.
  • A rect value past f32 range, such as 1e39, which form.json read as infinity.

Any spec that fails to parse is acroform::invalid_form_spec, where it was acroform::invalid_form_json, now located at its line and column in form.yaml. A quill with no spec is acroform::missing_form_spec, where it was acroform::missing_form_json.

A Typst quill's plate is plate.typ at the quill root, found by name, and the typst: section holds only plate_file, which names another path. Two kinds of quill that rendered now fail, and no type checker sees either:

  • A quill holding no plate.typ and declaring no typst.plate_file fails to open as typst::plate_missing, its hint naming both, where it rendered a blank page and reported nothing. quillmark validate reports it, under --no-render too. A plate_file that is not a string, such as 5 or an empty plate_file:, or that names no path, such as "" or ./, fails as typst::plate_path_invalid, where it rendered the same blank page or failed as typst::plate_missing. Ship the plate as plate.typ, or name it.
  • A key under typst: other than plate_file fails the load as quill::unknown_backend_key, carrying backend and key in args and a hint listing the valid keys, where the quill loaded and every render and session warned typst::unknown_key, which is retired. typst.packages is the common one: delete it, since a quill vendors its packages under packages/. An acroform: section holding any key fails the same way, since that backend reads none.

A backend section that is a scalar or a sequence fails as quill::invalid_backend_section, where it was ignored; an empty one, typst: alone, reads as no keys. A quill whose typst: { plate_file: plate.typ } names the default loads unchanged, and may drop the section. quillmark_typst::plate_file, re-exported as quillmark::typst_plate_file, returns Result<String, RenderError>, where its Ok held an Option<String>.

Under WASM, a render that traps rejects as runtime::backend_trapped, the WebAssembly.RuntimeError on its cause, where it rejected as an uncoded attempted to take ownership of Rust value while it was borrowed. A native stack overflow rejects under the same code, its RangeError (V8, JavaScriptCore) or InternalError (Firefox) on cause, where it rejected as that error itself: a host catching instanceof RangeError reads cause instead. A host matching either routes on the code, and discards the realm.

A card declaring sections: fails to load with quill::invalid_card_schema, its hint naming the top-level object. A card groups fields by declaring a top-level object keyed by the group's id, with the group's title and its fields as properties (Grouping). The object is data: documents nest each group's keys under its id, and a plate reads data.<group>.<field>. To keep every address, move each section's fields up under the card's fields: instead, ungrouped. ui.group / ui.groups load as before, with quill::deprecated_groups hinting the object rewrite. quill::empty_section, quill::invalid_section_id, quill::mixed_sectioning and quill::duplicate_field, which named a field declared twice across fields: and the sections, are retired with the key. The usaf_memo fixture declares its fields ungrouped, so its schema emits no ui.groups or ui.group; its addresses are unchanged.

A $quill name may carry a collection, borb.usaf_memo@1.2, and no longer starts with _. A quill name and a collection name are each [a-z][a-z0-9_]*, at most 64 characters; quill.name in Quill.yaml holds to the same cap. The engine pairs a document with a quill by the part after the .. QuillReference carries collection: Option<String>.

A blueprint's $kind: main line carries main.title and main.description, and nothing where neither is declared, where it fell back to quill.description. quill.description describes the package, for a gallery or a quill picker, and never reaches the blueprint. A quill that wants the line kept copies its quill.description to main.description. Documents are unaffected.

A blueprint prints a blank default: ("", [] or {}) as no default does: notes: # text<plain>, where it printed notes: "" # text<plain>. The field renders its blank either way, so the cell says nothing the empty one does not. A matrix's # e.g. line shows such a column as a defaultless one, its type. 0 and false print as before. A required typed table without a default: prints [] # array<object>, and an optional one an empty cell, r: # array<object>?, each led by its synthetic row commented out, where it printed the row live; a blueprint shipped unedited renders that table empty, or none where optional, where it rendered one blank row. A consumer that read "" as "an author may skip this" reads the field's description. The blank default: stays legal and draws no diagnostic, so a quill deletes it at its own pace. Deleting one drops it from Quill::schema and moves the field's resolve() source from default to blank, which an editor may read: @quillmark/svelte marks a field without a default: as one an author is expected to fill. Documents are unaffected.

A $quill selector is @MAJOR.MINOR or @MAJOR.MINOR.PATCH, so $quill: memo@1 fails parse::invalid_quill_reference, where it matched any 1.x.x. A quill's version ranks a bump by what it costs a stored document, and a MINOR bump needs each one rewritten, so a lone major names no compatible series (Quill Versioning). Rewrite the reference to the series the document was written against, memo@1.2, or drop the selector to match any version. VersionSelector::Major is removed.

A stored row whose $quill is a lone major fails to load, and 0.126 cannot open it to fix it. No storage door reports parse::invalid_quill_reference. WASM fromStored / loadStored and Python Document.from_stored throw one uncoded diagnostic whose message holds invalid quill reference "memo@1". In Rust, Document::try_from(StoredDocument) returns StorageError::InvalidQuillReference { value, reason }, and serde_json::from_str::<Document> a serde_json::Error carrying the same message. Rewrite such rows before upgrading. A row is JSON, so a text match finds it:

grep -lE '"type":[[:space:]]*"quill",[[:space:]]*"value":[[:space:]]*"[a-z_][a-z0-9_]*@[0-9]+"' <rows>

With a 0.125 build, open each row, set the series and save it: fromStored, setQuillRef("memo@1.2"), toStored (Python from_stored, set_quill_ref, to_stored). Without one, rewrite the string in place; nothing else in the row changes:

sed -i -E 's/("type":[[:space:]]*"quill",[[:space:]]*"value":[[:space:]]*"memo@1)"/\1.2"/' <row>

A document the quill creates pins its MAJOR.MINOR, name@1.2, where it pinned the exact version, name@1.2.0. The seeded, empty and example documents carry it (seed_document, empty_document, example_document, WASM seedDocument, emptyDocument, exampleDocument), and so does the blueprint's $quill line. Every patch in the series reads such a document, where a patch release failed it with quill::version_mismatch. A host comparing a created document's quillRef / quill_ref to name@version compares to name@MAJOR.MINOR; Python's Quill.quill_ref stays name@MAJOR.MINOR.PATCH, so it no longer equals one. A document an earlier build created keeps its exact pin: rewrite it to name@MAJOR.MINOR for a later patch to read it.

A quill's version is MAJOR.MINOR.PATCH, so version: "1.2" fails to load with quill::invalid_version, where it loaded. Write 1.2.0. An unquoted version: 1.10, which read as 1.10, fails the same way: write 1.10.0.

Break Surface Action
A Typst quill holding plate.typ and declaring no typst.plate_file renders that plate, where it rendered a blank page; one that does not compile fails open and quillmark validate Typst rendered output, open / render, quillmark validate Delete or fix a stale plate.typ, or declare the plate as typst.plate_file
A .ttc or .otc the quill ships loads every face, up to 256, where it was skipped Typst rendered output, quillmark workspace fonts None
A quill whose only fonts are collections no longer carries the embedded Roboto Typst rendered output Ship Roboto beside the collections to keep it
A character the page draws as an empty box, because no loaded font covers it, warns typst::missing_glyph, naming the characters; quillmark validate fails an example.md that draws one Typst session warnings, RenderResult.warnings, quillmark validate Bundle a font that covers them
status_report is 0.2.0, and its state values are on_track, at_risk, blocked documents against status_report@0.1 Rewrite on track → on_track, at risk → at_risk, and pin status_report@0.2
A javascript:, vbscript:, data: or file: link renders as its text Typst rendered output Link over https:
A block whose expansion passes 2× its bytes + 4 KiB of scalars, or its bytes + 4 KiB of nodes, fails to parse Document parse, every surface Write the values out, or alias less
A body nested past 12 levels fails parse::body_import, at the body's path Document parse, every surface Flatten the deepest quotes and lists
A stored row whose body nests past 12 levels fails to load with an uncoded diagnostic WASM fromStored / loadStored, Python from_stored, Rust Document deserialize Flatten it with a 0.125 build before upgrading
A one-line text value drops its trailing whitespace before the single-line check, and the not_single_line diagnostics carry no trailingNewline arg validation::not_single_line, edit::field_not_single_line, conform::field_not_single_line args; Rust ValidationError::NotSingleLine / EditError::FieldNotSingleLine Drop any branch on trailingNewline and the trailing_newline field
A stored row holding more than 1,000 cards fails to load with an uncoded diagnostic WASM fromStored / loadStored, Python from_stored, Rust Document deserialize Split it with a 0.125 build before upgrading
Placing a 1,001st card fails edit::too_many_cards; EditError::TooManyCards { max } is a new variant WASM insertCard / addCard, Python insert_card / add_card, Rust push_card / insert_card Split the document, or match the variant
MAX_NESTING_DEPTH is 12; ParseError::BodyImport holds { path, message } quillmark-content, quillmark-core Rust API Match the struct variant
usaf_memo's schema emits no ui.groups / ui.group schema() of the fixture quill None
A card declaring sections: fails quill::invalid_card_schema Quill.yaml load, every surface Make each section a top-level object, nesting its keys in documents and plates; or lift its fields into fields:
A field named body fails quill::reserved_field_name Quill.yaml load, every surface Rename the field in the quill and its documents
A form.json fails acroform::legacy_form_json acroform quills, open / render, every surface Rename it to form.yaml, contents unchanged
acroform::invalid_form_json is acroform::invalid_form_spec; acroform::missing_form_json is acroform::missing_form_spec acroform open / render diagnostics Match the new codes
A Typst quill with no plate.typ and no typst.plate_file fails typst::plate_missing, where it rendered a blank page Typst open / render, quillmark validate Ship plate.typ, or declare typst.plate_file
A typst.plate_file that is not a string, or names no path ("", ., ./), fails typst::plate_path_invalid Typst open / render, quillmark validate Write the plate's path, or delete the key
A key the backend does not read under its section fails quill::unknown_backend_key; typst::unknown_key is retired Quill.yaml load, every surface Delete the key; vendor a typst.packages entry under packages/
A backend section that is a scalar or a sequence fails quill::invalid_backend_section Quill.yaml load, every surface Write a mapping, or delete the section
plate_file returns Result<String, RenderError> quillmark-typst, quillmark Rust API (typst_plate_file) Drop the Option
QuillConfig has public license and homepage fields, so a struct literal naming neither fails to compile quillmark-core, quillmark Rust API Name both fields, None where undeclared
A WASM trap rejects as runtime::backend_trapped, and so does a native stack overflow, where it rejected as the bare RangeError (V8, JavaScriptCore) or InternalError (Firefox), so an instanceof RangeError check no longer matches it @quillmark/wasm Engine.render / open, LiveSession.update / render / paint Route on the code, the original error on cause; discard the realm
A quill.name over 64 characters fails quill::invalid_name Quill.yaml load, every surface Shorten the name, and every $quill naming it
$quill: _name, or a name or collection over 64 characters, fails parse::invalid_quill_reference Document parse, StoredDocument load (StorageError::InvalidQuillReference), card-wire input, every surface Rewrite such a $quill to a valid name before upgrading; no quill could carry it
QuillReference has a collection field quillmark-core Rust API Construct with QuillReference::new, or name the field
A blueprint's $kind: main line no longer falls back to quill.description blueprint(), every surface Copy quill.description to main.description to keep the line
A blank default: prints as an empty cell, and a defaultless typed table as [], or an empty cell where optional, under its commented-out row blueprint(), every surface Read whether a field wants an answer from its description; optionally delete blank defaults
$quill: name@N, a lone major, fails parse::invalid_quill_reference Document parse, card-wire input, every surface Rewrite it to name@N.M, or drop the selector
A stored row whose $quill is name@N fails to load with an uncoded diagnostic WASM fromStored / loadStored, Python from_stored; Rust StorageError::InvalidQuillReference Rewrite the row before upgrading, with a 0.125 build or in its JSON
A two-segment Quill.yaml version, "1.2" or unquoted 1.10, fails quill::invalid_version Quill.yaml load, every surface Write all three segments, 1.2.0
A created document's $quill is name@1.2, where it was name@1.2.0 seeded, empty and example documents, the blueprint, every surface Compare a created document's quillRef / quill_ref to name@MAJOR.MINOR; rewrite an exact pin an earlier build created
VersionSelector::Major is removed quillmark-core Rust API Match Minor or Exact
type: plaintext, type: string, inline: and ui.multiline fail quill::field_parse_error; a richtext without multiline: does too Quill.yaml load, every surface Declare text / richtext with multiline: per the table above
A former string value is content in a Typst plate Typst plates Read it through qm.str(..) wherever the plate computes with it
No Quill.yaml declaring a text field loads on both 0.125 and 0.126 Quill.yaml load, every surface Upgrade each quill and the engine together
A stored former string value holding an interior line break renders it as a space and warns validation::line_break_folded (on 0.126.0 its render failed validation::coercion_failed); a write refuses it; a one-line value ignores trailing whitespace quillmark check, validate, render, writes, every surface Run quillmark check --strict over the stored documents; declare multiline: true on a field whose breaks carry meaning
schema() carries multiline where it carried inline: true; string and plaintext are text; the inline key is gone WASM and Python schema(), WASM QuillFieldSchema, quillmark schema Branch on multiline; read it off the field, not ui
The transform schema carries quillmark:multiline, not quillmark:inline; a text field is a content object Rust build_transform_schema, the Typst plate helper Branch on quillmark:multiline
The blueprint prints text<plain>, text(multiline)<plain>, richtext<markdown> and richtext(multiline)<markdown>; richtext<markdown> is one line blueprint(), every surface Read the annotations per the table above
validation::not_inline, edit::field_not_inline, conform::field_not_inline, quill::seed_not_inline end in not_single_line; the codec arg names text diagnostics, every surface Match the new codes and value
The target of validation::coercion_failed, edit::field_coercion_failed and conform::field_coercion_failed is text(single_line), richtext(single_line) or text, where it was plaintext(inline), richtext(inline) or plaintext diagnostics, every surface Match the new values
FieldType::String is removed and PlainText { inline } is Text { multiline }; RichText { inline } is RichText { multiline } quillmark-core Rust API Match the new variants
ValidationError::NotInline is NotSingleLine; EditError::FieldNotInline is FieldNotSingleLine quillmark-core and quillmark Rust API Match the new variants
QUILLMARK_INLINE_KEY is QUILLMARK_MULTILINE_KEY; document::edit::CODEC_PLAINTEXT is CODEC_TEXT quillmark-core Rust API Use the new constants
UiFieldSchema::multiline is removed quillmark-core Rust API Read the line shape off FieldSchema::r#type