# Document Format

> The markdown subset Postext parses, and the rules for authoring source documents

- HTML version: https://postext.dev/en/docs/document-format
- Last updated: 2026-09-22
- Reading time: 10 min
- Other languages: [es](https://postext.dev/es/docs/document-format.md)

**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](/en/docs/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:

```md
---
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`](https://github.com/jonschlinkert/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.

> **Figure: Prose block constructs at a glance**
> The seven prose block types Postext recognises: heading, paragraph, blockquote, unordered list, ordered list, task list, and display math, each with its markdown syntax. Directives and resource embeds are covered separately.
>
> *Every prose block type and its markdown entry point.*

| 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](https://postext.dev/en/docs/document-format#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.

> **Figure: List nesting depth**
> Lists nest using exactly two spaces per level, up to a maximum depth of five. Each level indents further and may use different bullet styling.
>
> *Two spaces per level. Max depth: five.*

### Heading attributes

A heading line may end with an attribute block in braces — the same `key="value"` syntax directives use:

```markdown
# 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](/en/docs/configuration#heading-styles) 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`:

```markdown
# 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 (<code>"…"</code>), single-quoted (<code>'…'</code>), or bare (<code>startAt=17</code>). 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](/en/docs/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](/en/docs/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](/en/docs/configuration#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.

```md
:::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:

```md
:::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.

```md
The old chapter ends here.

:::pagebreak{parity="odd"}

# A new chapter
```

#### Parity 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:

```md
---
title: "A Book With Front Matter"
---

# Preface

…

:::pagebreak{parity="odd"}
:::numbering{format="decimal" startAt=1}

# Chapter 1
```

Preface 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).

```md
# Contents {style="front-matter" toc="false"}

:::toc
```

The 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](/en/docs/configuration#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.

```md
# 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).

> **Figure: Inline formatting at a glance**
> Markdown-to-rendered comparison for bold, italic, bold italic, inline code, and links.
>
> *Markdown on the left, rendered result on the right.*

| 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`, `p<em>K</em>~a~`). Combines with bold and italic (`**H~2~O**`). |
| Inline code | ```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](https://…)` | The visible text is kept in the flow; the URL is discarded by the current renderer. Link handling is on the roadmap. |
| Image | `![alt](src)` | 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](https://postext.dev/en/docs/document-format#inline-chips). |
| Inline math | `$…$` | A LaTeX formula that flows with the surrounding text, e.g. `$e^{i\pi}+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](https://postext.dev/en/docs/document-format#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).

```markdown
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 `gap` is 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](https://www.mathjax.org/) 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 an `unclosedMath` entry in the warnings panel with a click-to-focus source anchor.
- **Errors:** TeX source that MathJax rejects (undefined macros, syntax errors) surfaces as an `invalidMath` warning. The formula is replaced by a small red placeholder so the layout geometry stays valid.
- **Configuration:** the `math` section of the config exposes `enabled`, `fontSizeScale` (relative to the body font size), `color` (inherits the body colour when unset), and the display margins.

```md
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](/en/docs/sandbox#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:

```md
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](#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:

```md
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](/en/docs/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](/en/docs/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](/en/docs/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](/en/docs/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 `:ref` a 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](/en/docs/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 on `PostextContent.notes` and 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`.** Inline `![alt](src)` is 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:

```md
---
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](/en/docs/architecture) page for how blocks become geometry.
