0.130 → 0.131 — font family in richtext, a host font library, and tables as pipes or HTML¶
A writer sets a run of richtext in a font family, spelled in markdown as a
qm-span pair, and an app hands the engine a font library of its own to
choose families from. A table writes as a GFM pipe table or as HTML, which
spells merged cells, and its layout leaves the qm-* carrier: qm-table
and qm-cell no longer spell a table's layout, so markdown 0.130 exported
with either loses the table's widths, placement, header row setting and cell
alignment when it is read back. The sections up to Tables are the changes no
type checker reports; the table closing the guide lists every break.
Stored rows¶
A row holding a font_family mark, an element named table, a merged
table cell or a table head 0.130 cannot draw saves under
quillmark/document@0.131.0, the first tag that can hold them: 0.130 refuses
the element as corrupt and draws the rest wrong. Every other row saves under
@0.129.0 as before, so a 0.130 build reads it, byte for byte but for the
one-time settles below. A 0.130 build refuses a row under the
new tag, as a build too old for it, so deploy every reader before a writer
saves one, and a writer rolled back to 0.130 cannot open the rows 0.131 saved
under it.
A body mark spanning a line break loads as one mark per line. Normalization cuts a formatting mark at each \n, the form markdown export writes, so **a\⏎b** imports as two marks, a link over a break as two links with one url, and a MarkOp::Add across lines reads back split. A stored row holding such a mark saves to new bytes on its first load, so a hash over it recomputes once. A table cell's or footnote's marks still span its <br>.
A character holds at most one link. link is keyed like font_family: stored overlapping links settle to one url per character, the later-starting one winning, and a MarkOp::Add of a link over a link replaces its url there. Markdown could already write only one, so exported markdown is unchanged.
A table's header rows are head, a count, and headless: true sits beside head: 0, so a 0.130 build, which reads headless alone, draws no header row there. A stored headless: true normalizes to head: 0 beside it: the row loads as before and rewrites once when saved, so a hash over its stored bytes recomputes. A richtext field value holding such a table reads as content, exporting as a table and revising from markdown as one.
A table whose grid pads past MAX_TABLE_CELLS slots is refused as Invariant::TableTooLarge, on load, on overwrite and on an island op, before the mint pads it.
A cell's colspan and rowspan span it, where 0.130 carried them as keys it did not name. Normalization settles each to what it can cover: empty cells no other span covers, inside the grid and its head or body section. A span past that stores shorter, and a covered cell's colspan and rowspan drop, so a host that stored either key under another meaning reads it rewritten, and the row rewrites once when saved, under @0.131.0 where a span still covers more than one slot. A covered cell keeps its align and valign, which it draws again once no span covers it, though markdown, which spells no covered cell, drops them. A cell holding a mark, a comment's anchor included, is no placeholder a span covers.
Document.currentStorageVersion() (WASM) and
Document.current_storage_version() (Python) return the newest tag,
@0.131.0, though most rows still save as @0.129.0. A check that treats any
other tag as a version mismatch now flags rows this build wrote: let
fromStored decide, and read the tag only once it throws.
Markdown¶
<qm-span family="Carlito">text</qm-span> inside a line sets its text's
family, where 0.130 dropped the tags and warned parse::dropped_construct
(qm-span). Markdown that spelled one now imports with a font mark and
renders in that family. Pairs nest, and the outer family resumes past an inner
pair. A character holds one family, so export writes nested pairs back side
by side, and a MarkOp::Add of a family replaces the family under its range.
An attribute other than family drops alone and warns
qm-span[<name>]. A tag setting no family, or a pair left open, drops and
warns qm-span as before, so a family value no family takes, empty, spaced
at an edge or holding a control or bidi character, warns twice:
qm-span[family] and qm-span. A qm-span tag line, which 0.130 read as an
element named span around the blocks below it, drops and warns qm-span, and
the blocks stay.
Markdown Syntax has the full rules.
Rendering¶
A Typst render sets a font run's family ahead of the font list in force, so a
character the family lacks falls through to the surrounding font. A family the
host library lacks keeps the surrounding font and warns
typst::unknown_font_family once per family and field, at the field's path,
on every compile. Code spans and equations keep their own font. An acroform
render ignores the mark.
An app adds fonts through the engine: Engine::with_fonts(files), the
fonts option of new Engine(..) in JavaScript, Engine(fonts=..) in
Python, and --font on quillmark render and quillmark workspace. The
embedded Roboto still loads only for a quill shipping no fonts. A file other
than .ttf, .otf, .ttc or .otc fails the engine with
engine::font_unsupported, and one holding no face Typst reads with
engine::font_unreadable. A font run sets only a family the library holds: a
quill's own fonts and the embedded Roboto set the plate, never a run. A
library face sets only marked text: a plate naming its family, a missing bold
or italic, and glyph fallback never reach it, so a document without font
marks renders as it did on 0.130.
engine.fontFaces(quill) (WASM), Engine.font_faces(quill) (Python) and
Engine::font_faces (Rust) list the library's faces, with their bytes. A
Rust app on the quillmark facade reaches the library as
quillmark::FontLibrary and the workspace writer as
quillmark::typst_workspace::workspace, both behind the typst feature.
Tables¶
qm-table and qm-cell¶
<qm-table widths="2 1"> around a pipe table is an ordinary element named
table around a bare table, which a quill renders as if the tags were absent,
warning typst::unregistered_element; <qm-cell align="right">42</qm-cell>
in a cell imports 42 and warns qm-cell. A host that keeps documents as
markdown converts
each table holding either: import it on 0.130, which still reads the carrier,
and export the content on 0.131, which writes HTML, or rewrite it by hand.
0.130 drops an HTML table, so the rewritten markdown needs 0.131:
<table widths="2 1" align="center">
<thead>
<tr><th>Item</th><th>Qty</th></tr>
</thead>
<tbody>
<tr><td>Total</td><td align="right">42</td></tr>
</tbody>
</table>
A headless wrapper is a table with no <thead>, its rows <td> cells in a
<tbody>. A pipe table's delimiter row spells its column alignment, which
HTML spells aligns="none right" on <table>. Loading a stored row on 0.131
and exporting it writes this spelling for every table that needs it.
Export¶
to_markdown writes an HTML table for a table holding widths, a
placement, a cell's align or valign, a span, or more or fewer than one
header row, where 0.130 wrote a pipe table inside a qm-table wrapper with
qm-cell pairs. Every other table writes as pipes, as before, and a <table>
holding nothing pipes cannot write re-exports as pipes. A consumer reading
exported markdown with its own parser reads an HTML block for such a table:
GitHub draws its grid and spans and shows the markdown in its cells as
written.
Import¶
An HTML block opening <table> imports as a table, where 0.130 dropped
it and warned each of its tags, table, tr and td among them. Markdown holding a raw HTML table renders it now.
What the subset does not hold warns parse::dropped_construct:
tablefor a table its block ends before its</table>: a blank line inside a table ends it, and the rows after it drop as an HTML block of their own, warning their tags;table:rowfor a row too short for the table, which is as wide as its widest row;table:textfor text outside every cell, and a tag's name, such ascaption, for any other tag;table:sizefor the rows pastMAX_TABLE_CELLS(100 000) slots, rows times columns, which drop, a pipe table's as an HTML table's;<tag>[<attr>], such astd[nowrap]ortable[widths], for an attribute the subset does not read or a value outside its spelling;border,class,scopeandstyledrop without a warning.
What follows a </table> in its block, on its line or the next, reads as
markdown of its own, so prose written tight under a table imports.
qm-table[<attr>] and qm-cell[<attr>] no longer occur.
Fixes that change output¶
An acroform render binds a richtext array element holding no text as an
empty line, where 0.130 skipped it, so a blank line between groups reaches
the widget. A list item with nothing after its marker ends at the blank line
below it, so a code fence after that blank line opens outside the item, and
a card fence inside that code block stays code, where 0.130 registered the
card.
Every break¶
| Break | Surface | Action |
|---|---|---|
A row holding vocabulary @0.129.0 lacks (§ Stored rows) saves as quillmark/document@0.131.0, which a 0.130 build refuses |
stored documents | Ship readers before writers |
currentStorageVersion() / current_storage_version() return quillmark/document@0.131.0, the tag a row holding vocabulary @0.129.0 lacks saves under |
WASM, Python | Stop comparing a row's tag to it; catch fromStored instead |
An inline qm-span pair imports as a font_family mark, where it dropped and warned qm-span |
imported content, rendered output, Parsed.warnings |
None |
A qm-span tag line drops and warns qm-span, where it opened an element named span |
imported content, Parsed.warnings |
Set the family with an inline pair inside each line |
parse::dropped_construct names qm-span[<attr>] for an attribute a qm-span pair cannot read |
Parsed.warnings, doc.warnings, revise warnings |
Drop the attribute, or let the warning stand |
A font run in a family the host library lacks warns typst::unknown_font_family |
Typst session warnings, RenderResult.warnings |
Supply the family, or treat the code as an error where a hard failure is wanted |
MarkKind gains FontFamily { name }; StoredDocument gains V0_131_0, and STORAGE_V0_131_0 is the tag a row holding vocabulary @0.129.0 lacks saves under |
Rust | An exhaustive match adds the arm |
ContentMark gains a font_family member, and the content dicts Reader.get_content / get_content_at return hold font_family marks |
WASM, Python | A switch over type handles it |
TypstBackend is a struct holding a font library: the unit value TypstBackend is TypstBackend::default(), or TypstBackend::new(Arc<FontLibrary>) |
Rust | Construct it |
quillmark_typst::workspace::workspace, re-exported as quillmark::typst_workspace::workspace, takes a &FontLibrary third |
Rust | Pass &FontLibrary::default() for no library |
A refused font in new Engine({ fonts }) rejects every verb that loads the render build, acroform renders included, with the same error each time; the fonts are parsed once |
WASM | Catch the rejection on each verb, or build a new engine without the font |
quillmark workspace --font writes fonts/host-<n>.<ext> beside the quill's fonts |
CLI | None |
qm-table is an ordinary element and a qm-cell pair drops, warning qm-cell, so the table imports without their layout |
imported content, Parsed.warnings, doc.warnings, revise warnings |
Rewrite the table as HTML |
| A table holding layout, alignment, a span or other than one header row exports as an HTML table | exported markdown | None; a consumer parsing the markdown reads an HTML block |
An HTML block opening <table> imports as a table, where it dropped |
rendered output, imported content | None |
An HTML table warns table, table:row, table:text and <tag>[<attr>], and a table past the cell bound table:size; qm-table[<attr>] and qm-cell[<attr>] are gone |
Parsed.warnings, doc.warnings, revise warnings |
Match the new constructs |
A stored table past MAX_TABLE_CELLS slots is refused as Invariant::TableTooLarge |
fromStored, overwrite, island ops |
Split the table |
A stored headless: true gains head: 0 beside it |
stored documents | None to load; a hash over stored bytes recomputes |
A body formatting mark spanning a \n normalizes to one mark per line, a link to one link per line |
stored documents, imported content, Content.marks |
None to load; a hash over stored bytes recomputes, and a reader expecting one mark over a break reads several |
link is keyed: overlapping links settle to one url per character, and a MarkOp::Add of a link replaces the link under its range; MarkKind::is_keyed holds for Link |
stored documents, Content.marks, mark ops, Rust |
None to load; a hash over stored bytes recomputes |
A cell's colspan and rowspan span it and normalize to what they can cover; a covered cell's span keys drop |
stored documents, rendered output | Store a span only where the editor merges cells; a hash over stored bytes recomputes |
The Typst backend draws every header row in table.header and a span through table.cell(colspan:, rowspan:), leaving covered cells out |
Typst rendered output | A plate addressing cells by x and y addresses a merged cell by its first slot |
TableLayout.headless is head: usize; TABLE_KEYS names head; carrier::CELL and carrier::TABLE are gone and RESERVED holds anchor alone; Dropped gains Table, TableRow, TableText, TableSize and TableAttr; Invariant gains TableTooLarge; MAX_TABLE_CELLS is new |
Rust | Read head == 0; an exhaustive match adds the arms |
TableProps.headless is head?: number, and TableCell gains colspan? and rowspan?; the content dicts Reader.get_content / get_content_at return carry the same keys |
WASM, Python | Read head === 0, or head == 0 in Python |