Skip to content

Migration Guides

A release that breaks the document syntax, the plate-JSON wire format, or a public API ships a guide for that version step. So does a release that moves the embedded Typst to a new minor version, since a plate is Typst source: its guide names the new Typst version and links that version's changelog, even when no plate in this repository changed. Most breaks are hard cutovers — the old form stops parsing or compiling — so the guide is the path forward, not an optional read.

Each guide covers one step. To cross several versions, work through them in order; each states its own breaks in full. The rows below name the step's headline break, enough to pick the guide.

A stored row loads across every step but three: 0.112 → 0.113 refuses a host-authored content name, 0.125 → 0.126 a body nested past 12 levels or a $quill its narrowed grammar refuses, such as the lone major memo@1, and 0.129 → 0.130 a row under any tag older than quillmark/document@0.124.0. Each guide says how to rewrite such a row before upgrading. The storage DTO's bytes and tag hold across every step here except 0.111 → 0.112, 0.114 → 0.115, 0.115 → 0.116, 0.123 → 0.124, 0.128 → 0.129, 0.130 → 0.131, 0.133 → 0.134 and 0.134 → 0.135, where canonical bytes move (content hashes recompute) and the tag becomes quillmark/document@0.112.0, @0.115.0, @0.116.0, @0.124.0, @0.129.0, @0.131.0, @0.134.0 and @0.135.0 in turn. At 0.122 → 0.123 a row holding a comment that ends in whitespace loads without it, and at 0.130 → 0.131 one holding a table's headless: true loads it with head: 0 beside it, a body mark over a line break as one mark per line, and overlapping links as one link per character, and at 0.131 → 0.132 one holding a term line or a description or element container loads as what it held; each rewrites once when saved, under the same tag. At 0.132 → 0.133 a row holding a table on a line it shares with prose loads as stored, where it loaded with the line broken. A build older than a step that moves the tag refuses a row the newer build has rewritten.

Guides

Step What changes
0.134 → 0.135 The content engine's breaking release. A write fills ? placeholders, naming them in the receipt's minted; the structural edits are Document verbs answering a Receipt, beside diffRange and blockAt. A write takes a value only in its canonical form, a stored one reads leniently for this release with content::noncanonical_read, and a table cell and a footnote's note are each one paragraph. A field's content stores canonical, the schema-free writes are doc.unchecked.storeField / insertCard. Core is single-writer: a stale base refuses with edit::stale_base, where it rebased, and late writers merge through @quillmark/collab. The anchor step places or moves (move: true); repair answers its change; movedAnchors carries a reason; the writer's revise answers the content Receipt; a text field is stored as codec: "plain" content. Storage writes quillmark/document@0.135.0, a line-form value loads migrated and warns content::noncanonical_read so a resave rewrites it, and a content value or change holding a key its shape does not define is refused.
0.133 → 0.134 Content v2: every body and richtext value is the content model, with blocks, containers, anchors and island slots carrying ids, in storage under quillmark/document@0.134.0, the Card wire and every read. Offsets in content values and changes cross WASM in UTF-16 code units, where they counted scalar values. Document gains read, apply, revise and replace, each answering a Receipt and taking the base hash the writer read; overwrite, reviseContent, applyChange, mapMarks and mapPos are removed, and so is the line form's ChangeBundle op lane. Every composable card carries a $id, which to_markdown writes after its $kind. Python's writer.revise_body, revise_field and revise_document return a dict receipt, where they returned a list of Diagnostic: a loop over the result runs over its keys without failing, so read receipt["warnings"], the same Diagnostic objects. The dict carries dropped_anchors, and from on the document revise. Revised gains dropped_anchors, and WASM's revise receipts droppedAnchors. A table cell's anchor is listed, carried and reported as the prose's.
0.132 → 0.133 The blueprint is retired: quill.blueprint, QuillConfig::blueprint() and quillmark blueprint are deleted, and a model writes the JSON authoring document instead, read through authoringSchema and imported with fromAuthoring. build_transform_schema takes a SchemaCodec, quillmark validate renders the empty and seeded documents only, and a multi-line description: or an enum member holding >, ; or \| loads. A root example.md is an ordinary file: quillmark validate and example_document() no longer read it, and a quill's example is derived from the example: its fields declare and the body.example its kinds declare, so move each value onto its field. example: returns at any depth, checked as default: is under quill::example_*, and a body takes description and example. schema() and the authoring schema expose them. Quill::example_document() returns Option<Document>, the bindings no longer throw from it, EXAMPLE_FILE and quill::example_reference are deleted, and FieldSchema and BodyCardSchema gain public fields. Normalizing content moves no text: a table's slot sharing its line with prose stays there, where the mint broke the line around it, and markdown export breaks it instead. overwrite takes the shape where it threw block island in a line's prose, a Set on such a table lands, and Typst sets the table as a block in place. WASM gains normalize(content), the form overwrite stores.
0.131 → 0.132 A qm-<name> element such as qm-keep imports as what it wraps, its open tag warning parse::dropped_construct, and a definition list as one paragraph; a stored row holding either loads as what it held and saves without it. The Typst helper's elements registry and typst::unregistered_element are gone, so a plate calling qm.elements.update(..) fails to compile until the call goes. LineKind::Term, Container::Description and Container::Element are gone, as are the WASM term, description and element members. Every Typst PDF carries a title: the render's new title option, else the plate's, else the quill's description.
0.130 → 0.131 A qm-span pair inside a line sets its text's font family, where it dropped, and a row holding one saves as quillmark/document@0.131.0, which a 0.130 build refuses. A family the host library lacks warns typst::unknown_font_family. An app adds a font library through Engine::with_fonts, the WASM fonts option, Engine(fonts=) or --font. In Rust, MarkKind gains FontFamily, TypstBackend is constructed, and workspace takes a &FontLibrary. qm-table and qm-cell no longer spell a table's layout, so markdown written with either loses it: rewrite such a table as HTML. A table holding widths, a placement, a cell alignment, a merged cell or other than one header row exports as an HTML table, and an HTML <table> block imports as a table. A stored headless: true gains head: 0 beside it, which a 0.130 build ignores, and a cell's colspan and rowspan span it. A table holds at most 100 000 cells: import drops the rows past it, and a stored table past it is refused. A body mark over a line break loads as one mark per line, and link is keyed, so a character holds at most one link.
0.129 → 0.130 A stored row tagged quillmark/document@0.81.0 through @0.116.0 fails to load as an unknown schema version: only @0.124.0 and @0.129.0 are read. Resave every older row with a 0.124 to 0.129 build before upgrading.
0.128 → 0.129 A footnote reference and its definition render a footnote, where the definition dropped and the reference stayed text, and $…$ and $$…$$ render equations, where they were text: each imports as an island, footnote and math, so a save writes quillmark/document@0.129.0 and a 0.128 build refuses the row. Escape a dollar, \$, to keep it text. A paragraph with a : description line under it renders a definition list, a term line and a description container; escape the colon, \:, to keep it text. The Typst backend translates TeX to Typst math, warning typst::unsupported_tex for a command it does not lower, and embeds Noto Sans Math for a quill shipping no math font. schema() no longer writes ui.layout: flat on an object alone in a legacy ui.group, ui.layout: flat warns quill::deprecated_flat, and a card-level field outside every group declared after a top-level object warns quill::field_after_object.
0.127 → 0.128 A quill whose requires floor is above the running release fails to load as quill::requires_unmet, where it failed as quill::engine_too_old, and a host matching the old code stops matching. The Rust struct and Python class Quillmark are Engine, the name JavaScript already carries, with no alias, so use quillmark::Quillmark fails to compile and from quillmark import Quillmark fails to import.
0.126 → 0.127 An acroform quill's field spec is Form.yaml: one holding form.yaml fails to open as acroform::legacy_form_yaml, so no acroform quill opens on both releases. An acroform quill whose Form.yaml gives a field a box with no width or height, or one wholly off its page, fails to open as acroform::invalid_field_rect, where it opened and showed that field's value nowhere.
0.125 → 0.126 A .ttc or .otc font collection a quill ships loads every face it holds, up to 256, where the backend skipped it, and a quill whose only fonts are collections no longer renders in the embedded Roboto. The status_report fixture is 0.2.0, its state storing on_track / at_risk / blocked where it stored display text. A javascript:, vbscript:, data: or file: link renders as its text, and a card-yaml block whose alias expansion outgrows its own size fails to parse. A body nests at most 12 levels deep, where it nested 100, and one past it fails parse::body_import at the body's path. A WASM trap rejects as runtime::backend_trapped. A $quill name may carry a collection, borb.usaf_memo@1.2, and no longer starts with _ or runs past 64 characters. A quill declaring a field named body fails to load with quill::reserved_field_name. An acroform quill's field spec is form.yaml: a form.json fails as acroform::legacy_form_json, and invalid_form_json / missing_form_json are invalid_form_spec / missing_form_spec. A Typst quill's plate is plate.typ by name: a quill holding it and declaring no typst.plate_file renders it, where it rendered a blank page, and nothing reports the change; a quill with neither fails as typst::plate_missing; and a typst: key other than plate_file fails the load as quill::unknown_backend_key, where it warned typst::unknown_key. A card declaring sections: fails quill::invalid_card_schema: a card groups fields under a top-level object, which documents and plates address. A blueprint's $kind: main line no longer falls back to quill.description. A $quill selector is @MAJOR.MINOR or exact, so name@1 fails parse::invalid_quill_reference and a stored row holding it fails to load, a document the quill creates pins name@1.2 where it pinned name@1.2.0, and a quill's version is MAJOR.MINOR.PATCH, so version: "1.2" fails quill::invalid_version. A blueprint prints a blank default: as an empty cell, and a defaultless typed table as [], or an empty cell where optional, under its commented-out row. type: plaintext is type: text, type: string is retired in favor of text, read by a plate as a str through qm.str(..), and inline: and ui.multiline give way to multiline:, one line being the default, so no quill declaring a text field loads on both releases, and a stored string value holding an interior line break fails to render; schema() carries multiline where it carried inline: true, and the transform schema a plate reads quillmark:multiline where it carried quillmark:inline.
0.124 → 0.125 A value a Typst plate prints itself, #data.subject included, surfaces no region: a field prints through qm.ink or sits in a qm.region claim, and quillmark check --regions lists what no click reaches. No dictionary carries $ink, every typed dictionary carries $path, and typst::unclosed_field_region is typst::unclosed_region. The helper is version 0.2.0, imported as qm, any other import failing as typst::helper_import; field-region is qm.region, and ink(x).f and display(..) are qm.ink(x, "f", ..). Quill::validate warns backend::declined_construct, the render's own diagnostic, where it warned validation::declined_construct, and draws none for a document the render floor refuses. A typed set / setAll returns the parse::dropped_construct of each richtext string it writes, and addCard its fields' ahead of its body's. A table align, widths or headless outside its set rests as written and reads as absent, where normalization removed it. droppedAnchors holds DocumentAnchors with their line, matched at the exact address. A backtick fence opener whose info string holds a backtick opens no fence for the card scanner, and a raw tag inside a footnote definition warns nothing beside the footnote. A placed table aligns its cells as an unplaced one does. A card's fields go in titled panels under sections:; ui.group warns quill::deprecated_groups, and a grouped card lists its fields in display order. ImportWarning is a struct, Container::Element holds a carrier::Element, the typed writer's set / set_all return Vec<Diagnostic>, and SessionHandle::update takes the session's QuillConfig.
0.123 → 0.124 An HTML block drops as CommonMark reads it, now with a warning, and a line holding only element and qm-table tags reads as a tag line wherever it stands. A fenced block holding <!-- a --> b keeps its line, text after a block nested in a tight list item opens its own line, and a line holding only tags on the line after a table's rows ends the table. - [ ] and - [x] render a box, ticked for [x], and a footnote definition drops with a warning, where it silently defined a link. A raw tag a markdown import drops warns parse::dropped_construct at its body, on doc.warnings, and quillmark validate fails an example.md holding one; a one-shot render's RenderResult.warnings no longer carries doc.warnings, and in WASM a JSON null inside a returned value crosses as null, where it crossed as undefined. A </u> closes the innermost open <u> across **, an unclosed <u> drops where its text ends, and export writes <u> outside the delimiters. from_markdown returns Imported { content, warnings }. A revise returns Revised { delta, warnings }, where it returned the Delta, and add_card its body's warnings; WASM importMarkdown returns { content, warnings }, and Python's revise_body / revise_field / add_card return the warnings. Quill::validate warns validation::declined_construct at a field holding a construct the backend declines, an image under Typst, and an acroform render warns backend::declined_construct for every construct but the paragraph. A qm-table wrapper lays out its table, a qm-* element is stored content rendered through the plate's elements registry, and a save writes the storage tag quillmark/document@0.124.0. A revise keeps the id of each island it leaves unchanged or edits in place. Text after a card-yaml block's root value, and a mapping holding two keys of one text such as 1 and "1", fail the parse where they dropped. quillmark validate reports a bad typst.plate_file under the render's typst::plate_* codes, where it raised cli::plate_file_*. A base PDF with hybrid cross-reference data fails as pdf::xref_stream. A WASM options object, Addr or CardInput inheriting an unknown key from a null-prototype object throws.
0.123.0 → 0.123.1 A richtext(inline) value holding an inline <br> fails as validation::not_inline, where 0.123.0 glued the words either side of it into one line. An inline <br> renders as a line break in a paragraph and a table cell, and a space in a heading, where it rendered nothing, and nothing reports the change. A \n in a table cell's text is a line break, where normalization made it a space. A <br> in an image's alt is a space.
0.122 → 0.123 A number under a boolean field, a matrix member's bare tick included, renders by whether it is non-zero. A float of magnitude at most 2.220446049250313e-16, such as 1e-17, renders true where it rendered false, and nothing reports the change. An acroform form.pdf whose page /Annots hold a widget annotation fails to open as pdf::existing_acroform, whatever its catalog /AcroForm holds. One whose trailer, catalog, page-tree node or /Info object names a key twice or with no value, or holds a token where a key belongs, fails to open as pdf::parse, or pdf::write for /Info. A YAML comment drops the whitespace ending its line, a stored row's included, so to_markdown writes # note for # note, and nothing reports the change. A comment under a tagged empty value, inside an alias key's value or around a merge keeps the slot the YAML gives it, where it moved or dropped, and nothing reports the change; a top-level alias key keeps its place in the field order, where it moved to the block's end. A WASM options object, Addr or CardInput that is not a plain object throws, a class instance or Object.create(defaults) included, where 0.122 read its keys, own or inherited; so does a non-enumerable key its verb does not read. An open matrix's added item whose title is "" or whitespace alone fails as validation::enum_violation, where it printed a bare tick.
0.121 → 0.122 Only true and false are booleans: an unquoted y, n, yes, no, on or off, in any case, is the text written. A string field that printed false for middle_initial: N prints N, and nothing reports the change. A word under a boolean, number or integer field, a matrix member's tick included, fails as validation::type_mismatch. In Quill.yaml a word where a boolean belongs fails to load, and values: [US, NO, DE] loads. Every stamped checkbox, checked or not, carries /AP << /N << /Yes … /Off … >> >> with /AS naming its state, so a filler can toggle it, where a checked box carried one stream and an unchecked box no /AP. A reader taking /N only as a stream draws an empty box, and nothing reports it. StampOptions gains states; AppearanceStates::Selected stamps the one-stream shape for such a rasterizer. A Typst quill whose plate_file names a file at a path Typst refuses, such as a name holding a \, fails to open as typst::plate_path_invalid, where it loaded that plate as main.typ and so shadowed the quill's own main.typ. A WASM options object refuses a key it does not read: engine.render and session.render throw on a render option outside format, ppi, pages and regions, and new Engine on a key other than backends, where each dropped it. engine.render(quill, doc, { today }) rejects rather than rendering the local date; the date is the trailing argument. A comment keeps the slot the YAML gives it: one in a sequence at its key's column, indented less than its block, in a compact nested sequence or trailing a plain scalar's continuation line re-emits there, where it moved or dropped, and nothing reports the change. parse::unsupported_yaml_tag warns on every tagged node at its own path, a flow collection's and a sequence element's included. A column-zero key outside the field grammar fails as parse::invalid_structure. A failed Typst compile's error carries the quill's load warnings after its errors, and nothing reports the change: a host counting every diagnostic as an error reads severity.
0.120 → 0.121 A matrix member is held by being present, so a mapping naming no held flips from unticked to ticked with nothing reporting it, and a mapping storing held — as 0.120's strict write and the editor stored every tick — fails validation as validation::held_stored. A matrix reaches the plate as the members it holds, so a plate reading held or a member's title fails to compile and prints through roster instead, and a member's tick is its own address. An open matrix admits items a document adds.
0.119 → 0.120 quill::table_column_not_flat is quill::table_column_not_leaf: the refusal of a ui.layout: table column that is not a leaf is unchanged but for its code.
0.117 → 0.118 A card kind declares seed:, which Add Card writes into a new card of the kind. A document's $seed warns wherever the card it seeds would: an undeclared nested key (validation::unknown_field), a stranded variant cell (validation::out_of_variant), a $body that is not importable markdown (validation::seed_overlay_shape) and an array past max: (validation::cardinality), so quillmark validate can fail a quill whose example.md carries one. CardSchema has a public seed field.
0.116 → 0.117 example: and body.example leave the quill schema: a Quill.yaml declaring either, at any depth, fails to load (quill::field_parse_error, quill::invalid_body), and a format hint moves into description. The quill::example_{type_mismatch,not_in_enum,format_violation,on_namespace,over_max} and quill::body_example_* codes are deleted and richtext_example_import is richtext_default_import; quill::example_reference is new, for the example document. The blueprint drops its # e.g. and # body e.g. lines but a matrix's member hint, and schema() carries no example, so QuillFieldSchema.example and QuillCardBody.example are deleted. Also: $quill: name@latest fails as parse::invalid_quill_reference; write the bare name, which matches the same versions. VersionSelector::Latest is Any and has no Display, and QuillReference::latest is deleted. version: "", a tagged version such as !!float 1.10, and a card kind named main fail to load. quillmark validate reads a root example.md as the quill's example, and fails the quill on any diagnostic it carries. A quill shipping no fonts renders in the embedded Roboto, not Figtree.
0.115 → 0.116 !must_fill is removed, with its store and fill APIs. A card of an undeclared $kind, and a body under body.enabled: false, render and warn where they failed, on a one-shot render's warnings and from quill.validate(doc); an undeclared key warns too. A typed dictionary or table row carries $ink and $path. A card with no $kind, and a non-mapping payload, fail at the fence. Also: the Rust render verbs take a required today: CalendarDate, WASM paint takes one scale, a card-wire item refuses fill or any unknown key, the blueprint leaves every body empty with its body.example on a # body e.g. line, FORMAT_RULES is MARKDOWN_RULES, and blueprint_instruction is deleted.
0.114 → 0.115 A matrix member mapping takes the ordinary ladder: a mapping naming no held is unheld, where presence alone ticked it, so { detail: X } means what { held: false, detail: X } means. It is the release's one silent break — the value is well-typed under both readings, and the page prints the box unticked and the member's columns blank — so documents storing a ticked member as a bare mapping need held: true spelled before upgrading. Two copies of a fact an island already carried go. An island’s canonical form loses loss, and a block island’s line is a para — whether its slot’s markup is a block is read off the island’s type, where the mint and both emitters already read it. A host branching on line.kind === 'island' reads the island instead, and the setKind in every block-island bundle goes. ContentIsland.loss, ContentLossClass, Loss, Island::with_loss, IslandType::default_loss and LineKind::Island are deleted, and the closed content vocabularies drop to four. Canonical bytes move for every document holding an island, so content hashes recompute and the tag becomes quillmark/document@0.115.0. Stored rows keep loading: the decoder reads island as para, and ignores a loss, permanently. Also: a schema is loaded, not deserialized — QuillConfig, CardSchema and FieldSchema drop Deserialize and FieldSchema::from_quill_value turns crate-internal. All three still serialize. A matrix roster is one flat mapping of id: Title, so MatrixGroup, MATRIX_GROUP_KEY, QuillMatrixGroup, the group key on every projected member and quill::duplicate_matrix_member all go. A blueprint stops naming a variant-bearing enum's dormant worlds: each is its own cells, commented out under a # when <MEMBER>: header. quillmark validate compiles the plate: it renders the empty document, the blueprint and the seed through the quill's backend, so a quill whose plate does not render exits 1 where it exited 0 — --no-render is the configuration read on its own. And a ui.layout: table column that is not a leaf is quill::table_column_not_flat at load.
0.112 → 0.113 The pdfform backend becomes acroform at every layer — crate, backend: id, feature, pdfform::* codes — with no alias, so every form quill's Quill.yaml stops rendering until edited. The content vocabularies close: a line kind, container, mark type, island type or loss outside the built-ins stops opening a stored row. Every column-zero ~~~ block is a card whatever its info string, so a ~~~rust fence in a body opens one where it opened a code block — a backtick fence is the escape hatch. A content line op the text or block contradicts lands and the terminal normalize settles it, so Ok stops meaning the op landed as written. A Content read omits a zero Container.instance where it spelled one on every container, so the key a host read off a read is absent and instance is optional in TypeScript. Every quillmark-content and quillmark-core item is named at the module that defines it, so a Rust consumer's quillmark_content::Delta and quillmark_core::Document imports gain a module segment — the quillmark facade absorbs it and moves nothing. Also reader.get in the values form, reader.resolve(), the toStored / fromStored / loadStored renames, a wide pruning sweep, and a group of tightened refusals no type checker reports: new parse::* and backend::* codes, a field write past the §8 field count refused at the write, a Quill.yaml declaring more fields than one card carries refused at load, a stricter main: and Quill::validate, and the acroform canvas-box origin.
0.111 → 0.112 One spelling per name: every vocabulary member carries its payload in attrs, built-ins included, so a host reading line.level or mark.url reads them under attrs and the authored lane refuses the old spelling.
0.110 → 0.111 A fix release: four surfaces refuse input they were producing something wrong from. The one break that can stop a render is a pdfform background carrying its own /AcroForm (pdf::existing_acroform).
0.109 → 0.110 Every hand-built container literal must spell instance: the discriminator becomes a required field on the whole-document write lanes, and ContentContainerInput is deleted.
0.108 → 0.109 Canonical form becomes a type — Normalized, which every codec returns and every projection takes — and Container gains instance, so an exhaustive match needs ...
0.107 → 0.108 A cell holds a value, a namespace holds cells: a default: / example: on a typed dictionary fails load, an absent container reaches its properties' defaults (a render-output change), and must_fill: retires.
0.106 → 0.107 A declared type means the same thing at every depth. The break no checker reports: a plate read of a typed-table row cell regions on the cell (main.refs[0].org). A date lowers to a native datetime.
0.105 → 0.106 A schema address may step one property into a declared container — so region addresses shift — and an enum may declare variants:, which rests that field as {value: <member>, …} rather than a bare string.
0.104 → 0.105 A field's blank becomes a property of the field rather than a member of its type's domain: an absent no-default: enum renders blank instead of values.first(), and every enum accepts "" as input.
0.103 → 0.104 type: enum with values: is the one spelling of a closed string domain (enum: is a load error), and a geometry address spells an array element bracketed: main.references[0].
0.102 → 0.103 WASM: init() resolves to the core surface, and Quill, Document and the free functions leave the static exports — destructure them from await init(). CLI: render --verbose writes its progress to stderr.
0.101 → 0.102 The pre-1.0 vocabulary reset: install* → overwrite*, setBody → reviseBody, LiveSession.apply → update, and the edit::* codes rename. @quillmark/wasm ships --target web, so await init() is required.
0.100 → 0.101 Two Rust constructors leave the published surface (Document::from_main_and_cards, QuillConfig::from_yaml), and the change-bundle verbs take one ChangeBundle struct, which gains an island channel.

For how Quills themselves are versioned and how authors target a version, see Quill Versioning.