Repository navigation
Configuration Styles
Part of the Configuration reference.
Generated from docs/configuration-styles-en.mdx (updated 2026-10-16). Read it on postext.dev, where it is also available in Spanish, Catalan, Portuguese, French, German, Italian, Chinese, Japanese and Arabic. To change this page, edit the MDX and run pnpm wiki in apps/web.
The paragraphStyles property declares named styles that a document applies to a run of paragraphs with a :::paragraphs{style="…"} container — bibliographies, glossaries, notes, any block of entries that wants its own face, weight, slant, size, leading, capitals, small capitals or a hanging indent. Every typographic field is optional and inherits the body text when unset, so a style only spells out what differs from running text.
const config: PostextConfig = {
paragraphStyles: [
{
id: 'bibliography',
name: 'Bibliography',
fontSize: { value: 7, unit: 'pt' },
lineHeight: { value: 1.2, unit: 'em' },
hangingIndent: { value: 2, unit: 'em' },
spaceBetween: { value: 0.25, unit: 'em' },
marginTop: { value: 1, unit: 'em' },
marginBottom: { value: 1, unit: 'em' },
},
],
};## References
:::paragraphs{style="bibliography"}
Knuth, D. E. (1984). *The TeXbook*. Addison-Wesley.
Bringhurst, R. (2004). *The Elements of Typographic Style*. Hartley & Marks.
:::| Property | Type | Default | Description |
|---|---|---|---|
id |
string |
required | Identifier referenced from :::paragraphs{style="…"}. |
name |
string |
id |
Human-readable name, for editor UIs only. |
fontFamily |
string |
body text font | Font family. Its weights are fontWeight / boldFontWeight below (the body text's when unset). |
fontSize |
Dimension |
body text size | Font size. |
lineHeight |
Dimension |
body line height | Leading. em/rem are relative to the style's own font size, so an inherited 1.5em tightens along with a smaller size. |
color |
ColorValue |
body text color | Text color. Bold and italic runs keep the body emphasis colors unless boldColor / italicColor set the style's own. |
textAlign |
'left' | 'justify' | 'center' | 'right' | 'start' | 'end' |
body alignment | Horizontal alignment. 'center' and 'right' set every line ragged from the other side — a dedication, a signature block. In a right-to-left paragraph 'left' is its start side, the right. |
boldColor |
ColorValue |
bodyText.boldColor |
Colour of bold runs (an authors list with the names in the house colour). |
italicColor |
ColorValue |
bodyText.italicColor |
Colour of italic (*…*) runs — in an italic style, of the runs that turn upright. It does not follow color: a coloured style whose italics should stay in its colour sets both. |
fontWeight |
number |
bodyText.fontWeight |
Weight of the regular text (100–900) — a semibold question in a worksheet, a light epigraph. |
boldFontWeight |
number |
bodyText.boldFontWeight |
Weight of the bold (**…**) runs. |
italic |
boolean |
false |
Set the paragraphs in italics — stage directions, an epigraph. An italic *…* run inside them turns upright, as in a blockquote. |
smallCaps |
boolean |
false |
Set the paragraphs in small capitals: lowercase letters as capitals at 70% of the size, capitals at full size, drawn the same way on every backend (see Small capitals) — a cast list, the headwords of a glossary. |
hyphenation |
boolean |
body hyphenation | Hyphenate when justified (uses the document locale). |
indent |
Dimension |
0 |
Indent of every line from the left edge of the column (or of the box the paragraphs sit in); em is the style's own size. The first-line and hanging indents are measured from it, so an indented line of verse can hang its turnover deeper than its own start: indent: 1.5em with hangingIndent: 2.5em sets the line at 1.5 em and its turnover at 4 em. A negative value counts as 0. |
endIndent |
Dimension |
0 |
Indent of every line from the end side (the right of a horizontal line, the foot of a vertical one); em is the style's own size. With textAlign: 'end' it sets a line some characters up from the foot, the 地からN字上げ of a Japanese letter's date or signature. Since postext 1.16. |
firstLineIndent |
Dimension |
body first-line indent | Indent of the first line, from indent. With a non-zero hangingIndent it applies only when the style sets it itself: the first line starts at indent + firstLineIndent and the turnovers at indent + hangingIndent, so a line of verse can start 1 em in and hang its turnover 3 em. Inherited from the body, it gives way to the hanging indent and the first line starts at indent, as up to postext 1.22 (a configuration stored before has an explicit one dropped from such a style). |
hangingIndent |
Dimension |
0 |
Indent applied to every line except the first, from indent — the classic bibliography or glossary shape, and the turnover of a line of verse. The first line starts at indent, or at the style's own firstLineIndent when it sets one. |
spaceBetween |
Dimension |
0 |
Vertical gap between consecutive paragraphs inside the container. 0 makes entries abut. |
marginTop |
Dimension |
0 |
Space above the container's first paragraph. Collapses with the spacing already pending and vanishes at the top of a column, like any other margin. |
marginBottom |
Dimension |
0 |
Minimum space below the container's last paragraph. How it meets the space of the block after the container is bodyText.paragraphContainerSpacing. |
snapToGrid |
boolean |
true |
Snap the flow back onto the baseline grid under the container, the space below being a minimum. false keeps the exact space: the text after the container stays off the grid until the next block that snaps (a heading, the end of a list, display maths), for a document that runs off the grid or a group whose leading is its own. Inside a callout, which has no grid, it changes nothing. |
containerSpacing |
'collapse' | 'add' |
unset | The rule for the space under the containers of this style, in the running text and inside a box: one of the two values of bodyText.paragraphContainerSpacing, for the containers that name the style. Unset: they follow the document. With 'add' the style's groups end on its own space, with no line of paragraph spacing added, while the rest of the document keeps 'collapse'. Since postext 1.28. |
textTransform |
'none' | 'uppercase' |
'none' |
Letter case of the paragraphs: 'uppercase' sets them in capitals (a cast list, a line of stage business), the words of a chip and the label of a :ref included. Length for length, so the editor's source map stays one to one: a letter whose capital is longer (ß) is left as written. Maths is left alone, and a running head that reads the paragraph as a mark ({firstMark.<em>style</em>}) takes the text as written; a design text's own textTransform sets it in capitals. |
wordBreak |
'normal' | 'keep-all' |
cjk.wordBreak |
Where the paragraphs' lines break between Chinese, Japanese and Korean characters (see cjk.wordBreak): 'keep-all' for a primer in phrase-spaced kana quoted in a book of ordinary prose, 'normal' for the reverse. It holds for a paragraph of such text and for the characters a Latin paragraph quotes. Since postext 1.16. |
lineNumbers |
boolean |
unset | Whether line numbers count the paragraphs' lines. Unset: counted when lineNumbers.count is 'all', and in a poem set in the style when it counts verse. true: counted under 'verse' too, and inside a callout, whose text is otherwise never counted. false: never counted. Since postext 1.23. |
tabStops |
TabStop[] |
bodyText.tabStops |
Tab stops of the style's paragraphs (see Tab stops), measured from the style's indent. A tab character in their text is a tab when the style has stops or an interval, its own or the body's. Unset: the body's; an empty list sets none. Since postext 1.23. |
tabInterval |
Dimension |
bodyText.tabInterval |
Default stops past the last of tabStops. Unset: the body's. Since postext 1.23. |
dropCap |
ParagraphDropCap |
none | A drop cap opening the first paragraph of each :::paragraphs group in the style, or every paragraph with each: true (see Drop caps). {dropcap=false} on a group's fence turns it off, {dropcap=2} sets its lines. Since postext 1.23. |
A play sets its stage directions in italics and its cast list in small capitals:
paragraphStyles: [
{ id: 'direction', italic: true, fontSize: { value: 9, unit: 'pt' } },
{ id: 'cast', smallCaps: true, textAlign: 'center', fontWeight: 600 },
],:::paragraphs{style="direction"}
Elsinore. A platform before the castle. *Francisco* at his post.
:::The direction prints in italics and the name inside it upright; the weights, italic and smallCaps of a style also apply inside callouts.
A book of verse indents some lines and hangs the turnover of a line too long for the measure deeper than the line itself. indent moves every line of the paragraph in, and the hanging indent counts from there:
paragraphStyles: [
{ id: 'verse', textAlign: 'left', firstLineIndent: { value: 0, unit: 'em' }, hangingIndent: { value: 4, unit: 'em' } },
{ id: 'verse-indented', textAlign: 'left', indent: { value: 1.5, unit: 'em' }, hangingIndent: { value: 2.5, unit: 'em' } },
],A line in verse-indented starts at 1.5 em and its turnover at 4 em, level with the turnovers of the lines in verse. Without indent a style can indent the first line or hang the others, not both: firstLineIndent is ignored once hangingIndent is set.
A menu sets each price flush with the end of the measure, behind a dot leader:
paragraphStyles: [
{
id: 'menu',
textAlign: 'left',
firstLineIndent: { value: 0, unit: 'em' },
tabStops: [{ position: 'end', align: 'end', leader: '. ' }],
},
],:::paragraphs{style="menu"}
Onion soup :tab 8.50
Grilled sea bream with fennel and lemon :tab 21.00
:::The dots of every line end half an em before the price (leaderGap). A dish too long for its line wraps, and its last line keeps the leader and the price; when the price does not fit beside the last word, that word goes down with it.
A :::paragraphs{style="<id>"} line opens the container and a bare ::: line closes it; every paragraph in between takes the named style, while headings, lists, and other blocks inside keep their usual styling. Containers may be nested inside other fenced containers. An unknown style id is not an error: the paragraphs render as plain body text.
The fence also takes align (start, end, left, right, center, justify), indent and endIndent (bare numbers are ems), with or without a style: with one, they override it; without, they apply over the enclosing container's style, or over the text style where the fence stands (the body, a part, a styled section or a box). :::paragraphs{align=end} sets a block flush with the end of the line (地付き) and :::paragraphs{align=end endIndent=1} one character up from it. Since postext 1.16.
Inside the container the flow leaves the baseline grid — a 7pt entry with 1.2em leading cannot sit on an 8pt/1.5em grid — and the last paragraph snaps the flow back onto it (the grid wins; the space below is a minimum, the same convention headings follow). Entries split across columns and pages like body paragraphs, with the same orphan and widow protection; a heading immediately before the container keeps with its first paragraph.
The space under the container is the larger of the style's spaceBetween and marginBottom and the paragraph spacing of the text around it (a line when bodyText.paragraphSpacing is on), and it merges with the space the next block keeps above itself, as the space between two body paragraphs does: a heading after a bibliography sits its own marginTop below the last entry (or the style's space, when that is larger), and a paragraph after a group of tighter entries keeps the text's paragraph spacing. The flow snaps back to the grid under the text first, and what the snap did not cover is carried on in whole grid lines, so the text after the container lands on the grid. Up to postext 1.4 the style's space was set under the last line before the snap and the next block's space above was added under it, and the paragraph spacing was left out; bodyText.paragraphContainerSpacing: 'add' keeps that rule, and configurations stored before it read with it. A style with snapToGrid: false does not snap: the text after the container sits the exact space below it, off the grid until the next block that snaps. A container that closes on a list is set as in 1.4 under either rule: the list keeps its own space below it, and marginBottom follows that space, merging with the next block's.
A style can set the rule for its own containers with containerSpacing: 'add' on the style of a large figure or of a list of references ends its groups on the style's own space, in the text and inside a box, while every other container of the document keeps 'collapse'; 'collapse' on a style does the reverse in a document that reads with 'add'. A container that names no style follows the document.
Inside a :::callout the container takes its style's margins the same way: marginTop and marginBottom collapse with the spacing of the blocks around it (a negative one pulls them closer), and a container that opens the box takes no top margin, as at the top of a column. A box has no baseline grid to snap back to, so the space below the last paragraph is the larger of marginBottom, spaceBetween and the box's own paragraph spacing (its body.paragraphSpacing, a line of its text; left out with paragraphContainerSpacing: 'add'), or the next block's own top margin, when that is larger still; a negative marginBottom pulls the next block up instead. (Up to postext 1.4 a container inside a callout ignored both margins.)
const resolved = resolveParagraphStylesConfig(config.paragraphStyles, resolvedBodyText);
// => every unset field filled from the resolved body text
const minimal = stripParagraphStylesDefaults(config.paragraphStyles);
// => undefined when the list is empty; zero margins and `name === id` droppedThe chipStyles property declares the named styles of the inline :chip[text]{style="…"} — the rounded, tinted boxes of a word bank, a keyboard key, a tag (the syntax and its line-breaking rules are in the document format reference). One style, chip, ships by default (a pale blue fill with a hairline in the main colour, slightly rounded, the text as the words around it), so :chip[…] works without any configuration; declaring chipStyles replaces that default list. A chip without style, or with an id no style declares, takes the first style.
const config: PostextConfig = {
chipStyles: [
{ id: 'chip', name: 'Word bank' },
{
id: 'key',
name: 'Keyboard key',
background: { hex: '#fff4d6', model: 'hex' },
borderColor: { hex: '#8a6d1f', model: 'hex' },
borderRadius: { value: 2, unit: 'pt' },
bold: true,
},
],
};Classify: :chip[battery] :chip[cable] :chip[switch]
Press :chip[Ctrl]{style="key"} + :chip[C]{style="key"}.| Property | Type | Default | Description |
|---|---|---|---|
id |
string |
required | Identifier referenced from :chip[…]{style="…"}. |
name |
string |
id |
Human-readable name, for editor UIs only. |
backgroundEnabled |
boolean |
true |
Paint the box fill. |
background |
ColorValue |
#e8eef7 |
Box fill (palette-linkable). |
borderColor |
ColorValue |
main palette colour | Outline colour. |
borderWidth |
Dimension |
0.5pt |
Outline width; 0 draws none. The outline is stroked inside the box edge. |
borderRadius |
Dimension |
0.3em |
Corner radius, clamped to half the box height (a large value gives a pill). |
paddingX |
Dimension |
0.3em |
Room between the outline and the text, left and right. Part of the chip's advance. |
paddingY |
Dimension |
0.1em |
Room above and below the text band. Paints outside the line box: it never changes the line height. |
paddingTop, paddingBottom
|
Dimension |
paddingY |
Room above or below the text band, each in place of paddingY. The band runs 0.8 em above the baseline and 0.25 em below it, so its middle sits 0.275 em above the baseline, lower than the middle of a capital (about 0.35 em in most faces): a capital or a figure in a round chip (borderRadius: 1em) looks high. A top padding larger than the bottom one by twice the difference centres it: paddingTop: 0.2em with paddingBottom: 0.05em for a face whose capitals are 0.7 em tall. |
fontFamily |
string |
surrounding text | Family of the chip text. Weights follow the text around it. |
fontSize |
Dimension |
surrounding text | Size of the chip text; em is relative to the surrounding text. |
color |
ColorValue |
surrounding text | Colour of the chip text. Unset, bold and italic runs keep the emphasis colours. |
bold |
boolean |
false |
Set the chip text bold, on top of its own markup. |
italic |
boolean |
false |
Set the chip text italic, on top of its own markup. |
gap |
Dimension |
0.25em |
Least room kept between the box and a neighbouring word or chip across a word space; a narrower space is topped up inside the chip's advance, so justification never eats it. Nothing is added at a line edge or against glued punctuation. |
Em lengths of the box (paddingX, paddingY, borderRadius, borderWidth, gap) are relative to the chip's own font size. The box is a band 0.8 em above and 0.25 em below the baseline, grown by paddingY and the outline; it paints outside the line box and never changes the leading, so the baseline grid holds. When the box ends up taller than the line pitch, a chip can run into a chip on the line above or below — the sandbox lists a "Chips touch the next line" warning when two chips on different lines overlap, with the overlap in points, so paddingY, the outline or fontSize can be reduced. A tall chip with no chip above or below it is not flagged.
In the VDT a chip is a line segment of kind: 'chip' whose chip field carries the text runs (each with its font string and width), the box geometry (boxWidth, ascent, descent, paddingX, borderWidth, borderRadius, the gap margins) and its colours; the segment's text is a one-character placeholder, so plain-text offsets and source maps count a chip as one character.
const resolved = resolveChipStylesConfig(config.chipStyles);
// => the built-in `chip` style when unset; every field filled
const minimal = stripChipStylesDefaults(config.chipStyles);
// => undefined for the built-in default; static defaults dropped
const style = pickChipStyle(resolved, 'key');
// => the `key` style, else the first oneThe markStyles property declares the named styles of the inline :mark[text]{style="…"}: the coloured band behind a phrase, as a textbook highlights its key words (the syntax and its line-breaking rules are in the document format reference). One style, mark, ships by default (a pale yellow band reaching a little past the phrase, its ends slightly rounded, the text in its own colour), so :mark[…] works without any configuration; declaring markStyles replaces that default list. A mark without style, or with an id no style declares, takes the first style.
const config: PostextConfig = {
markStyles: [
{ id: 'mark', name: 'Key idea' },
{
id: 'key',
name: 'Definition',
background: { hex: '#cdebd5', model: 'hex' },
color: { hex: '#14532d', model: 'hex' },
},
],
};The sun warms the sea and :mark[water rises into the air as vapour].
- :mark[Evaporation]{style="key"}: water turns into vapour.| Property | Type | Default | Description |
|---|---|---|---|
id |
string |
required | Identifier referenced from :mark[…]{style="…"}. |
name |
string |
id |
Human-readable name, for editor UIs only. |
background |
ColorValue |
#fff0a0 |
Band colour (palette-linkable). |
color |
ColorValue |
the text's | Colour of the marked words. |
borderRadius |
Dimension |
0.2em |
Radius of the band's two ends, clamped to half its height. |
paddingX |
Dimension |
0.15em |
How far the band reaches past the first and the last letter of the phrase, into the space beside it. |
paddingY |
Dimension |
0.05em |
Room above and below the text band. |
paddingTop, paddingBottom
|
Dimension |
paddingY |
Room above and below, each in place of paddingY. |
Em lengths (paddingX, paddingY, paddingTop, paddingBottom, borderRadius) are relative to the marked text's size. The band takes no room: the text is laid out as it would be without the mark, the band is painted under it, and its padding reaches into the word spaces and the line gap. In the VDT the marked words' segments carry a mark ({ style, id, start?, end?, from?, to? }) and each line holds its bands in highlights ({ x, y, width, height, color, radiusLeft, radiusRight }, relative to the line's bbox.x and baseline), which the canvas, PDF and HTML backends paint before the line's text.
const resolved = resolveMarkStylesConfig(config.markStyles);
// => the built-in `mark` style when unset; every field filled
const minimal = stripMarkStylesDefaults(config.markStyles);
// => undefined for the built-in default; static defaults dropped
const style = pickMarkStyle(resolved, 'key');
// => the `key` style, else the first oneThe codeStyle property sets how code listings look: the ``` and ~~~ fences of the text (see Document format › Code blocks), and inline code when it asks for a code face. A listing is set line by line as written, in a monospaced face, in a box: no line is hyphenated or justified, every space keeps its width, and a tab goes on to the next tab stop. The box comes from the same machinery as a :::callout, so a listing splits between lines across columns and pages, each part in a box of its own, and a fence inside a callout is a box nested in it. Every property is optional. Since postext 1.23.
const config: PostextConfig = {
codeStyle: {
fontFamily: 'JetBrains Mono',
fontSize: { value: 0.8, unit: 'em' },
background: { hex: '#0e1116', model: 'hex' },
color: { hex: '#d3d9df', model: 'hex' },
padding: { top: { value: 4, unit: 'mm' }, right: { value: 5, unit: 'mm' }, bottom: { value: 4, unit: 'mm' }, left: { value: 5, unit: 'mm' } },
borderRadius: { value: 2, unit: 'pt' },
lineNumbers: true,
tokens: {
keyword: { color: { hex: '#f2b134', model: 'hex' }, bold: true },
string: { color: { hex: '#3ddc84', model: 'hex' } },
comment: { color: { hex: '#8a939d', model: 'hex' }, italic: true },
},
inline: { background: { hex: '#eef1f4', model: 'hex' } },
},
};| Property | Type | Default | Description |
|---|---|---|---|
blocks |
boolean |
true |
Read fences as code blocks. false reads a fence and its lines as Markdown, as postext 1.22 did; a configuration stored before 1.23 whose text has a fence gets it (see Bundles written by postext 1.4 or earlier). |
indentedCode |
boolean |
false |
Also read a run of lines indented four columns (four spaces, or a tab), after a blank line, as a listing; four columns come off each line. Off by default: Postext text often indents with spaces, and nested list items read leading spaces. |
fontFamily |
string |
'Source Code Pro' |
The code face. A monospaced face keeps columns aligned; for Chinese or Japanese comments, one with kanji (BIZ UDGothic), whose full-width characters take two cells. |
fontSize |
Dimension |
0.85em |
em is the body size. |
fontWeight / boldFontWeight
|
number |
400 / 700
|
The weights of the code and of bold tokens. |
lineHeight |
Dimension |
the body's grid line | The leading of the code lines; em is the code size. Unset, the lines sit on the baseline grid. |
snapToGrid |
boolean |
true |
The text after a listing goes back to the baseline grid; false keeps the exact marginBottom, as a callout's. |
color |
ColorValue |
the body colour | The code's colour, which tokens without a colour of their own keep. |
backgroundEnabled / background
|
boolean / ColorValue
|
true / #f4f4f4
|
The box's fill. |
border |
{ enabled, color, width } |
off, #cccccc, 0.5pt
|
The box's outline. |
borderRadius |
Dimension |
0 |
Rounded corners; a part of a split listing keeps them, as a split box does. |
padding |
{ top, right, bottom, left } |
0.6em each |
em is the code size. |
marginTop / marginBottom
|
Dimension |
0.75em |
The space above and below the box. |
span |
'column' | 'page' |
'column' |
A page-span listing crosses every column of a multi-column page, as a span: 'page' box does. A fence sets its own with span=page. |
tabSize |
number |
4 |
A tab goes on to the next multiple of this many character cells (a full-width character counts two). It stays a tab: copied text keeps it. |
overflow |
'wrap' | 'shrink' | 'clip' |
'wrap' |
A line wider than the box. 'wrap': it breaks after the last space or punctuation that fits (between two characters when none does), and the rest goes on below, wrapIndent cells in, behind wrapMarker; a part of a split listing never starts with such a rest when another cut fits. 'shrink': the whole listing is set smaller until its widest line fits, down to minFontScale, and what still does not fit wraps. 'clip': the line stops at the box's inner edge; the characters past it are not printed. Each one raises a codeOverflow warning. |
wrapIndent |
number |
2 |
The indent of a wrapped line's rest, in character cells. |
wrapMarker |
string |
'»' |
Set in that indent, in the line numbers' colour; not part of the text (copies leave it out, a tagged PDF paints it as an artifact). '' sets none. ↪ is missing from most code faces, where it prints as an empty box. |
minFontScale |
number |
0.8 |
With 'shrink', the smallest share of fontSize a listing is set at. |
lineNumbers |
boolean |
false |
Number the lines of every listing, in a gutter before the code. A fence sets its own with lineNumbers, lineNumbers=false and start=N. The rest of a wrapped line has no number. The numbers are set beside the text, not in it: the HTML viewer hides them from a selection and from assistive technology, a tagged PDF paints them as artifacts. |
lineNumberColor |
ColorValue |
#8a8a8a |
The numbers' colour (and the wrap marker's). |
lineNumberGap |
Dimension |
1em |
The room between the widest number and the code; em is the code size. |
highlightBackground |
ColorValue |
#fff4c2 |
The band behind the lines a fence names with highlight="3,5-7", across the box. |
keepTogether |
boolean |
false |
As a callout style's: false splits a listing taller than the room left between lines; true moves it whole, and splits it only when it is taller than a column. |
splitMinLines |
number |
2 |
The fewest lines on each side of a split, so no part holds a single line. |
repeatTitle |
boolean |
false |
Repeat the title at the head of each part, with the "(cont.)" suffix of the document language. |
continuesMarkerEnabled / continuesMarker
|
boolean / string
|
false / "Continued" |
A mark under the last line of a part that goes on. |
titleStyle |
CalloutTitleStyleConfig |
the code face, bold, 0.9 of its size | The title row a fence's title prints (see Callout styles for the fields). |
label |
CalloutLabelConfig |
none | When set, the title prints in a label tab on the box's top edge instead (the code face and size unless the label names its own). |
highlight |
'builtin' | 'none' |
'builtin' |
Colour the tokens with the built-in tokenizer (and any registered highlighter); 'none' sets every listing in color. |
tokens |
Partial<Record<CodeTokenKind, { color?, bold?, italic? }>> |
a quiet palette | The look of each kind of token, merged kind by kind onto the defaults (see below). A palette-linked colour follows colorPalette and a part's palette. |
inline |
InlineCodeStyleConfig |
unset | Inline code in a code face (see below). Unset, inline code is set in the text's face, as before 1.23. |
A small tokenizer built into the engine names the tokens of js and ts (javascript, jsx, typescript, tsx), json, python, bash (sh, zsh, shell), console (a shell session), css, html and xml (svg), markdown and sql; a listing in any other language, or with none, is set in color. It reads the whole listing, so a comment or a string that runs over lines stays one token. In a console listing, a line that opens with a prompt ($ , % , # , > , >>> , PS …> ) is what the user typed (prompt) and every other line is what the programs printed (output).
| Kind | Default | What it names |
|---|---|---|
keyword |
#8b2c8f |
Reserved words: const, def, if, SELECT, an HTML tag name, a CSS at-rule. |
string |
#3d7a2a |
Strings, template literals, attribute values. |
number |
#985f00 |
Numbers and constants (true, None, null), colours, entities. |
comment |
#7a7f87, italic |
Comments. |
function |
#2b5fb4 |
A name followed by a parenthesis; shell builtins. |
type |
#99540a |
Types and capitalised class names, CSS selectors. |
operator |
the code's colour | Operators, shell pipes and redirections. |
punctuation |
the code's colour | Brackets, separators. |
variable |
#b23b2e |
Shell variables, JSON keys, CSS properties, HTML attributes, self. |
meta |
#985f00 |
Decorators, command-line options (-l, --all), a doctype. |
prompt |
the code's colour, bold | The typed line of a shell session. |
output |
#5c6168 |
What the programs printed. |
A host plugs its own highlighter in (Shiki, Prism, highlight.js) with registerCodeHighlighter. Configurations are serialisable data, so the function is registered with the engine, not written in codeStyle:
import { registerCodeHighlighter } from 'postext';
// fn(code, lang) returns the listing's lines, each as runs whose texts join into the line.
registerCodeHighlighter('rust', (code) => code.split('\n').map((line) => [
{ text: line, token: line.trimStart().startsWith('//') ? 'comment' : undefined },
]));
registerCodeHighlighter('*', null); // remove the one registered for every languageA highlighter registered for a language takes precedence over one registered for '*', which takes precedence over the built-in tokenizer. A run names a token kind (coloured by tokens) or a color of its own (a CSS hex). A highlighter that returns undefined, throws, or returns lines that do not join into the listing's own is passed over. Layout reads the registry when it sets a listing: register before building, and in a web worker (the Sandbox lays out in one) register inside the worker.
codeStyle.inline sets text between backticks in a code face, in a box as a chip is. A line may break at a space inside the code, or after a hyphen between a letter or digit and a letter: the box is cut in two there and nothing is added. The spaces inside the code keep the code face's width, and justifying the line never stretches them. Unset, inline code keeps the text's face.
| Property | Type | Default | Description |
|---|---|---|---|
fontFamily |
string |
codeStyle.fontFamily |
The face. |
fontSize |
Dimension |
0.9em |
em is the size of the text around it. |
color |
ColorValue |
the text's | |
bold / italic
|
boolean |
false |
On top of the text's own marks (code inside bold is bold). |
background |
ColorValue |
none | A fill behind the span. |
borderColor / borderWidth
|
ColorValue / Dimension
|
none / 0.5pt
|
An outline. |
borderRadius |
Dimension |
0.2em |
em is the span's size. |
paddingX / paddingY
|
Dimension |
0.2em with a fill or outline, else 0 / 0.1em
|
Room inside the fill; the vertical padding paints outside the line box. |
otherScripts |
'text' | 'code' |
'text' |
The face of the Arabic, Hebrew and CJK letters the code holds: the text's ('text'), since few code faces have them, or the code face ('code') for one that does. |
- One line per source line. The lines are built from the code face's own advances, not by the paragraph breaker: spaces keep their width, a run of them is kept, leading spaces indent. A listing in a right-to-left book reads left to right, its lines set from the far side of the box as a left-to-right quotation is, its numbers in the gutter on their left. In a vertical book a listing follows the vertical flow (its Latin turned sideways, as vertical text sets it) and takes no line numbers.
-
Splitting. A listing taller than the room left splits between lines across columns and pages, each part framed as a box of its own, never leaving fewer than
splitMinLineslines on a side; the rest of a wrapped line stays with it when another cut fits. -
Outputs. The canvas, the HTML viewer and the PDF paint the lines and the box as laid out. The HTML viewer keeps the spaces (
white-space: pre), so a selection copies the listing with its indentation and a line feed after each source line, without numbers or wrap markers. A tagged PDF sets each listing as a paragraph holding aCodeelement, with real space glyphs, its numbers and wrap markers as artifacts. The reflowable EPUB writes<pre><code class="language-…">with the tokens' colours and a stylesheet fromcodeStyle; the fixed-layout EPUB is the print. -
Fonts. The Sandbox and
configFontFamiliesload the code face when the text holds a fence (or the configuration has acodeStylesection), in its regular and bold weights, upright and italic; the PDF embeds the faces the lines use.
The parts property configures the part-divider pages a document opens with a :::part{number="…" title="…"} container — the “Part I — Foundations” page that groups a run of chapters. A part always occupies a page of its own: the container breaks to a fresh page of the configured parity, converts it into a single-column page whose body area comes from parts.margins, lays the opener design over the whole page, and breaks again after the closing fence so the next chapter (with its own breakBefore.parity) starts clean — with the default H1 settings that yields the classic recto part page, blank verso, chapter on the next recto.
const config: PostextConfig = {
parts: {
breakBefore: { parity: 'odd' },
breakAfter: { enabled: true, parity: 'any' },
margins: { top: { value: 9, unit: 'cm' }, left: { value: 3, unit: 'cm' }, right: { value: 3, unit: 'cm' } },
design: {
elements: [
{
kind: 'text', id: 'number', content: 'Part {numberRoman}',
fontSize: { value: 12, unit: 'pt' }, fontWeight: 600, align: 'left',
placement: { anchor: { to: 'page', edge: 'top-left' }, offset: { x: { value: 3, unit: 'cm' }, y: { value: 5, unit: 'cm' } }, size: { width: 'auto', height: 'auto' } },
},
{
kind: 'text', id: 'title', content: '{titleText}',
fontSize: { value: 28, unit: 'pt' }, fontWeight: 700, align: 'left', overflow: 'wrap',
placement: { anchor: { to: '#number', edge: 'below' }, size: { width: { value: 15, unit: 'cm' }, height: 'auto' } },
},
],
},
bodyStyle: { fontSize: { value: 11, unit: 'pt' }, numberColor: { hex: '#AA0000', model: 'hex' } },
},
};:::part{number="I" title="Foundations"}
1. The lantern and its parts
2. Trimming the wick
3. Reading the weather
:::
# The lantern and its parts| Property | Type | Default | Description |
|---|---|---|---|
page |
boolean |
true |
Whether a :::part opens a divider page. With false no page is opened and the fence's body is not set: the part's number, title and palette take effect from the next content on, with no break of their own. Typical use: htmlViewer.overrides.parts.page: false, a screen edition without section dividers. |
breakBefore.parity |
HeadingBreakParity |
'odd' |
Parity of the page the part opens on. Same values and blank-page ownership rules as the heading breakBefore: a blank inserted to reach the parity belongs to the part (its {partTitle} already resolves to the new part); the mandatory separator of 'always-*' belongs to the previous content. |
breakAfter.enabled |
boolean |
true |
Move the content after the closing fence to a fresh page. When false it continues in the part page's single column. |
breakAfter.parity |
HeadingBreakParity |
'any' |
Parity of that fresh page. Leave it at 'any' and let the next chapter's own breakBefore.parity decide whether a blank verso follows. The break is applied when the next block is placed, so a part that closes the document leaves no trailing empty page. |
margins |
PageMargins |
page margins | Body area of the part page — the single column the blocks inside the fence flow in. Each side inherits the page margin when unset; mirror swaps inner/outer on even pages exactly like the page margins. |
design |
DesignSlot |
empty | Opener design. Its container is the page trim box, so container anchors and 'page' anchors coincide and 'bleed' runs to the bleed when cut lines are on. Purely decorative: it never reserves body space — raise margins.top to keep the body clear of it. When empty, a default {number} {titleText} text in the H1 typography is synthesised at the top-left of the body area, with the H1's numberSeparator between number and title. |
versoDesign |
DesignSlot |
empty | Design of the blank verso that follows a part page (the back of the divider leaf): same container and placeholders as design. Leave empty for a plain verso. It paints only when the page after the part page is left blank, which takes a break with a parity: see The verso design below. |
bodyStyle.fontFamily, fontSize, lineHeight, color, textAlign
|
as in bodyText
|
inherit bodyText
|
Typography of the paragraphs, blockquotes and list items inside the fence. Weights, emphasis colours and hyphenation come from the body text. |
bodyStyle.bulletColor |
ColorValue |
unorderedLists.color |
Bullet colour of unordered lists inside the part. |
bodyStyle.numberColor |
ColorValue |
orderedLists.color |
Number colour of ordered lists inside the part. Numbers are always set in the body's bold weight, so a chapter list reads as a table of contents. |
bodyStyle.unorderedLists |
UnorderedListsConfig |
— | Partial overrides applied on top of the document's unorderedLists inside the part, after bulletColor. List-wide values propagate to the levels that inherited them; levels entries apply to their level only. |
bodyStyle.orderedLists |
OrderedListsConfig |
— | Partial overrides applied on top of the document's orderedLists inside the part, after numberColor and the bold weight — e.g. a separator of '•' with its own separatorFontFamily and separatorColor for a part opener's chapter list. |
The design slot resolves the heading placeholder set with the part's own values: {titleText} is the fence's title; {number} the number exactly as written; {numberDecimal}, {numberRoman}, {numberRomanLower}, {numberAlpha}, {numberAlphaLower} re-format it — the number is parsed as a decimal, a roman numeral or Chinese numerals, with or without the words that frame them ("IV", "iv", "4", "4", "四", "卷四" and "第四卷" all give {numberDecimal} = 4), and resolve to '' for anything else. {partTitle} / {partNumber}, {chapterTitle} / {chapterNumber} (the chapter before the part), {pageNumber}, {totalPages}, {bookTotalPages} and the metadata placeholders are available too. {attr.<key>} reads the current chapter's H1 attributes.
versoDesign decorates the page right after a part page when that page holds no content: the back of the divider leaf. The part itself never leaves that page blank. breakAfter.parity defaults to 'any', so the content after the fence starts on the very next page unless something asks for a parity:
-
The next chapter's heading. The default H1
breakBeforeis'always-odd', and'odd'does the same after a recto part page: the chapter moves to the next recto and the verso stays blank. The verso design paints. -
parts.breakAfter: { enabled: true, parity: 'odd' }. The part asks for the next recto itself, whatever the next heading does. Use it when chapters may open on either side (breakBefore.parity: 'any', orbreakBefore.enabled: false).
With neither, the chapter opens on the verso and no verso design is drawn. With breakAfter.enabled: false the content continues on the part page itself. The verso takes the part's palette, so a palette="band=#…" on the fence recolours it too.
parts: {
breakBefore: { parity: 'odd' },
breakAfter: { enabled: true, parity: 'odd' }, // always a blank verso to paint
versoDesign: {
elements: [{
kind: 'box', id: 'field',
style: { backgroundColor: { hex: '#b07d2b', model: 'hex', paletteId: 'band' } },
placement: { anchor: { to: 'page', edge: 'top-left' }, size: { width: 'fill', height: 'fill' } },
}],
},
}A part that closes its chapter. In a book laid out chapter by chapter (the Sandbox, buildBundle), a :::part fence can be a chapter of its own, or the end of one. Its part page is then the chapter's last page, and the next chapter takes over what the part still owes: it applies breakAfter before its first block and paints versoDesign on its first page when that page is left blank. The pages come out as they would with the whole book in one document. Only directives that place nothing (:::numbering, :::space) may follow the fence; anything else is content of the chapter, which then takes the break itself. An empty chapter right after the part is a page of its own: that page is the verso. A host that lays chapters out itself gets this from continuationAfter(), which reports afterPartPage: true for a chapter that ends with a part; pass it on in the next chapter's continuation.
A :::part{number="…" title="…"} line opens the part and a bare ::: closes it; both attributes are optional (they default to ''). The blocks in between — typically the chapter list — flow in the part page's single column with bodyStyle, starting at margins.top; a body longer than the page continues on ordinary pages. The page is classified role: 'part' (VDTPage.partInfo carries the number and title), so header and footer elements can target or skip it with pages: 'part' / pages: 'body'; the PDF backend adds the part to the outline above its chapters. An empty body (:::part{…} directly followed by :::) is the common case and still produces the page — consecutive parts never share one. A :::part nested in another part is flattened into the outer one.
A third attribute, palette="<id>=<hex>[, <id>=<hex>…]", gives the part its own colours: on the part page and on every page that follows it — until the next part — each design colour (header, footer, opener band, part and verso designs) linked to one of those palette ids takes the part's value instead of the document palette's. That is how a book's sections recolour the corner tab, the running-head dot and the chapter-opener band without a second design: :::part{number="II" title="…" palette="band=#f6c297"}. The text flow follows too: on the same pages, every flow colour equal to the base value of an overridden palette entry — headings, bold, italic and reference colours, bullets and list numbers, caption labels and caption bars, table text, rules and fills (header, body, zebra rows and a cell's own fill), callout boxes (background, border, stripe, title) and chips (fill, outline and text) — takes the part's value, so headings.levels[1].color linked to band sets each section's headings in its own colour. Inline swatches keep the colour written in them. Pairs are separated by commas, semicolons or spaces, = or : joins id and colour, the # is optional. A part stays in effect after its fence closes: {partTitle}, {partNumber} and the palette follow the flow into the chapters after it and — through continuation.part, which continuationAfter() reports — into chapters laid out on their own, so the second chapter of a section shows the section in its running heads exactly as the first does.
Two palette entries may share a base value and still take different values in a part, when the part overrides one and not the other or gives them different colours. The value alone does not say which entry a flow colour came from, so each colour of the flow is matched with the settings it can come from, and takes the value those settings link to. These are told apart:
- the colours of a block's text: the text colour, the bold, italic and reference colours, the list marker (a bullet or a number) and the separator after a number. With
bodyText.colorlinked toinkandbodyText.boldColorlinked toaccent, both#1a1a1a, a part withpalette="accent=#b8413d"recolours the bold runs and leaves the text; withunorderedLists.colorlinked toaccentinstead, it recolours the bullets and leaves the item text; - each heading level, and each heading style that sets a colour;
- a contents row's text, its number, and its page number and subtitle;
- in each callout style, the fills (background, stripe, label tab), the border, the rules (marker and label) and the text (title, icon, marker glyph, label);
- each colour of each table, chip and caption style. A named table style is apart from
tableStyle, and a resource type's caption style fromcaptionStyle. A cell's own fill follows its own link.
One case is still decided by value: settings in different places that set the same colour of a block. bodyText.color, bodyText.blockquote.color, a paragraph style's color and a callout style's body.color all set a block's text colour, for example. When two of them link to entries that share a base value and the part sets them apart, the colour takes the override (the last one written, when both are overridden). Give such entries base values of their own.
const resolved = resolvePartsConfig(config.parts, resolvedPage, resolvedBodyText, resolvedUnorderedLists, resolvedOrderedLists);
// => margins filled from the page, bodyStyle from the body / list configs
const minimal = stripPartsDefaults(config.parts);
// => undefined when only static defaults remainThe headingStyles property declares named styles a document applies to a heading with # Title {style="<id>"}. A style does two things. It overrides the heading's level typography, design and numbering — every field of a level entry except level (font, size, colour, breakBefore, span, advancedDesign, textTransform, hidden, numberingTemplate…) — and it governs the section the heading opens: its pages, up to the next heading of the same or a higher level, take the style's running heads, page geometry, body typography and palette. That is how a book's front matter (a preface in a single wide column with roman folios and blue bands) sits in a two-column, decimal-numbered manual without a second configuration.
const config: PostextConfig = {
headingStyles: [
{
id: 'front-matter',
numbered: false,
breakBefore: { enabled: true, parity: 'odd' },
span: 'page',
advancedDesign: { enabled: true, minHeight: { value: 52, unit: 'mm' }, slot: { elements: [/* bands, `{titleText}` */] } },
header: { elements: [/* folio | rule | `{title}. {subtitle}` */] },
margins: { left: { value: 50, unit: 'mm' }, right: { value: 17, unit: 'mm' } },
layout: { layoutType: 'single' },
bodyStyle: { fontSize: { value: 10.5, unit: 'pt' }, textAlign: 'justify' },
palette: { band: '#547396' },
},
],
};# Preface {style="front-matter"}| Property | Type | Default | Description |
|---|---|---|---|
id |
string |
— | Identifier referenced from {style="…"} on a heading line. An unknown id leaves the heading as it is. |
name |
string |
id |
Human-readable name (editor UI only). |
numbered |
boolean |
true |
Whether the heading counts: advances its level's counter (the numberingTemplate numbers, the {h1} of resource numbers), the chapter ordinal behind {chapterNumber} and the number printed in the contents. false for a preface, an authors list, an index: the first numbered chapter after them is still chapter 1, and {chapterNumber} is empty on their pages. |
toc |
boolean |
true |
Whether :::toc lists the heading. A heading overrides it with {toc="false"} / {toc="true"}. |
runningChapter |
boolean |
true |
Whether a level-1 heading of this style becomes the running chapter: the chapter that {chapterTitle}, {chapterNumber}, {attr.<key>} and their …AtTop forms name on its page and the pages after it. false for a plate, a map or a cover set as an H1 inside a chapter: the running heads pass over it, on its own page too, and keep naming the chapter it interrupts; it sets no h1 guide word either. The heading still counts when numbered (its own design reads its own {chapterNumber}) and is still listed by :::toc when toc. With toc: false as well it gets no PDF bookmark: a chapter's plate on the page before its opener leaves the bookmarks to the chapters. Headings of other levels ignore it. With the default, the pages after a plate print the plate's title. A preface or a prologue with numbered: false is still a chapter of its own and keeps the default. The option changes only which chapter the placeholders name: the style still opens a section of its own, as every heading style does, so up to the next level-1 heading the pages take the plate style's running-head slots, margins, columns, body style and palette (the document's where the style sets none), not those of a styled section the interrupted chapter opened. In a book laid out chapter by chapter, the running heads do not carry over from one chapter file to the next, so a plate that opens a file shows empty chapter placeholders until the file's first chapter heading. |
| level fields | as in headings.levels[]
|
the level's values |
fontFamily, fontSize, lineHeight, fontWeight, italic, color, marginTop, marginBottom, snapToGrid, breakBefore, span, advancedDesign, textTransform, letterSpacing, lineSpan, indent, firstLineIndent, jidori, dropCap, hidden: each one set replaces the heading level's value for headings of this style (dropCap: false takes the level's drop cap off). breakBefore merges field by field over the level's: a style that only sets parity keeps the level's enabled, and one that only sets enabled: true keeps its parity (up to postext 1.4, the missing field came from the no-break default instead). |
numberingTemplate |
string |
the level's | Template the style's headings are numbered with, in place of their level's (same tokens as levels[].numberingTemplate). The counter stays the level's: an appendix style with 'Appendix {1:A}' after five chapters would print Appendix F, so restart the count with {startAt=1} on the first appendix. '' prints no number while the heading still counts: not even the chapter ordinal the contents and {chapterNumber} show for a level-1 heading without a template. The number shows in the flow, the {number} of the style's design, the contents and {chapterNumber}. |
header, footer
|
DesignSlot |
the document's | Running heads of the section's pages, replacing header / footer there (element parity and pages filters still apply). An empty slot removes them. |
margins |
PageMargins |
page margins | Body area of the section's pages; each side inherits the page margin when unset, mirror included. Takes effect on the pages the section opens — pair it with breakBefore. |
layout |
LayoutConfig |
layout |
Column layout of the section's pages (layoutType, gutterWidth…): a single wide column for a preface set in a two-column book. Its columnRule is drawn on the section's pages, and each field it leaves unset takes the document's layout.columnRule value, so a section that only changes its columns keeps the document's rule (see Column rule). |
bodyStyle |
PartsBodyStyleConfig |
inherit bodyText
|
Typography of the paragraphs, blockquotes and lists in the section — the same fields as parts.bodyStyle. |
palette |
Record<string, string> |
{} |
Palette overrides (id → hex) for the section's pages, on top of the current part's — the same mechanism as a part's palette attribute, and with the same reach: not only the design slots laid out on those pages (running heads, the opener band — every colour linked to an overridden id) but the text flow too, by value: every flow colour equal to the base value of an overridden entry — headings, bold, italic and reference colours, bullets and list numbers, caption labels and caption bars, table text, rules and fills, callout boxes (background, border, stripe, title) and chips (fill, outline and text) — takes the section's value, as it does under a part, including the rule for two entries that share a base value (see The :::part container). Inline swatches keep the colour written in them. The page colour follows as well: when page.backgroundColor links to an overridden entry (or, unlinked, has its base value), the section's pages are painted in the section's value, so a newspaper's business pages print on salmon while the rest stay white. Since postext 1.18. |
A section closes at the next heading of the same or a higher level: an unstyled # after a styled one returns to the document's running heads and geometry; a styled one opens its own section. Pages the section left blank for parity belong to it, like chapter titles do.
A page break does not stand in for the heading's own break. A style inherits its level's breakBefore (the level-1 default is { enabled: true, parity: 'always-odd' }), and a heading applies it wherever it stands, right after a :::pagebreak too. The page break opens a new page, and the heading then still asks for its side of the spread. With parity: 'odd', a break that lands on a verso is followed by a blank page, so the heading opens on the next recto. With 'always-odd' the separator blank comes too, and the page break changes nothing, since the heading would have opened that page anyway. A style meant to start on the page a manual break opens, such as a contents page after the title page, turns its own break off:
headingStyles: [
// Starts where the text puts it: on the page the `:::pagebreak` before it opened.
{ id: 'contents', numbered: false, toc: false, breakBefore: { enabled: false } },
],To keep a page of its own without choosing a side, set breakBefore: { parity: 'any' } instead and leave the :::pagebreak out.
Which section a page belongs to. Running heads and palette are chosen per page, not per heading. A page takes the section in effect after the last section change on it: where one section ends and another starts on the same page — two short letters of a dictionary, say — the page carries the second one's running heads and palette; where a styled section ends mid-page at an unstyled heading, the page returns to the document's. {chapterTitle} follows the same rule: a page where two chapters meet shows the later one's title. Blank pages follow the rule of chapter titles: a parity blank (blankForParity) belongs to the section that opens after it, and the separator an 'always-odd' / 'always-even' break adds (blankForForce) to the section before it. A part divider closes the open section.
# A {style="letter"}
Aardvark, abacus.
# B {style="letter"}
Babble, badger… (runs on to the next page)Both letters start on page 1, so page 1 takes the running heads of the B section: a thumb tab set in the style's header reads "B" there, and no page carries the tab for "A". Give each section a page of its own (breakBefore) when every one needs its tab.
Lettered appendices after numbered chapters, and a dedication page that the contents and the PDF bookmarks list but the page does not title:
headingStyles: [
{ id: 'appendix', numberingTemplate: 'Appendix {1:A}' },
{ id: 'silent', hidden: true, numbered: false },
],# Dedication {style="silent"}
For M., who read every draft.
# Method
…
# Survey instrument {style="appendix" startAt=1}
# Raw data {style="appendix"}With numberingTemplate: '{1}.' on level 1, the chapters print 1., 2.…, the appendices Appendix A and Appendix B. The dedication opens its page (its level's breakBefore), prints only its paragraph, and still shows as Dedication in :::toc, in {chapterTitle} running heads and in the PDF outline — add toc: false to the style to leave it out of the contents. A plate or a map set as an H1 in the middle of a chapter wants the opposite of the dedication: a style with runningChapter: false (usually with numbered: false and toc: false) keeps the running heads on the chapter it interrupts.
const resolved = resolveHeadingStylesConfig(config.headingStyles, resolvedPage, resolvedBodyText, resolvedUnorderedLists, resolvedOrderedLists);
// => level overrides normalised, margins filled from the page, bodyStyle from the body
const minimal = stripHeadingStylesDefaults(config.headingStyles);
// => undefined when no style remains← Configuration: resources, tables, captions, diagrams and videos · Configuration: callout boxes →
Postext is MIT-licensed. Maintained by Ignacio Ferro and community contributors. See the live documentation for the most up-to-date reference.
Overview
Writing
- Document Format
- Hyphenation and Justification
- Chinese Layout
- Arabic Layout
- Japanese Layout
- Comics
- Composed Pages
- Sandbox
- Agent Skill
- Command Line
- WebMCP
Configuration
- Overview and index
- Page and layout
- Headers and footers
- Body text
- Hyphenation and language
- Headings
- Widows, orphans and balancing
- Lists and maths
- Notes and references
- East Asian typography
- Resources
- Styles and parts
- Callout boxes
- Comics
- Composed pages
- Fonts, colours, viewers
Code
Internals
External