Postext reads a deliberately small markdown dialect.
The parser ships as a hand-written tokenizer — not a full CommonMark implementation — so the source format is narrow and predictable. The intent is twofold: keep the engine small and fast, and make documents trivially portable between Postext and any other CommonMark reader (Obsidian, Pandoc, VS Code…). Anything not listed on this page is either treated as plain text or removed from the inline stream.
If you are building a document programmatically, the parseMarkdown function (see Configuration › Parsing) gives you the exact block structure the layout engine consumes.
#Frontmatter
A document may begin with an optional YAML frontmatter block fenced by --- markers:
---
title: Chapter One
author: Jane Doe
publishDate: 2026-04-15
---
# Chapter One
The story begins here…Call extractFrontmatter(source) to split the frontmatter from the body. The parsed metadata object is returned alongside the remaining markdown and the character offset where the body begins — useful if you need to map errors or cursor positions back to the original source.
Frontmatter is parsed with gray-matter, so any shape of YAML is accepted. Postext itself only looks at title, subtitle, author, and publishDate; additional keys are preserved on PostextContent.metadata and are yours to use.
#Block constructs
Postext recognises seven prose block types, plus the directives and resource embeds covered later on this page. Blocks are always terminated by a blank line or by the start of another block.
| Construct | Syntax | Notes |
|---|---|---|
| Heading | # Title … ###### H6 | One to six # characters followed by a space and the heading text. Levels 1–6 map directly to the headings.levels config. |
| Paragraph | Plain text over one or more lines | Consecutive non-blank, non-special lines are joined with a single space and emitted as one paragraph. Manual line breaks inside a paragraph are not preserved — use a blank line to start a new paragraph. |
| Blockquote | > quoted text | Every line of the quote must start with > (one optional space after). Consecutive quote lines merge into a single blockquote block. |
| Unordered list | - item, * item, + item | Any of the three bullet markers is accepted. Nesting uses exactly two spaces per level, up to a maximum depth of 5. |
| Ordered list | 1. item, 2) item | Digits followed by . or ). The start number is preserved (so a list can begin at 5, or at 0). The separator rendered in the output comes from orderedLists.separator, not from the source. |
| Task list (GFM) | - [ ] todo, - [x] done | An unordered item with a bracketed checkbox. Accepts lowercase x or uppercase X. Rendered with the taskCheckboxChar / taskCheckedChar glyphs. |
| Display math | $$ … $$ | A LaTeX formula set as its own block. Rendered centred on the column, snapped to the baseline grid like a heading, and kept vectorial in the PDF output. The single-line and fenced multi-line forms are described under Mathematical formulas. |
A single blank line between two list items is tolerated — the list stays together. Two or more blank lines terminate the list.
Lists of mixed kinds at the same depth are accepted (you can switch from unordered to ordered mid-run), but the engine treats the runs as separate for numbering purposes. In practice, keep one kind per depth unless you have a reason to mix them.
#Heading attributes
A heading line may end with an attribute block in braces — the same key="value" syntax directives use:
# The Long Road {author="I. Zango Martín" year=1998}The braces and their content are removed from the heading text (the title above renders as The Long Road) and stored on the heading as attrs. They are exposed to design slots as {attr.<key>} placeholders: in the heading's own advanced-design slot, and in page headers and footers, where they resolve from the current chapter's H1. Only a balanced, brace-free block at the very end of the line is recognised; a lone {} or an unbalanced brace stays in the text. See Configuration → Headers & footers.
Two attributes have a meaning of their own. style="<id>" applies a named heading style to the heading — a preface or an authors list with its own opener design, running heads, page geometry and body typography, and no chapter number when the style says numbered: false. toc="false" (or "true") overrides whether the heading is listed by :::toc:
# Preface {style="front-matter"}
# Contents {style="front-matter" toc="false"}#Directives
Directives are single-line control tags written as :::name or :::name{attrs} on their own line. They produce no visible output — they drive the placement and numbering pipeline.
| Syntax | Effect |
|---|---|
:::pagebreak | Force the next block to open on a new page. |
:::pagebreak{parity="odd"} | Same, plus ensure the new page is odd (right-hand). Inserts a blank padding page when needed. |
:::pagebreak{parity="even"} | Same, but targeting an even (left-hand) page. |
:::pagebreak{parity="always-odd"} | Guarantee at least one mandatory blank separator page before landing on an odd page. The separator blank belongs to the preceding content; any further parity padding belongs to what follows. Useful when every chapter must start on a fresh spread. |
:::pagebreak{parity="always-even"} | Same, but targeting an even page. |
:::numbering{format="decimal" startAt=1} | At the next page boundary, switch the page-numbering sequence. Both attributes are optional — omit format to keep the format, omit startAt to continue the counter. |
:::columnbreak | End the current column here: the next block opens in the next column of the same page (or on a new page when the directive lands in the last column). A no-op in an empty column, so it never produces a blank column or page. The column it ends keeps its bottom gap — column balancing does not stretch it. |
:::toc | Print the table of contents here: one entry per heading of the listed levels (title, number, page number, optionally the chapter authors) and one row per part divider, typeset per the toc configuration. The entries follow the document — rename, move or renumber a chapter and the contents follow. |
Attribute values may be double-quoted ("…"), single-quoted ('…'), or bare (startAt=17). A bare key without = is treated as a present-but-empty flag.
Only pagebreak, numbering, columnbreak and toc are recognized as single-line directives today — any other :::name line that is not a container (below) is parsed as a paragraph and surfaces an Unknown directive warning in the sandbox.
#Containers
A container wraps a run of blocks in a fence: an opening line :::name or :::name{attrs}, then any ordinary content — paragraphs, headings, lists, blockquotes, formulas, even other directives — and a closing line holding a bare :::. Containers may nest; each closing ::: closes the innermost open one.
:::callout{type="note"}
Keep the lantern lit **every** night.
- Check the wick.
- Trim it at dusk.
:::
Three container names are recognized. What each one renders is set in its own section of the configuration:
| Syntax | Effect |
|---|---|
:::callout{…} … ::: | Boxed content — a note, tip or warning set apart from the body in a bordered or tinted box. |
:::paragraphs{…} … ::: | A run of paragraphs typeset with a named paragraph style (a lead-in, an epigraph, a set of small-print notes) instead of the body style. |
:::part{…} … ::: | A part or section opener: the enclosed heading and prose form the opener page of a major division. |
The attributes accepted by each container, and how it is styled, are documented in Configuration. Attribute values follow the same grammar as directives. A :::callout fence takes type (the id of a configured callout style — unknown or missing types fall back to the first style), title (overrides the style's default title), and span / placement (column, page or side; here, top, bottom or fixed) to override the style's extent and position for that box:
:::callout{type="objectives" title="What you will learn" span="page" placement="top"}
- Name the parts of the lantern.
- Trim the wick without touching the glass.
:::
A callout is laid out as a box — an optional title, then its content typeset with the style's own body and list typography. It keeps together by default, moving whole to the next column or page when it does not fit (a box taller than a whole column splits all the same, rather than overflow); a style with keepTogether: false lets it split between its blocks, or between lines, leaving at least splitMinLines text lines — or a figure, table, display formula or nested box — on each side of the cut. Full-width boxes (span="page") cut the page into column bands; span="side" boxes leave the flow into the float-only side column of a one-and-a-half layout (layout.sideColumnRole: 'floats'), stacked beside the text they interrupt, and lay out as column boxes where there is no such column; placement="fixed" boxes leave the flow and are pinned to page coordinates (a self-assessment badge in the bottom-left corner of a chapter's last page, say), the columns they cover giving up that zone; floating boxes (placement="top" / "bottom") leave the flow where they occur and take the first free band after that position — the foot of the page, or the head or foot of the next — while the text after them fills the page they left. A style with floatBarrier: true (a chapter's closing "key points" box, typically) makes the box a float barrier: every figure or table referenced before it is placed before it — in the page's free slots, or on pages opened ahead of the box — so no float escapes past the end of its chapter. An unknown :::name fence is not a container — the line is treated as text, exactly like an unknown directive.
A :::part fence takes number (as you want it printed — "I", "IV", "3"; it is also parsed so the design can re-format it) and title; both are optional. A third attribute, palette="band=#hex" (several id=#hex pairs, comma-separated), recolours every design colour linked to those palette ids — running heads, opener band, part designs — and the text-flow colours that share their base value (headings, bold, references, bullets, captions) for the part and the chapters that follow it, until the next part; see Configuration › Parts. The container always opens a page of its own: a page break of the configured parity before it, a single body column at the parts.margins, the opener design over the whole page, and another page break after the closing fence. The body — usually the list of the chapters the part groups, or nothing at all — is typeset with parts.bodyStyle:
:::part{number="I" title="Foundations"}
1. The lantern and its parts
2. Trimming the wick
:::
# The lantern and its parts
With the default heading settings this produces the classic sequence: part page on a right-hand page, blank left-hand page, chapter on the next right-hand page. The page is reported as role: 'part' so headers and footers can skip it, and {partTitle} / {partNumber} resolve to the current part on every following page. See Parts in the configuration reference.
A fence does not need a blank line before it: an opening or closing fence glued directly under a paragraph, list or blockquote ends that block. A container left open at the end of the document is closed automatically there, and the sandbox reports an Unclosed container warning pointing at the opening line. A stray ::: with no container open is left in the text as a visible paragraph rather than dropped silently.
#:::columns
Inside a :::callout, a :::columns{count=2} … ::: group sets the blocks between its fences in count columns of equal width (the style's columnGap apart): the run is cut where the columns level best — between blocks, or between the lines of a paragraph or list item, whose tail goes on at the head of the next column without its bullet — and the box grows to the tallest column. Blocks after the group take the full width again. Outside a callout the fences are ignored and the blocks flow as usual. A breaks attribute fixes the column starts instead of balancing: :::columns{count=2 breaks="4"} opens the second column at the group's fourth block (a comma list for more columns), with no cut inside a paragraph — a text column beside a figure column.
:::callout{type="summary"}
:::columns{count=2}
- Every element is one kind of atom.
- Electrons live in orbitals.
- A bond shares or transfers electrons.
:::
:::A :::callout fence also accepts label="…": the text a style with a label tab prints on the box's top corner (:::callout{type="box" label="BOX 1-1" title="The octet rule"}).
A :::callout inside another one is a box of its own — a worksheet card holding answer boxes, say. It takes its own style (background, border, radius, padding, title, icon) at the full inner width of the outer box and stacks among its other blocks; its span and placement are ignored, since a nested box always flows inside its parent. Each fence closes the innermost box still open:
:::callout{type="card"}
The statement of the exercise.
:::callout{type="answer"}
A white answer box with its own border and padding.
:::
:::callout{type="answer"}
A second answer box.
:::
:::When the outer box splits across columns or pages (keepTogether: false, or taller than a column), a nested box moves whole to the next fragment unless its own style lets it split too; every fragment redraws the frames it holds, and a continued nested box drops its title and icon.
#:::pagebreak
The directive itself does not force parity on its own — only the next block's layout. Use it to end a preface, force a dedication onto its own page, or mark the end of a section. When both a page break and a numbering reset are wanted at the same point, compose :::pagebreak followed by :::numbering — the numbering switch applies at the fresh page the :::pagebreak just created.
The old chapter ends here.
:::pagebreak{parity="odd"}
# A new chapterParity attribute
The parity attribute accepts the same five values as headings.levels[*].breakBefore.parity:
'any'— the default: no parity constraint; the break simply opens a new page.'odd'/'even'— the new page opens on the requested side of the spread; a single blank is inserted only when the natural next page is on the wrong side.'always-odd'/'always-even'— guarantee at least one mandatory blank separator page between the previous content and the new page, then enforce parity. The separator blank belongs to the previous chapter; any further parity padding belongs to whatever follows.
Blank-page ownership
The two kinds of blank pages :::pagebreak (and breakBefore) can introduce are distinguished in the VDTPage model:
blankForParity: true— inserted to satisfy a parity constraint. In{chapterTitle}headers this page carries the upcoming chapter's title, because the blank exists only to push that chapter onto the right parity.blankForForce: true— the mandatory leading separator of an'always-*'mode. It belongs to the previous chapter — a deliberate end-of-chapter breath, not parity padding for the next chapter.
Document-start exception
When :::pagebreak is the very first construct in a document (or a heading with breakBefore would pull one in), parity enforcement is skipped while the first page is still empty. The next block lands on page 1 as written, regardless of the requested parity — no spurious leading blank.
#:::numbering
:::numbering is how you restart the page counter mid-document. The canonical book example:
---
title: "A Book With Front Matter"
---
# Preface
…
:::pagebreak{parity="odd"}
:::numbering{format="decimal" startAt=1}
# Chapter 1Preface pages are labelled i, ii, iii, …; the first chapter opens on a right-hand page labelled 1.
Format-only changes (no startAt) keep the counter flowing — useful for, say, switching from lower-alpha to upper-alpha without resetting.
#:::toc
:::toc prints the table of contents where it stands. It expands into ordinary blocks — one per heading of the levels toc.levels lists (level 1 by default) and one per :::part — so the contents flow through columns and pages like any other text and a click on an entry lands on the directive line. An entry shows the heading's number (its numberingTemplate output, else the chapter ordinal), its title, a dotted leader and the label of the page it starts on, and, when toc.subtitle is enabled, a second line with a heading attribute such as the chapter's {author="…"}. A part gets a row of its own, designed by toc.parts.design and coloured with the part's own palette. Headings whose style has numbered: false are listed without a number; {toc="false"} on a heading keeps it out (the contents' own heading, typically).
# Contents {style="front-matter" toc="false"}
:::tocThe page numbers are the ones the document actually prints. A document laid out on its own is laid out again with the labels of the previous pass until they no longer move — with the numbering restarting after the front matter (the :::numbering recipe above) one extra pass settles it. A chapter laid out on its own (the sandbox previews) receives the whole book's outline from its host instead. See Table of contents in the configuration reference.
#Line breaks in titles
Write \\ inside a heading (or inside a part's title attribute) to force a line break where the title is displayed as a title: the in-column heading keeps flowing and shows a space there, while an opener design's {titleText} breaks the line at that point. Running heads, {chapterTitle}, {partTitle} and the PDF outline always render the title on one line.
# Concepts of health and illness. \\ Community health {author="I. Zango Martín"}#Inline formatting
Inline markup is recognised inside any text block (headings, paragraphs, blockquotes, list items).
| Markup | Syntax | Notes |
|---|---|---|
| Bold | bold or bold | Rendered with bodyText.boldFontWeight. An optional bodyText.boldColor overrides the default body color for bold spans. |
| Italic | italic or italic | Rendered with the italic variant of the current font family. An optional bodyText.italicColor overrides the default body color for italic spans. |
| Bold italic | both or both | Both flags combine. |
| Superscript | ^text^ | Set at 58% of the text size and raised a third of it — an exponent (10^-8^), an ion charge (Na^+^). The marked text starts and ends with a non-space character; a lone caret in prose stays literal. |
| Subscript | ~text~ | Same size, lowered a third of the text size — a chemical index (H~2~O, pK~a~). Combines with bold and italic (H~2~O). |
| Inline code | | Backticks are stripped; the span is rendered as plain text. Distinct code styling is on the roadmap. |
| Escape | *, _, ^, ~, ` | A backslash sets the marker character itself — the asterisk of a table footnote (* pOH = −log [OH^−^]), a literal caret — instead of opening a span. Works in the body and in captions, cells and notes. |
| Link | text | The visible text is kept in the flow; the URL is discarded by the current renderer. Link handling is on the roadmap. |
| Image | | Inline image markdown is removed from the text. Images must be declared on PostextContent.resources so the layout engine can place them according to resourcePlacement rules. |
| Chip | :chip[text] | A boxed run of text that wraps as one unit — a word bank, a key, a tag. Styled by chipStyles; see Inline chips. |
| Inline math | $…$ | A LaTeX formula that flows with the surrounding text, e.g. $e^+1=0$. Typeset by MathJax and rendered as vector paths on every backend. Use \$ for a literal dollar sign. Scaling, the display ($$ … $$) form, and error handling are covered under Mathematical formulas. |
#Inline chips
:chip[text] sets text in a box — a rounded, tinted "chip" — that flows with the line: the words of a word bank or a classify exercise, keyboard keys, tags. :chip[text]{style="key"} picks a named style from chipStyles (see the configuration reference); without style, or with an id no style declares, the chip takes the first style (a built-in chip style when the configuration has none; the sandbox warns about an unknown id).
Classify: :chip[battery] :chip[cable] :chip[switch] :chip[bulb]
Press :chip[Ctrl]{style="key"} + :chip[C]{style="key"} to copy.- One unit. A chip is never broken or hyphenated inside; the line breaks between chips, at the word spaces around them. A chip wider than the whole line overflows it rather than splitting.
- Width. Its advance is the text plus the horizontal padding and the outline on both sides. Justification stretches the word spaces only, never the inside of a chip. The style's
gapis the least room kept between the box and a neighbouring word or chip across a space — a narrower space is topped up (not at a line edge, and not against glued punctuation such as:chip[a],). - Height. The box is a band around the baseline, 0.8 em above and 0.25 em below at the chip's size, grown by the vertical padding and the outline. The vertical padding paints outside the line box and never changes the line height, so the baseline grid holds; a box taller than the line pitch touches the chips of the next line, and the sandbox flags it ("Chips touch the next line") so the padding, outline or size can be reduced.
- Text. The chip text takes its own inline marks (
:chip[**bold** word],:chip[x^2^]) and the emphasis around it (**:chip[a]**); the style may set its family, size, colour, bold and italic. Write\]for a literal bracket inside. References, swatches and math inside a chip stay literal. - Where. Paragraphs, list items, blockquotes, callouts, table cells, captions and notes. Headings keep
:chip[…]as literal text. - Output. Canvas, HTML and PDF paint the box and set the words as real text: selectable in the HTML, extractable and in reading order in the PDF (in a tagged PDF the box is a layout artifact and the words belong to the paragraph).
#Mathematical formulas
Math support is a first-class part of the document format. Postext parses $…$ for inline formulas and $$…$$ for display (block) formulas, and renders them via MathJax in SVG mode. The same vector paths drive all three backends, so the canvas preview, the HTML export, and the PDF output are pixel-for-pixel consistent — and the PDF stays fully vectorial regardless of the zoom level.
- Inline:
$…$. Recognised inside any text block (paragraph, heading, blockquote, list item). Contributes a single atomic, non-breaking box to the line; Knuth-Plass treats it like a word that must not be split. If the formula's natural height would break the line box, it is scaled down uniformly so the baseline grid is preserved — very tall expressions belong in display mode. - Display:
$$…$$. Either on its own line ($$\int_0^1 x^2\,dx$$) or fenced across multiple lines with$$markers on their own lines. Rendered centred on the column and snapped to the baseline grid with configurable top and bottom margins (math.marginTop,math.marginBottom) — exactly the same correction mechanism headings use, so the paragraph after the formula lands back on the grid. - Escaping:
\$is a literal dollar sign. Unmatched$or$$delimiters produce anunclosedMathentry in the warnings panel with a click-to-focus source anchor. - Errors: TeX source that MathJax rejects (undefined macros, syntax errors) surfaces as an
invalidMathwarning. The formula is replaced by a small red placeholder so the layout geometry stays valid. - Configuration: the
mathsection of the config exposesenabled,fontSizeScale(relative to the body font size),color(inherits the body colour when unset), and the display margins.
The Euler identity $e^{i\pi}+1=0$ links the five fundamental constants.
$$
\int_0^{\infty} e^{-x^2}\,dx = \frac{\sqrt{\pi}}{2}
$$#Resources
Images, SVGs, and tables are not written inline. They are declared once as resources (managed in the sandbox's Resources panel, which handles image and SVG upload, an interactive table editor, and caption and placement editing) and then connected to your prose by id. Referencing a resource is enough to incorporate it — you mention it once with an inline :ref{id="…"}, and the engine floats the figure or table into the first free slot after that reference — the bottom of the column you mention it in, the top of the next column, a band of the next page — just as a print typesetter would. You do not place it a second time.
Both forms below are net-new syntax that does not collide with CommonMark, so a document using them still reads as plain text in any other markdown viewer.
#Inline reference (the primary form)
Refer to a resource from within prose with :ref{id="…"}. The first reference both incorporates the resource (so it gets placed on the page) and renders its computed number, prefixed by the type's short label by default:
As shown in :ref{id="lighthouse-diagram"}, the lantern room sits above the gallery.renders as: As shown in Fig. 1.7, the lantern room sits above the gallery. — and the diagram itself floats to the nearest free slot after the sentence (the bottom of this column, the top of the next, or a band of the next page), while this sentence and the text after it flow on uninterrupted.
The running text is never broken at the reference point. Where the resource lands — the first free slot, or only a top or bottom slot; within a single column or across the full width — is governed by its placement (see Placement below), and by where you mention it: the search starts right after the reference.
#Block embed (optional, explicit inline placement)
Occasionally you want a resource to sit at an exact point in the flow rather than float. Opt out of floating by giving the resource placement.position: "here" and embedding it with ::resource{id="…"} on its own line:
Here is the floor plan we discussed.
::resource{id="lighthouse-diagram"}
The keeper's quarters occupy the eastern wing.For a floated resource the ::resource directive is unnecessary — the :ref already placed it, and a redundant ::resource for the same id is simply treated as another reference, not a second copy. A ::resource only renders the resource inline when its resolved placement is "here". An inline resource keeps a line of space above it (the float gap), like a float would, unless the block before asks for more.
The id must match a resource defined in the Resources panel. The engine renders the resource (bitmap, SVG, or table) with its caption drawn underneath as a figure/table foot. The caption text is composed from the resource type's captionPrefix, the computed number, and the resource's own caption — e.g. Figure 1.7. The original lighthouse plan. Its typography is governed by Configuration › Caption style: label and description share one typeface and size, while the label keeps independent bold/italic/colour settings; the gap above the caption defaults to 0.75em and the alignment to left. The caption can instead sit above the resource (captionStyle.position: 'above', globally or per resource type), optionally on a coloured bar. A resource may also carry a note — a short source line or credit, with the same inline formatting and :ref marks as the caption — set in a smaller run under the resource (under the caption when the caption is below, under the body when it is above) and styled through captionStyle.note.
Table resources draw their own grid, styled through Configuration › Table style: body and header cells have fully independent typography, the header background defaults to #f0f0f0, borders to 0.75pt, and cellPadding to 0.375em — any field left unset inherits from the body text. Column widths are part of the table itself: TableModel.columnWidths holds one relative weight per column ([2, 1, 1] gives the first column half the width); when unset, columns split the width equally. A table can also be set in a named variant: table.styleId picks one of the document's tableStyles (see Configuration › Named table styles), whose unset fields inherit tableStyle; an unknown or missing id keeps tableStyle.
A cell places its content with TableCell.align (left, center, right; list items stay flush left) and TableCell.verticalAlign (top, the default, middle or bottom). The vertical alignment moves the cell's whole content — the image and the text under it, as one unit — inside a cell taller than it: a row stretched by a longer neighbour, or the rows a rowSpan covers. It applies in every output (canvas, HTML, PDF), in rotated tables and in each slice of a table split across pages. Both are set per cell from the alignment buttons of the sandbox's table editor toolbar.
A table cell can also hold an image. TableCell.image names a bitmap or SVG resource by id ({ "resourceId": "fig-arm", "width": 0.7 }): the image is drawn inside the cell — never numbered, floated or captioned — fitted to the cell's inner width (or the fraction of it given by width, default 1) with its aspect ratio kept, aligned like the cell's text, and any cell text runs under it. The row grows to hold it. In the sandbox's table editor the image button of the toolbar picks the resource for the active cell and a width field sets the fraction. An id that matches no image resource leaves the cell text-only.
A cell can carry its own fill. TableCell.background is a colour value ({ "hex": "#c1dfd6", "model": "hex" }, optionally linked to a document palette entry with paletteId) painted instead of the style's header or body background — a compatibility matrix shades its cells green, red and yellow this way. The sandbox's table editor sets it from the fill control of the toolbar. To key such fills, the caption, the note and any text block accept an inline colour swatch: :swatch{color="#c1dfd6"} (a hex, or the id of a palette entry — :swatch{color="table-compatible"}) sets a small square on the baseline, three quarters of the font size, filled with the colour and outlined in the text colour, so a note can read :swatch{color="ok"}: compatible; :swatch{color="no"}: incompatible. A colour that resolves to nothing draws an empty outline.
SVG resources can additionally be recoloured for single-spot-colour printing via diagramStyle.singleInk (default false). When enabled, every colour in an SVG diagram is remapped to a luminance-mapped tint of diagramStyle.inkColor (which defaults to the main palette colour, #295AA3) — white maps to paper, black to the full ink — so figures reproduce faithfully when the document is printed with a single spot colour. See Configuration › Diagram style.
A malformed embed (missing or empty id, extra attributes) is not promoted to a resource block; it falls through to ordinary paragraph parsing and remains visible in the output, and the sandbox surfaces a warning.
Inline references are recognised inside any text block — paragraphs, headings, blockquotes, and list items — and may sit alongside bold, italic, inline code, and inline math.
Reference options
The :ref directive accepts three optional attributes, in any order. style selects how the computed label is rendered: style="number" prints the bare number (1.7), style="full" prints the type's full name plus the number (Figure 1.7), and when style is unset the type's shortLabel plus the number is used (Fig. 1.7). case changes the letter case of the label part only — lower, upper or capitalize — leaving the number untouched. text is a verbatim override that replaces any computed label and takes precedence over both style and case. The rendering options side by side:
| Syntax | Renders | Notes |
|---|---|---|
:ref{id="…"} | Fig. 1.7 | Default style: the type's shortLabel followed by the number, joined with a non-breaking space so they never wrap apart. |
:ref{id="…" style="number"} | 1.7 | The bare computed number, no label. |
:ref{id="…" style="full"} | Figure 1.7 | The type's full name followed by the number. Use at the start of a sentence or where the abbreviation reads poorly. |
:ref{id="…" case="lower"} | fig. 1.7 | Recases the label only: lower (fig. 1.7), upper (FIG. 1.7) or capitalize (first letter upper-cased). Combines with style="full" (figure 1.7); the number is never touched, and an unrecognised value is ignored. |
:ref{id="…" text="see the plan"} | see the plan | An explicit override. The given text is used verbatim instead of any computed label — useful for prose links like "as we saw earlier". When present, text takes precedence over style and case. |
If a :ref (or ::resource) names an id with no matching resource, the label falls back to ? and the sandbox raises an unknown resource warning.
#First-reference numbering
A resource's number is assigned the first time it is mentioned in reading order — whether that first mention is a ::resource block embed or an inline :ref. From then on, every reference to the same id prints that same number.
This means numbers follow the order the reader meets them, not the order resources were created in the panel:
- If you
:refa figure in the introduction and only embed it (::resource) two pages later, it still takes the introduction's number — the reference came first. - Inserting a new reference earlier in the document automatically renumbers everything after it. There is no manual numbering to keep in sync.
Numbering is per resource type and respects each type's reset scope and counter format — see Configuration › Resource types for the template tokens ({h1}, {n}), resetOn, and counterFormat.
#Placement
Each resource has a placement that decides where its float lands, resolved per resource (its own placement), then its type's defaultPlacement, then the built-in default of auto / column:
| Field | Values | Meaning |
|---|---|---|
position | "auto" · "top" · "bottom" · "here" | "auto" (the default) takes the first free slot after the reference, top or bottom; "top" / "bottom" only accept slots of that kind; "here" opts out of floating and embeds inline at the ::resource directive. |
width, align | 0 < width < 1; "left" · "center" · "right" | A resource narrower than its slot: width is the fraction of the column (or page) width it takes, align where it sits in the slot (a small table centred in its column; a page-wide band whose picture spans one column). Floats and inline ::resource embeds alike. |
captionSide | true · false | In a one-and-a-half layout whose side column is reserved for floats (layout.sideColumnRole: 'floats'), a "column" float with captionSide keeps its body in the main column and sets its caption (and note) in the side column, level with the figure's top — or its bottom for a bottom float; the side column gives up that band. A page without such a column keeps the caption under the figure. |
span | "column" · "page" | Occupy a single column, or break the column flow and span the full content width across all columns. In a single-column layout the two are identical. |
rotate | "ccw" · "cw" | Set the resource turned a quarter turn — a landscape table in a portrait book. "ccw" turns it counter-clockwise, its top facing the left edge of the page (the reader turns the book clockwise), the usual convention; "cw" the other way. A turned resource is always a page-span float on a page of its own: it is laid out across the height of the content area, sits flush to the spine when the margins are mirrored (flush left otherwise), and a table too wide for one page is cut between rows and continues, turned, on the following pages with its header repeated, exactly like an upright table taller than a page. A turned figure is scaled to fit the page. Ignored for an inline ("here") embed. |
A float goes into the first free slot after its first reference, in reading order: the bottom of the column the reference sits in, then the top and the bottom of the next empty column of the same page, then the bands of the next page the flow opens (a page-span float takes the bottom of the page when every column still has room for it, else a band of the next page). It is never shrunk, and it never lands before its reference. Floats of one numbering sequence appear in reference order: a figure that fits nowhere on a page holds up the figures behind it (a waiting table does not hold up a figure, nor the other way round), so figure 12 never appears before figure 11. A table that would otherwise wait is cut instead: offered the head of an empty column, it takes the rows that fit and continues in the next slot — the column beside it or the next page — with its header rows repeated (see tableStyle.overflow).
Floats never escape their chapter: at a chapter opener (a heading level with breakBefore or span: 'page'), a :::part, a callout style with floatBarrier: true (the closing "key points" box of a chapter) and at the end of the document, every float still pending is placed first — in the page's free slots, or on pages opened ahead of the boundary. A figure or table that asked for the head of a page may then take the foot of the chapter's closing page, under its balanced columns, rather than a page of its own. A :::pagebreak simply sends pending floats to the page that follows it.
Float bands are corrected against the baseline grid so the surrounding text keeps the page-wide vertical rhythm. A top band grows its bottom margin to the next grid line, so the text below the float stays aligned with the neighbouring columns and the facing page. A bottom float is anchored so its caption's last line shares a baseline with the last text line of the other columns (captionless content aligns its bottom edge to the last grid slot) — full pages therefore end at the same height across columns and across facing pages.
All three backends — the canvas preview, the HTML viewer, and the PDF output — render resources. In the HTML backend, image payloads live out-of-band, so the host supplies them through the resourceImageUrl(fileId) resolver option; when the resolver is missing (or returns nothing for a file), the resource renders as a neutral placeholder box so the layout stays stable.
#What is NOT supported
Postext does not recognise the following CommonMark features. They are either treated as plain text (and therefore will appear literally in the output) or silently dropped:
- Setext-style headings — the
===/---underline form. Use ATX (#) headings. - Fenced or indented code blocks — triple-backtick fences and 4-space indentation. Inline code works; multiline code will be rendered line-by-line as paragraphs.
- HTML passthrough — raw
<tags>are not interpreted. MDX-style tags are not supported either; Postext source is pure markdown. - Horizontal rules —
---,***,___. - Tables — pipe tables are not parsed. Tables are modeled as structured resources on
PostextContent.resources. - Reference-style links —
[text][id]plus a definition block. - Autolinks —
<https://example.com>. - Strikethrough —
~~text~~. The strikethrough renderer is currently reserved for completed task items. - Footnote markers in markdown —
[^1]. Footnotes ride onPostextContent.notesand are referenced by id, not by inline syntax.
This list will shrink over time. Until then, anything not explicitly listed in the supported section above should be assumed to be literal text.
#Authoring conventions
A few conventions make the difference between a document that parses cleanly and one that surprises you:
- Leave a blank line between blocks. Two paragraphs separated by a blank line are two paragraphs. Two paragraphs on consecutive lines become one — every line collapses into the preceding paragraph.
- Nest lists with exactly two spaces per level. One space is parsed as a level-1 item. Three or four spaces round down to level 2 (the engine uses
floor(leading / 2) + 1, clamped to depth 5). Tab indentation is not recognised — convert tabs to spaces. - Do not indent the first list item. Level-1 items start at column 0. Leading whitespace on a bullet implicitly raises the depth.
- Task markers must be in square brackets with a single space.
[ ],[x],[X]— no variations.[*]or[-]are not task markers; they render as literal text. - Blockquotes inside lists are not supported. Start the blockquote at column 0, outside the list.
- Images and tables live in
resources. Inlineis stripped precisely because inline images break column-aware placement. Declare each image as a resource and reference it by id — the engine then decides whether it floats, breaks the column, or moves to the top of the next page. - Escape dollar signs with
\$when you do not mean math. Postext interprets$…$as inline LaTeX, so a raw$in prose will start a formula. Prices, shell prompts, and anything else with a bare dollar sign should be written as\$.
#Worked example
A short document that exercises every supported construct:
---
title: The Typesetter's Craft
author: Anon
---
# Opening
A good book reads itself. The **reader** should never notice the
typesetter's work — only the author's voice.
## What makes text readable
Three properties matter most:
1. Line measure — 40 to 75 characters per line.
2. Leading — 1.3 to 1.5 times the font size.
a. Tighter at short measures.
b. Looser at long measures.
3. Contrast between body and headings.
Common failure modes include:
- Lines that stretch across the whole page.
- Headings that float without a following paragraph.
- Orphans and widows at column boundaries.
> Typography is the craft of endowing human language with a durable
> visual form.
> — Robert Bringhurst
### Review checklist
- [x] Column width under 75 characters
- [x] Leading set to 1.5
- [ ] Orphan and widow pass
- [ ] Final proofread
### A note on formulas
Inline math such as $a^2 + b^2 = c^2$ flows with the surrounding text, and
display math sits centred on the baseline grid:
$$
\int_0^1 x^2\,dx = \tfrac{1}{3}
$$The same document, rendered through the layout engine, produces a structured VDTDocument whose pages carry each of these blocks as typed entries — see the Architecture page for how blocks become geometry.