0.123 → 0.124 — surface parity: one stored model, every surface a codec of it¶
A document's stored JSON is its one model, and markdown, the op wire and the Typst lowering are codecs or projections of it; PARITY.md holds what each surface does with each construct. Each section below is one concern, those whose breaks no type checker reports first, and the table closing the guide lists every break.
Raw HTML around markdown¶
HTML other than the carrier still drops as CommonMark reads it, and the
import now says so. CommonMark runs a block that opens with a tag line, such
as <div>, <center>, <details>, <p> or a custom element, to the next
blank line, and the block drops with the markdown it holds, as in 0.123:
renders nothing, and warns parse::dropped_construct (div). With a blank
line under the <div> and above the </div>, the usual GitHub spelling, the
table renders. A block holding only closing tags and the text it drops warns
under its first tag. Put blank lines around the tags, or delete them. A line
holding only the tags of qm-* elements and qm-table is the exception: it
reads as a tag line wherever it stands outside a heading, a mark or a link, so
<qm-keep> tight above a table keeps the table.
An inline <u> pairs as HTML pairs it. A </u> closes the innermost
<u> still open, one carrying an attribute included, and never a **:
**bold <u class="x">a</u> b** renders all three words bold, where 0.123 ended
the bold at </u>, and <u>a **b</u> c** underlines a b and bolds b c. A
<u> still open where its paragraph, heading, item text or table cell ends
drops and reports as u, where 0.123 underlined the rest of the body; a
hundred of them no longer fail the import. Close each <u> in the text it
opens in. Export writes a <u> tag outside the **, _ and ~~ delimiters
at its position, so **ab<u>cd</u>** exports as **ab<u>cd**</u>: both read
back as the same content.
A line holding only tags on the line after a table's rows ends the table,
where a type 7 tag added an empty row. </qm-table> under the rows
rendered an empty last row; </div> and </center> ended the table already.
A fenced code block holding <!-- a --> b keeps its line, where 0.123 broke
it after -->. The comment repair edited the raw text, fences included, and
now edits only HTML the parser found.
A paragraph whose first line holds only tags that drop opens on its second
line, where 0.123 opened it with a space. <b><i> over text stores text,
where it stored text and exported  text; the tags warn as any dropped
tag does, and nothing else reports the change.
Text after a block nested in a tight list item opens its own line¶
0.123 joined such text to the nested block's last line, so after imported
into the code block; 0.124 renders it as a paragraph in the item. The same
holds after a nested quote, heading, rule or HTML block.
Task lists render a box, and a footnote definition drops¶
- [ ] open and - [x] done render a box before the item's text, ticked
for [x], where 0.123 rendered the brackets as text. The item stores the
mark as its checked, true or false, and its text without it. A Typst
plate draws the box, or replaces it through the helper's tasks state:
#import "@local/quillmark-helper:0.1.0": data, tasks
#tasks.update(_ => (done, body) => if done { strike(body) } else { body })
To keep the brackets as text, escape the first: - \[ ] open. A row stored
under an older tag keeps its [ ] open as text, and export escapes it.
A marker with no text after it ends its item at the next blank line, as
CommonMark reads an empty item: in - [ ] then a blank line then an indented
b, b is a paragraph after the list, where 0.123 kept it in the item. Give
the marker text on its line to keep the blocks below it in the item.
A footnote definition, [^1]: The note., drops with its note and warns
parse::dropped_construct (footnote), and [^1] stays the text written.
0.123 read the definition as a link reference definition, dropping its line
with no warning and making every [^1] a link to The note.. Quillmark has
no footnotes; put the note in the prose.
Stored rows¶
The storage tag is quillmark/document@0.124.0, the first that can hold an
element or a task item's checked. A row under an older tag loads as before, and its content does not
re-import. A save rewrites it under the new tag, so a hash over stored bytes
recomputes once, though nothing else in the row moves. A 0.123 build refuses a
row from then on, as a build too old for its tag. A row tagged
quillmark/document@0.92.0, whose bodies are markdown, imports them on load as
0.124 imports any markdown.
A markdown import reports what it drops¶
The warning is parse::dropped_construct, Severity::Warning, one per
construct per body or field: args.construct is a raw tag's lowercase name
(div, span, pre), and args.count how many. A comment, a <u> with
no attribute that closes, an inline <br> and a qm-* element that closes
report nothing, and neither does a closing tag or a qm-anchor tag unless the block it
opens drops markdown with it, which reports under that tag. A qm-* element
left unclosed, self-closing, inside a line of text, or beside another tag in a
block tight against markdown reports as qm-<name>. A <qm-table> wrapper around one table folds its attributes
into the table and reports nothing; one wrapping anything else reports as
qm-table, and an attribute it cannot fold as qm-table[<attr>]. A
<qm-cell> pair around a table cell's whole content folds the same way into
the cell; one around part of its cell reports as qm-cell. A footnote
definition reports as footnote. A
parse anchors each at the body it sat in (main.body,
cards.<kind>[<i>].body) on Parsed.warnings, which is doc.warnings in both
bindings, and the CLI prints it. A richtext field's markdown in the card-yaml
block reports at the field (main.subject): from the conform Quill::parse
runs, from Quill::validate on a document loaded without its quill, and from
a revise of the field.
A host that fails on any warning, or counts them, meets a new entry for every
document holding a raw tag outside the allowlist. quillmark validate fails a
quill whose example.md body holds a raw tag, as cli::example_not_clean;
drop the markup from the example.
A one-shot render carries the render's warnings alone¶
0.123's WASM engine.render(quill, doc) and Python Quillmark.render put
doc.warnings, the load's parse and conform::* warnings, ahead of
RenderResult.warnings. They no longer do: a render carries the
quill.validate(doc) warnings but validation::declined_construct, then the
compile's, as Rust's Quillmark::render does. doc.warnings reports the load,
and a revise or a store can change what the document holds after it, so a
render after a revise kept reporting a tag the revise had removed and never the
ones it dropped. A host that showed the load's warnings through the render
reads doc.warnings after the load, and a revise's warnings after each
revise. The CLI's render parses and renders in one command, and still prints
its parse's warnings first.
A JSON null crosses to JavaScript as null¶
In WASM, a null inside a value a call returns crosses as null, where
0.123 crossed it as undefined. That covers a stored field value and
anything nested in one (doc.getStored, reader.get, doc.cards,
removeField), $ext and seed overlays, and a diagnostic's args, a thrown
error's diagnostics included. A present-null field reads null, and an
absent one still reads undefined. A check for a missing value that reads
=== undefined misses a null; == null takes both.
An import and a revise return their warnings¶
quillmark_content::import::from_markdown returns
Imported { content, warnings }. .content is 0.123's Normalized, and
warnings a Vec<ImportWarning> holding one
ImportWarning::DroppedConstruct { construct, count } per construct, where
construct is a Dropped saying what dropped and its Display is the name
the diagnostic reports.
quillmark_content::delta::diff_import returns
(Normalized, Delta, Vec<ImportWarning>).
quillmark_core::document::dropped_construct turns one into the diagnostic.
A revise returns a Revised { delta, warnings } receipt, where it returned
the Delta. Card::revise_body, revise_field and revise_field_checked,
their CardMut twins, and the typed TypedWriter::revise_body /
revise_field and CardWriter::revise_body / revise_field return
Result<Revised, EditError>, #[must_use]. A Card verb does not know its
card's address, so its warnings carry no path; the typed writer anchors them
at the body or field, and Revised::with_path does for any caller that knows
the address. TypedWriter::add_card returns the body's warnings,
Result<Vec<Diagnostic>, _>, where it returned (). quillmark::Revised
re-exports the receipt.
let delta = card.revise_body(md)?; // 0.123
let delta = card.revise_body(md)?.delta; // 0.124; .warnings holds what the import dropped
In WASM, importMarkdown, rebase and every revise return the warnings
beside the value. importMarkdown(md) returns { content, warnings }
(Imported), where it returned the Content, so the cold write takes
.content. rebase(base, md) returns { content, delta, warnings }.
doc.revise(addr, md), writer.reviseBody, writer.reviseField and their
writer.card(i) twins return { delta, warnings } (Revised), where they
returned the Delta, with each warning's path the address written.
writer.addCard returns the body's warnings, where it returned undefined.
The warnings of importMarkdown and rebase carry no path: there is no
document.
doc.overwrite(addr, importMarkdown(md)); // 0.123; 0.124 throws, the argument being no Content
doc.overwrite(addr, importMarkdown(md).content); // 0.124
const delta = doc.revise(addr, md); // 0.123
const { delta, warnings } = doc.revise(addr, md); // 0.124
In Python, Writer.revise_body, revise_field and add_card return the
import's warnings as list[Diagnostic], where they returned None. Each
anchors at the body or field written. The Delta stays behind, as before.
validate reports what the backend will not draw¶
Quill::validate warns validation::declined_construct at a content field
holding a construct the quill's backend draws nothing for. One per field and
construct, Severity::Warning, with args.construct and args.count and the
field's path: main.body, main.<field>, cards.<kind>[<i>].body. The
Typst backend declines image, so a markdown image in a body or richtext
field now warns at validate, where only the render reported it, and so
does a richtext default the document leaves unset. The
construct values are those backend::declined_construct carries. WASM and
Python quill.validate(doc) return it, quillmark check prints it and exits
1 on it under --strict, and quillmark validate fails a quill whose
example.md holds an image, as cli::example_not_clean. A host that gates on
any validate diagnostic meets it; route on the code. A one-shot render's
RenderResult.warnings leaves it out and carries the backend's
backend::declined_construct, as before.
The acroform backend declines every construct but the paragraph. A
heading, rule, code block, list, quote, table or image in an acroform quill's
content warns validation::declined_construct at validate and
backend::declined_construct on the session's warnings and
RenderResult.warnings, where an acroform render warned nothing. The field
still binds its plaintext, as before.
A qm-table wrapper lays out its table¶
A Typst quill lays out the qm-table attributes widths and align,
where 0.123 dropped the tags.
0.123 rendered this table auto-fit, at the plate's placement; 0.124 weights
the columns and centers the table. A <qm-keep> around the wrapper keeps
the table on one page. A document whose markdown already holds such tags
renders the layout they spell.
A bare headless attribute, <qm-table headless>, draws the first row as a
body row, with no table.header. A plate that styles the header row with
show table.cell.where(y: 0) styles a headless table's first row too, since
Typst numbers it row 0 either way; condition the rule on the table holding a
table.header, as the Typst backend guide
shows. The acroform backend typesets no table and
declines it as before.
A qm-cell pair around a table cell's whole content aligns that one cell,
where 0.123 dropped the tags.
| Item | Notes | Qty |
| --- | --- | ---: |
| <qm-cell valign="bottom">Total</qm-cell> | one<br>two | <qm-cell align="center">**42**</qm-cell> |
align is left, center or right, and valign is top, middle or
bottom; the cell stores each as its own key, and a Typst quill lowers them to
table.cell(align: ..). A stored cell's align or valign in its set, a key
0.123 carried without reading, renders the alignment it holds.
A qm-* element is stored, and renders through the plate¶
A qm-<name> element is content, where 0.123 dropped its tags. A pair
of tag lines wraps the blocks between them and keeps its attributes as
strings. A line holding only the tags of elements and qm-table is a tag line
wherever it stands outside a heading, a mark or a link, tight against markdown
or several tags to a line, in the list item or quote CommonMark puts it in.
Export writes each tag on a line of its own with a blank line above and below,
and an element around nothing as its pair on one line.
0.123 dropped each tag and rendered what the pair wraps; 0.124 stores the
element. One still open where its list item, quote or body ends, a
self-closing tag and a tag inside a line of text drop, warning
parse::dropped_construct as qm-<name>, what they wrap importing. An
attribute outside the carrier grammar drops alone, warning
qm-<name>[<attr>]. qm-table and qm-anchor keep their meaning.
A Typst quill renders an element through the renderer its plate registers
under the element's name, and draws what any other wraps. The renderer lives
in the helper's elements state and takes the element's attributes, each the
string written, and its content:
#import "@local/quillmark-helper:0.1.0": data, elements
#elements.update(e => e + (boxed: (attrs, body) => block(stroke: 0.5pt, inset: 6pt, body)))
The dispatcher reads the registry's final value, so the update may stand
anywhere in the plate, after the content it renders included. An element whose
name the plate registers no renderer under, keep aside, warns
typst::unregistered_element at its field. The hint names the elements the
quill renders, so a misspelled qm-kep shows up there.
An element around nothing is its pair on one line. <qm-sig></qm-sig>
is an element holding an empty paragraph, and export writes it so; a body
holding only that is not blank, and its plate's renderer draws it.
<qm-sig/> drops, warning qm-sig, since HTML reads a self-closing
custom element as an open tag.
The built-in keep holds its content on one page on every Typst quill, so a
<qm-keep> pair a 0.123 document already holds keeps its run together where
0.123 let a page break split it.
The content types gain an element member. In Rust, Container::Element
{ name, attrs, instance }. In WASM, ContentContainer gains { container:
"element"; attrs: ElementAttrs; instance?: number }, where ElementAttrs is
{ $name: string; [attr: string]: string }: the element's name sits in the bag
under $name, beside its attributes.
A whole-document markdown write keeps anchors¶
writer.reviseDocument(md) in WASM, writer.revise_document(md) in Python
and TypedWriter::revise_document in Rust replace a document with markdown and
keep what the markdown cannot spell where they can match it. A door that
cold-imports each markdown write, such as Document.fromMarkdown then a store,
drops every comment anchor on each save; route the write through
reviseDocument instead. Composable cards align to the stored ones by $kind
and text, an aligned card's body and content fields rebase their anchors and
keep their island ids, a card's $ext carries over where it aligns, and the
receipt names each anchor the write dropped. doc.toAnnotatedMarkdown() in
WASM and Document::to_markdown_annotated in Rust return the markdown with a
read-only <qm-anchor ref="…"></qm-anchor> at each anchor, and the
anchors listed, so a writer sees where they sit; a revise reads no tag.
Every revise keeps the island ids it can match, where 0.123 re-minted each
by position. A table or image a revise leaves unchanged, or edits in its
place, keeps its id, and one it inserts takes the next isl-{n} past the
field's highest, so an IslandOp::Set a host holds still names the island it
named. That covers revise, rebase, diff_import, every writer revise*
verb and the whole-document revise.
Every break¶
| Break | Surface | Action |
|---|---|---|
A </u> closes the innermost open <u>, one carrying an attribute included, and never a **; a <u> left open where its text ends drops and warns parse::dropped_construct (u), where it underlined the rest of the body |
rendered output, imported content, Parsed.warnings, doc.warnings |
Close each <u> in the text it opens in |
A <u> tag exports outside the **, _ and ~~ delimiters at its position, and an underline crossing one of them as one pair the delimiters cross |
Document::to_markdown, toMarkdown, to_markdown |
None |
| A line holding only tags after a table's rows ends the table, where a type 7 tag added an empty row | rendered output, imported content | None |
| A paragraph whose first line holds only tags that drop opens without a leading space | imported content | None |
A fenced block holding <!-- a --> b keeps its line |
rendered output, imported content | None |
| Text after a block nested in a tight list item opens its own line, where it joined the block's last line | rendered output, imported content | None |
A raw tag in a body warns parse::dropped_construct at the body, and one in a richtext field's card-yaml value warns at the field from quill.parse and quill.validate; quillmark validate fails a quill whose example.md holds one, as cli::example_not_clean |
Parsed.warnings, doc.warnings, CLI |
Drop the markup, or let the warning stand |
A one-shot render no longer carries doc.warnings (parse and conform::*) ahead of its own |
WASM engine.render, Python Quillmark.render, RenderResult.warnings |
Read doc.warnings after a load, and a revise's warnings after a revise |
A JSON null inside a returned value crosses as null, where it crossed as undefined |
WASM field reads, doc.cards, $ext, seed overlays, diagnostics' args |
Test == null where a check read === undefined |
from_markdown returns Imported; diff_import returns the warnings third |
quillmark_content |
Take .content; bind the third element |
A revise returns Revised { delta, warnings }, #[must_use]; TypedWriter::add_card returns Vec<Diagnostic> |
Rust Card, CardMut, TypedWriter, CardWriter |
Take .delta; read or bind .warnings |
importMarkdown returns { content, warnings }; rebase gains warnings; revise, reviseBody and reviseField return { delta, warnings }; addCard returns Diagnostic[] |
WASM | Take .content and .delta |
revise_body, revise_field and add_card return list[Diagnostic] |
Python | None |
Quill::validate warns validation::declined_construct at a field holding a construct the backend declines: an image under Typst, anything but a paragraph under acroform; quillmark validate fails an example.md holding one |
Quill::validate, WASM and Python validate, quillmark check, quillmark validate |
Route on the code, or drop the construct |
An acroform render warns backend::declined_construct for a heading, rule, code block, list, quote, table or image in a content field |
acroform session warnings, RenderResult.warnings |
None |
A Typst quill lays out a qm-table wrapper's widths, align and headless, where the tags dropped |
rendered output | A plate styling table.cell.where(y: 0) conditions it on a table.header |
A Typst quill aligns a table cell a qm-cell pair wraps whole, and a stored cell holding align or valign in its set, where the tags dropped and the keys rode unread |
rendered output, imported content | None |
A \| in a table cell's code span or link destination exports escaped, as in the cell's text, where it split the cell on re-import |
Document::to_markdown, toMarkdown, to_markdown |
None |
A qm-* element imports as stored content, where its tags dropped, and a line holding only element and qm-table tags is a tag line wherever it stands |
imported content | None |
A Typst quill renders an element through the renderer its plate registers under the name, called (attrs, body), and the built-in keep holds its run on one page, where the tags dropped |
rendered output | Register a renderer in the helper's elements state, or drop the tags |
A Typst render warns typst::unregistered_element at a field holding an element whose name the plate registers no renderer under, keep aside; quillmark validate fails a quill whose example.md holds one, as cli::example_not_clean |
Typst session warnings, RenderResult.warnings, CLI |
Fix the name, or register a renderer: (attrs, body) => body draws it plain |
| A body holding only an element around nothing is not blank, and renders | Content::is_blank, rendered output |
None |
A task list marker renders a box, ticked for [x], and imports as the list item's checked, where it was text |
rendered output, imported content | Escape the bracket, \[ ], to keep the text |
A footnote definition drops with its note and warns parse::dropped_construct (footnote), and its reference stays text, where the definition defined a link silently |
rendered output, imported content, Parsed.warnings, doc.warnings |
Put the note in the prose |
Container gains Element, and Invariant gains BadElement for one outside the carrier grammar; Container::ListItem gains checked, and Dropped gains Footnote; StoredDocument gains V0_124_0, and STORAGE_V0_124_0 is the current tag |
Rust | An exhaustive match adds the arms; a ListItem literal sets checked: None |
ContentContainer gains an element member, and a list_item's attrs an optional checked |
WASM | A switch over container handles element |
A save writes quillmark/document@0.124.0, and a 0.123 build refuses the row from then on |
stored documents | None to load; a hash over stored bytes recomputes |
| A revise keeps the id of each island it leaves unchanged or edits in place, and mints past the field's highest, where it re-minted every id by position | revise, rebase, diff_import, the writer's revise* verbs |
None |
An empty own-line comment emits as #, where it emitted # |
Document::to_markdown, toMarkdown, to_markdown |
None |
Text other than comments after a card-yaml block's root value, such as b: 2 under a {$kind: note, a: 1} line or anything under a ... line, fails the parse as parse::yaml_error_with_location at that text, where it dropped |
Document::parse, Document.fromMarkdown, Document.from_markdown, quill.parse |
Write each field as its own key: value line |
quillmark validate refuses a typst.plate_file with the render's own diagnostic, typst::plate_missing, typst::plate_path_invalid or typst::invalid_utf8, where it raised cli::plate_file_missing or cli::plate_file_escapes_quill |
quillmark validate |
Route on the typst::plate_* codes |
A . or empty step in typst.plate_file drops, so tpl/./layout.typ is named tpl/layout.typ |
diagnostics' location.file, quillmark workspace |
None |
A tag inside a merge's value warns parse::unsupported_yaml_tag at the path the value holds it at, main.m.x for m: {<<: {x: !t 1}}, where it named the << |
Parsed.warnings, doc.warnings |
Match the warning's path without the << |
A mapping holding two keys of one text, such as 1 and "1", fails the parse as parse::yaml_error_with_location at the second, where the second's value replaced the first's |
Document::parse, Document.fromMarkdown, Document.from_markdown, quill.parse |
Keep one of the two keys |
A base PDF whose trailer names /XRefStm, or whose /Prev chain reaches an xref stream (a hybrid file), fails as pdf::xref_stream, where its stream-held objects were invisible to the stamp |
acroform quills' form.pdf, quillmark_pdf::stamp, quillmark_pdf::page_canvas_boxes |
Save the base without cross-reference streams |
A base PDF's /Kids that is no array, or a /Kids element that is no reference, fails as pdf::parse, and an annotation dictionary the reader cannot read fails as pdf::stamp_parse, where the read stopped or skipped it; a name is compared through its #xx escapes, so a key spelled two ways is a repeated key |
acroform quills' form.pdf, quillmark_pdf::stamp, quillmark_pdf::page_canvas_boxes |
Write each key once, and /Kids as an array of references |
An options object, Addr or CardInput inheriting a key its verb does not read from a null-prototype object throws unknown key, where the key was ignored |
engine.render, session.render, new Engine, an Addr, insertCard |
Drop the key from the object inherited from |