Chapter 5 · Part II · The craft
Configuration: text and headings
Body text typography, hyphenation and the document language; headings, bulleted and numbered lists, and maths
In short
This page covers the settings for the words on the page. You choose the font, the size and the line spacing of the main text, and how words are split at the end of a line. You tell Postext which language the book is written in. You set how each level of heading looks, and how bulleted and numbered lists are drawn. The last section sets the size and the colour of mathematical formulas.
#Body text
The bodyText property controls the typography of all paragraph text.
| Property | Type | Default | Description |
|---|---|---|---|
fontFamily | string | 'EB Garamond' | Font family for body text. Any Google Font, system font, or custom family declared in customFonts. One family, not a CSS font stack (see below). |
fontSize | Dimension | 8 pt | Base font size for body text. |
lineHeight | Dimension | 1.5 em | Vertical spacing between lines. Relative units (em, rem) scale with font size. |
paragraphSpacing | boolean | false | When enabled, inserts a blank line (equal to lineHeight) between consecutive paragraphs for publisher-style separation. |
color | ColorValue | #000000 | Text color. |
boldColor | ColorValue | Main Color (#295AA3) | Color applied to bold/strong spans. Resolved against the default palette's main-color entry, so changing the palette colour retints all bold runs across the document. |
italicColor | ColorValue | Main Color (#295AA3) | Color applied to italic/emphasis spans. Same palette-linked default as boldColor. |
referenceColor | ColorValue | Main Color (#295AA3) | Color applied to inline :ref labels (resource references). Same palette-linked default as boldColor. It follows the palette since postext 1.5; up to 1.4 it stayed #295AA3 whatever the Main Color was. |
referenceBold | boolean | true | Render inline :ref labels with the bold font. A reference keeps the emphasis of the text around it (inside … it stays bold italic with either value); this option only adds bold on top. |
referenceItalic | boolean | false | Render inline :ref labels in italics. A reference inside italic text stays italic with either value. |
emphasis | 'auto' | 'italic' | 'bold' | 'color' | 'overline' | 'auto' | How … is set: in italics, in the bold face and boldColor, upright in italicColor, or upright with a rule over the words. 'auto' is 'bold' in a document written in Arabic script and 'italic' in any other. See Arabic text. |
tashkil | 'keep' | 'strip' | 'strip-vowels' | 'keep' | Arabic vowel marks: set as written, all taken out, or taken out except the shadda. See Arabic text. |
textAlign | 'left' | 'justify' | 'start' | 'end' | 'justify' | Text alignment. 'left' (or 'start') is the side a line starts on: the right of a right-to-left paragraph (see Text direction). Justified text distributes spacing across each line for even edges. Last lines of justified paragraphs render ragged at their natural width — except when Knuth-Plass accepted an overfull final line relying on glue shrink, in which case the inter-word spaces compress so the line fits the measure exactly (TeX glue-setting semantics, applied identically in the canvas, HTML, and PDF backends). |
fontWeight | number | 400 | Weight for normal text (100–900). |
boldFontWeight | number | 700 | Weight for bold/strong text (100–900). |
hyphenation | HyphenationConfig | enabled, 'en-us' | Automatic hyphenation settings. See below. |
firstLineIndent | Dimension | 1.5em | Indent applied to the first line of each paragraph (or to all lines except the first when hanging indent is enabled). |
hangingIndent | boolean | false | When enabled, the indent is applied to all lines except the first (French/hanging indent). |
indentAfterHeading | boolean | true | When set to false, the first paragraph immediately following a heading is rendered without first-line indent — a typographic convention common in scientific publications and many book styles. The same applies to a paragraph right after a :::space line. A box set outside the text between them — in the side column (span: 'side'), floated to the head or foot of a page, or fixed — and a floated figure are looked past: in its column the paragraph still follows the heading, and is set flush. A box set in the text (placement: 'here') counts, and the paragraph after it is indented. Has no effect when hangingIndent is enabled. |
maxWordSpacing | number | 2 | Upper bound for word spacing in justified text, expressed as a multiplier of the normal space width. Knuth-Plass keeps every line within it that the paragraph allows, hyphenating a word or spreading the slack over the neighbouring lines first; a line no set of breaks keeps within it stretches past it, and one past 3× the normal space is set ragged. Lines exceeding this ratio are considered "loose": see Lines the breaker cannot fill, and maxJustifyTracking to let them take a little tracking instead. |
minWordSpacing | number | 0.6 | Lower bound for word spacing in justified text, as a multiplier of the normal space width. |
maxJustifyTracking | number | 0 | Most tracking a justified line may take, in thousandths of an em either way (the InDesign unit: 10 = 0.01 em per character), when its word spaces alone would stretch past maxWordSpacing or shrink past minWordSpacing. The part of the adjustment beyond the limit goes into the letters, so a loose line's spaces come back to maxWordSpacing and a tight one fits at minWordSpacing. Knuth-Plass weighs it when it chooses the breaks, and only for the lines word spacing alone would take past the limits: the others, a paragraph's last line (unless it runs over), a line of one word and a line holding a chip take none. The line records it as letterSpacing, and the canvas, HTML and PDF paint it. Needs optimalLineBreaking. 0 turns it off. See Tracking as a last resort. |
kashida | 'auto' | 'none' | 'auto' in an Arabic-script document, else 'none' | Kashida justification: a justified line of Arabic-script text takes its slack in its word spaces (up to a quarter of their width) and then in kashidas, whole tatweels (U+0640) inserted between two joined letters, never as letter-spacing. Knuth-Plass counts each word's elongation as stretch. Never in Latin words, digits, headings, ragged lines or a paragraph's last line. The tatweels are painted but left out of plain and copied text. See Kashida in Arabic text. |
kashidaPatterns | 'auto' | 'naskh' | 'simple' | 'nastaliq' | 'auto' | Which joins take a kashida, and in what order: the classical Naskh rules, the Microsoft priorities, or the Naskh rules tailored for Nastaʿlīq (after raqim-kashida). 'auto' reads the body font: none in a Ruqʿa or Dīwānī face (Aref Ruqaa), Nastaʿlīq rules in a Nastaʿlīq face, Naskh otherwise. |
kashidaPerWord | number | 1 | Most elongations in one word. |
kashidaMaxLength | number | 0.6 | Longest elongation at one join, in ems; it takes as many whole tatweels as fit. |
optimalLineBreaking | boolean | true | Use Knuth-Plass optimal line breaking instead of greedy first-fit. Produces more even word spacing across the paragraph. A Chinese, Japanese or Korean paragraph (see East Asian typography), or one with a word wider than the column, is still set line by line; a Latin paragraph that quotes a few CJK words keeps it. Ragged text takes it too with optimalRagged. See Hyphenation & Justification. |
optimalRagged | boolean | true | Break ragged running text with Knuth-Plass too: body text, blockquotes and list items set left, right or centred, and ragged paragraph styles, box bodies and the bodies of parts and section styles. Word spaces keep their width. The breaker weighs how far each line falls short of the measure (a line 3 em short costs what a justified line at maxWordSpacing does), so it evens out the edge instead of filling each line before the next, and the runt rules (avoidRunts, tightenRunts) and hyphenateAcrossColumns work on ragged text as on justified text. With hyphenation.ragged the zone still decides which syllables may end a line (see Ragged text). Ragged headings, captions, notes, table cells and the contents are still set line by line. Needs optimalLineBreaking. false sets ragged text line by line, as up to postext 1.4; configurations stored earlier that set some running text ragged read with it (see Bundles written by postext 1.4 or earlier). |
breakAfterDashes | boolean | true | Let a line end after an em or en dash set closed between words: say—that’s, riddles.—I, Hamburg–Berlin, also where the word after the dash is set in another style (see—and). Knuth-Plass takes it like a word space, and the line ends on the dash with nothing added. Never after a dash that opens an aside or a line of dialogue (—dijo, said "—Hola, sagte »—Ich: a space, or a space and a quotation mark, before the dash; after a quote that closes a word, as in "no"—and, German „nein“—und or French « non »—et, the line may end), before punctuation (él—,), before a quotation mark or a bracket (thinking—" and, says—“no”, says—(no): a quote after a dash often closes the speech the dash broke off), inside a run of dashes, or inside a range of numbers set with an en dash (1914–1918). false keeps the 1.4 breaks: Knuth-Plass never breaks after a dash, and the line-by-line breaker of formatted or hyphenated ragged text only between two letters; configurations stored earlier whose text sets such a dash read with it (see Bundles written by postext 1.4 or earlier). It applies to the running text, headings, lists, blockquotes and boxes. Captions, notes, table cells and the contents keep the 1.4 breaks, and a plain ragged paragraph set line by line follows pretext's own rules either way. |
breakAfterHyphens | boolean | true | Let a line end after the hyphen of a compound, a hyphen between two letters (well- · known, vencer- · se), in every paragraph Knuth-Plass breaks. The line ends on the hyphen and nothing is added; the break is priced like a syllable. Never after a hyphen next to a digit or a sign (COVID-19, -5 °C). A justified paragraph without inline formatting breaks there only with two letters on each side of the hyphen, so no line ends on the e- of e-mail. false keeps the 1.4 breaks: a justified paragraph without inline formatting never breaks there, while the same paragraph with one italic word anywhere, a ragged paragraph and a paragraph set line by line do; configurations stored earlier whose text sets a compound read with it (see Bundles written by postext 1.4 or earlier). It applies to the running text, headings, lists, blockquotes and boxes. See Compound words. |
repeatHyphen | boolean | false | Start the line after a break at a compound's hyphen with a hyphen too: vencer- · -se, as Portuguese spelling asks, and léxico- · -semántico, as the Spanish Academy's rules ask since 2010. The repeated hyphen is measured and painted with its line, which records it as repeatedHyphen; its plainStart and sourceStart point past it, so links, running heads and the Sandbox read the word as written. The PDF paints it under an /ActualText that leaves it out, so text copied or extracted from the PDF reads the word once. A web address never gets one. It applies to the running text, headings, lists, blockquotes and boxes; a paragraph without formatting that holds a compound is then broken by the breaker that sets formatted text. |
hardLineBreaks | boolean | true | Read a backslash that ends a source line, and before a space, as a forced line break inside a paragraph, a quotation or a list item (CommonMark's hard break): the next words start a new line of the same paragraph, and the line before is set at its natural width, as a last line is. A backslash ending the paragraph prints, and two spaces at the end of a line are no break (see Document format › Line breaks). false keeps the 1.22 reading: the backslashes print and the lines join with a space; configurations stored earlier whose text has such a backslash read with it (see Bundles written by postext 1.4 or earlier). Titles, captions, notes and table cells break at either way. |
tabStops | TabStop[] | unset | Tab stops of every paragraph, list item and quotation: where a tab (:tab, or a tab character in the text) sends the words after it. A paragraph style or a callout body that sets its own replaces them. Unset, a tab character is a word space, as it was before, and a :tab with no stop to go to is one too. See Tab stops. Since postext 1.23. |
tabInterval | Dimension | unset | Default stops every interval from the start of the measure, past the last of tabStops. Unset, a tab past the last stop is a word space. Since postext 1.23. |
blockquote | BlockquoteConfig | see below | How Markdown blockquotes (> …) are set: colour, italics and indents. See Blockquotes. |
#Blockquotes
A blockquote (lines that start with >) takes the body's family, size, leading, weights, alignment and hyphenation. bodyText.blockquote sets the rest; left unset, a blockquote looks as it did up to postext 1.4: grey, italic, with the body's first-line indent and no side indent.
| Property | Type | Default | Description |
|---|---|---|---|
color | ColorValue | #666666 | Text colour. A colour linked to a palette entry (paletteId) follows that entry, as everywhere; the body's bold, italic and reference colours do not apply inside a blockquote. |
italic | boolean | true | Set the text in italics. A … run inside turns back to upright; with false it is italic as in a paragraph. |
indent | Dimension | 0 | Indent of every line from the left edge of the column or box. The measure narrows by it, so justified lines end at the right edge. em is the body's size. |
firstLineIndent | Dimension | the body's | Indent of the first line of each quoted paragraph, counted from indent (with the body's hangingIndent, the indent of every line but the first). Unset: bodyText.firstLineIndent. |
bodyText: {
firstLineIndent: { value: 1.5, unit: 'em' },
// Upright verse in the body colour, set in by 2 em, no first-line indent.
blockquote: { color: { hex: '#241f26', model: 'hex' }, italic: false, indent: { value: 2, unit: 'em' }, firstLineIndent: { value: 0, unit: 'em' } },
}In the Sandbox these are the Blockquotes group of the Body text section.
#Verse
A :::verse poem whose lines carry no hemistich separator is set line by line (see Document format › :::verse). bodyText.verse gives the defaults of such poems; an attribute of the same name on a poem's fence sets it for that poem.
| Property | Type | Default | Description |
|---|---|---|---|
layout | 'auto' | 'bayt' | 'auto' | How a poem whose fence names no layout is set. 'auto': as bayts when a line carries a separator (||), line by line otherwise. 'bayt': as bayts always, a poem with no separator as centred single hemistichs, as up to postext 1.22. |
indentStep | Dimension | 0.5em | Width of one leading space of a line of verse (a tab counts four, an ideographic space two); em is the poem's size. |
turnover | 'hang' | 'right' | 'hang' | How a line wider than the measure turns over: hanging on the next line, or flush with the end of the measure behind turnoverMark. |
hang | Dimension | 2em | Indent of a hanging turnover from its line's start, when the poem's paragraph style sets no hangingIndent. |
turnoverMark | string | '[' | The mark before a turnover set flush right. |
stanzaSpace | number | 1 | Space between stanzas, in lines of the poem's leading. On the baseline grid it comes out whole grid lines; a paragraph style with snapToGrid: false keeps it exact. |
keepStanzas | number | 0 | Keep any stanza of this many lines of verse or fewer whole in one column. 0 is off. |
tighten | boolean | true | Tighten the word spaces of a line a little wider than the measure, down to bodyText.minWordSpacing of their natural width, and keep it on one line; only a line still too wide turns over. Letters are never tracked for it. false turns every line wider than the measure over, as postext 1.23 did. The reflowable EPUB, whose lines the reading system breaks, does not tighten. |
bodyText: {
// Turnovers flush right behind a bracket; tankas kept whole.
verse: { turnover: 'right', keepStanzas: 5 },
}A configuration stored before these settings (configVersion 8 or older) that lays out a poem with no separator is read with layout: 'bayt', so the poem keeps the centred lines postext 1.22 gave it. One stored by postext 1.23 (configVersion 9) that lays out a poem line by line is read with tighten: false, so its lines turn over where 1.23 turned them.
In the Sandbox these are the Verse group of the Body text section.
#Tab stops
bodyText.tabStops lists the stops a tab goes to: :tab, or a tab character in a paragraph that has stops (see Document format › Tabs and tab stops for the markup and how a line is set). A paragraph style's tabStops replace them for its paragraphs and a callout style's body.tabStops inside its boxes; an empty list sets none. Each stop is a TabStop:
bodyText: {
tabStops: [
{ position: { value: 30, unit: 'mm' } },
{ position: 'end', align: 'end', leader: '.', leaderGap: { value: 2, unit: 'pt' } },
],
tabInterval: { value: 10, unit: 'mm' },
}| Property | Type | Default | Description |
|---|---|---|---|
position | | required | Where the stop stands, from the start edge of the paragraph's measure (after a paragraph style's indent; em is the paragraph's own size): a length, 'end' for the end edge of the measure, or a share of the measure ('50%'). In right-to-left text the start edge is the right one. |
align | 'start' | 'end' | 'center' | 'decimal' | 'start' | How the text after the tab sits at the stop: it starts there, ends there (the text up to the next tab or the end of the paragraph), is centred on it, or has its decimal separator on it (a run with none ends there). |
leader | string | none | Repeated over the room before the stop: '.', '. ', '·', '_', '-' or any short text, in the paragraph's face and colour; 'rule' draws a line a little under the baseline. The leader ends flush with the end of its room, so the leaders of several lines line up. It is measured as a whole run, as a contents row's is. |
leaderGap | Dimension | 0.5em | Room kept between the leader and the text on either side, as the contents' leader.gap. None before the leader when the tab opens its line, none after it when nothing follows. |
decimalChar | string | the document language's | The separator a 'decimal' stop aligns on: . in English, Chinese, Japanese and Arabic, , in Spanish, Catalan, Portuguese, French, German and others. |
tabInterval adds stops every interval from the start of the measure, past the last stop of the list; without it a tab past the last stop is a word space. A tab character typed in the text is a tab only in a paragraph whose style has stops or an interval, its own or the body's; everywhere else it stays the word space it always was, so a stored document lays out as before and needs no pin.
The stops are checked with the rest of the configuration (see Configuration warnings): an unknown key in a stop (leaders) is reported as unknownConfigKey with the key it is closest to; an align that is none of the four as unknownConfigValue, and the stop is set as 'start'; a position that is no length, 'end' or percentage as unknownConfigValue with used: 'none', and the stop is left out.
The leader colour and face are the paragraph's; there is no setting of their own yet.
#Drop caps
A body paragraph can open with a drop cap: its first letter set large beside its first lines, which are shortened by the letter's width and a gap and set like any other lines (broken by Knuth–Plass, justified, hyphenated). A paragraph style gives one to the first paragraph of each :::paragraphs group in the style (every paragraph with each: true, for a catalogue of entries); a heading level or a heading style gives one to the first body paragraph after the heading (see the dropCap field of Per-level overrides, Paragraph styles and Heading styles). The paragraph after the heading is found past container fences, directives, boxes that left the flow and floated figures, as indentAfterHeading finds it; a paragraph inside a box, a list item, a quotation, a poem or a note never takes one. In the text, {dropcap}, {dropcap=false} and {dropcap=N} on a heading line or a :::paragraphs fence turn it on, off, or set its lines for that chapter or group (see Document format › Drop caps). Since postext 1.23.
headings: {
levels: [
{ level: 1, dropCap: { lines: 3, fontFamily: 'Libre Bodoni', fontWeight: 700, color: { hex: '#8b2e2a', model: 'hex', paletteId: 'accent' }, leadIn: { words: 3 } } },
],
},| Property | Type | Default | Description |
|---|---|---|---|
lines | number | 3 | Lines the initial spans, from the top of its capitals to its baseline. 1 with a larger fontSize is a raised initial standing on the first baseline. |
sink | number | lines | Lines the initial drops into the text: it stands on the baseline of line sink, and those lines are shortened. Fewer than lines raises it above the first line. |
characters | number | 1 | Grapheme clusters set large: É, a letter with a combining mark or a character outside the Basic Multilingual Plane counts as one. No further than the first word. |
fontFamily, fontWeight, italic | string, number, boolean | the paragraph's; false | Face of the initial. It is loaded and embedded with the document's other faces. |
fontSize | Dimension | see below | Size of the initial. Unset: the size that sets the top of its capitals level with the capitals of the first line while it stands lines lines down. |
color | ColorValue | the paragraph's | A palette-linked colour follows part and section palettes. |
gap | Dimension | 0.15em | Space between the initial and the shortened lines, em being the text size. |
punctuation | 'with-cap' | 'hang' | 'text' | 'with-cap' | An opening quote, ¿, ¡ or bracket before the letter: set large as part of the initial, hung at text size outside the measure before it, or set at text size at the start of the first line, after the initial. |
leadIn | { words, smallCaps, uppercase } | none | The first words words after the initial in small capitals (the default) or in capitals (uppercase: true); words: 'line' takes the words of the first line. A lead-in of the first line is counted on a first setting and set on a second: in small capitals the line may then hold a word more, set as written. |
shortParagraph | 'reserve' | 'shrink' | 'skip' | 'reserve' | A paragraph of fewer lines than sink: keep the block sink lines tall so the next block clears the initial, set the initial over the paragraph's own lines, or set the paragraph without one. Each case raises a dropCap content warning. |
each | boolean | false | Paragraph styles only: every paragraph of the group opens with the drop cap, not the first only. |
How the letter is set:
- Size. Both cap heights, the text's and the initial's, are measured from their faces (the ink of an
H; of the initial itself in Chinese or Japanese text), and a face that gives no ink metrics counts its capitals as 0.72 of its size. The default size suits any pair of faces, so a per-face correction is not needed. A design text'sdropCapkeeps its fixed 0.72 (see Text elements). - The letter and its word. The initial takes whole grapheme clusters. The rest of the first word goes on after it with no space; a one-letter word (A, Y) keeps its word space at the start of the first line. The paragraph's own first-line indent is dropped. Copying, search, the source map and the Sandbox's caret read the paragraph as written: the block's plain text includes the letter, the first line's
plainStartcounts past it, andVDTBlock.dropCapholds its range and geometry on the fragment with the first line. - A raised initial (
lines: 1with a largerfontSize, orsinkunderlines) rises above the first line, and the paragraph keeps that rise clear above it, in whole grid lines, so it does not overprint the text above. - Breaking. The paragraph never breaks before line
sink: it moves on whole to the next column or page instead, unless it is alone in an empty column that cannot hold those lines, where it breaks all the same and raises adropCapwarning. A heading kept with its text keeps those lines under it. The part that goes on in the next column carries no initial and full-width lines; broken again for a column of another width, the first lines keep their indent. Column balancing may set the paragraph loose or tight like any other, and never adds space between the shortened lines. - Scripts. The lines are shortened on the start side: the left in Latin text, the right in Arabic and Hebrew. A letter that joins the next one (Arabic, Syriac, N'Ko) is not set apart, and the paragraph raises a
dropCapwarning; horizontal Chinese and Japanese take a one-character initial (首字下沉). Vertical text takes none (warning). A paragraph that opens with no letter or digit (a reference, a formula, a note mark) takes none either (warning). - Outputs. The canvas, the HTML viewer and fixed EPUB paint the initial beside the lines; the HTML writes it right before the first line's text with nothing between, so a screen reader reads the word whole. In a tagged PDF the initial is part of the paragraph's
P, and with the rest of its word aSpanwhose/ActualTextis the word: text extraction reads Long before, never L ong before. A reflowable EPUB sets it as a CSSinitial-letter(lines, sink), floated where the reading system does not support it.
The settings are checked with the rest of the configuration (see Configuration warnings): an unknown key in a drop cap or its lead-in is reported as unknownConfigKey, a punctuation or shortParagraph that is none of its words, or a lines, sink or characters that is no whole number from 1, as unknownConfigValue, and the default is used. Drop caps are off by default, so no stored document changes.
#One family per fontFamily
fontFamily — here and in every other font-family field (headings.fontFamily, tableStyle.bodyFontFamily, separatorFontFamily, a chip style's or a design element's fontFamily…) — names one family. The canvas, the HTML output and the PDF must set the same face, and the PDF embeds one font per family with no fallback chain, so there is nothing for a CSS font stack to fall back to. A stack is set in its first family and reported as a configuration warning:
bodyText: { fontFamily: "'EB Garamond', Georgia, serif" } // set in EB GaramondLoad that family before laying out (see Custom fonts and the font provider under Generating PDFs); if it is missing, the browser measures with its default font, whatever the rest of the stack says. A comma inside quotes is part of a name ('"Foo, Bar"' is one family).
#Hyphenation
When text alignment is set to 'justify', hyphenation prevents excessive word spacing by breaking long words at syllable boundaries. The engine uses TeX/Liang patterns to find natural break points at syllable boundaries. See Hyphenation & Justification for a detailed explanation. Ragged text is only hyphenated when you ask for it: see Ragged text below.
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Whether to allow hyphenation. |
locale | LocaleTag | top-level locale, else 'en-us' | Language rules for syllable boundaries: one of the supported locales below, or any BCP 47 tag ('es-ES', 'pt-BR'). |
ragged | boolean | false | Also hyphenate ragged text (left, right or centre aligned), within the zone. See Ragged text. |
zone | Dimension | 3em | Hyphenation zone of ragged text: a word that does not fit is divided only when sending it whole to the next line would leave a gap wider than this. em is relative to the text's own font size. Ignored for justified text. |
compounds | boolean | true | Let the dictionary divide the words of a compound, a word with a hyphen between two letters (af-ter-dinner). false keeps such a word whole but for its own hyphen, where the line may still end (after- · dinner), as TeX does. A soft hyphen typed in the word still breaks, and a compound wider than the whole line is still divided. It applies to the running text, headings, lists, blockquotes and boxes; captions, notes, table cells and the contents keep dividing compounds. See Compound words. |
Supported locales: 'en-us' (English), 'es' (Spanish), 'fr' (French), 'de' (German), 'it' (Italian), 'pt' (Portuguese), 'ca' (Catalan), 'nl' (Dutch).
Region, script and variant subtags are ignored when the patterns are picked, and so are letter case and _ separators: 'es-ES', 'es-MX' and 'es_419' hyphenate with 'es', 'pt-BR' with 'pt', and every English tag ('en', 'en-GB') with 'en-us' — the only English patterns bundled, so British text gets American breaks. A language with no bundled patterns ('sv', 'pl', 'fi'…) is hyphenated with the 'en-us' patterns, which gives wrong breaks rather than none; the engine reports it once per tag with a console.warn, and the Sandbox lists it in its Checks panel. Set enabled: false for such a document; that silences the warning too. Chinese, Japanese and Korean (zh, ja, ko, whatever the region or script) need no patterns and print no warning: a document in one of them is set without hyphenation. To divide the Latin words quoted in it, set enabled: true and name their language in locale ('en-us' for English). enabled: true with no locale, or with a Chinese, Japanese or Korean one, leaves hyphenation off and says so once on the console. matchHyphenationLocale(tag) returns the bundled locale a tag maps to (undefined when there is none), and HYPHENATION_LOCALES lists the bundled ones. The resolved configuration (doc.config.bodyText.hyphenation) names the patterns actually used in locale, and keeps the tag you gave in tag when it differs. The PDF backend declares the document language from the top-level locale, and from this tag only when locale is unset (see Document language).
import { matchHyphenationLocale } from 'postext';
matchHyphenationLocale('es-MX'); // 'es'
matchHyphenationLocale('en-GB'); // 'en-us'
matchHyphenationLocale('sv'); // undefined: hyphenated with 'en-us', with a console warningWords shorter than 5 characters are never hyphenated. The engine requires at least 2 characters before and 3 characters after a break point.
Ragged text
By default only justified text is hyphenated: with textAlign: 'left', and in paragraph styles set left, centred or right, every word is kept whole however ragged the edge gets, except at a soft hyphen (U+00AD) typed in the text. Set hyphenation.ragged: true to hyphenate ragged text too.
A ragged line is never stretched, so dividing every word that does not fit would fringe the edge with hyphens. The hyphenation zone limits it, the way a desktop-publishing single-line composer does. When a word does not fit at the end of a line, the engine looks at the gap that sending it whole to the next line would leave. If that gap is wider than zone, the word is divided at the last syllable that fits; otherwise it goes down whole. A zone in em is relative to the text's own font size. The default, 3em, divides only the words that would leave a noticeably short line. A wider zone gives fewer hyphens and a more ragged edge; 0 divides every word that does not fit. Set line by line, no more than two lines in a row end on a syllable. Hard hyphens (enseñanza-aprendizaje), URL joints, soft hyphens typed in the text and words wider than the whole line break as they always do: the zone and the two-line limit only govern the dictionary's syllables.
The setting is document-wide. It applies to the body text and blockquotes when they are ragged, and to every ragged paragraph style and callout body whose own hyphenation is on (it defaults to the body's hyphenation.enabled), so hyphenation: false keeps one style's words whole. Headings, captions, notes, table cells and the table of contents are not hyphenated; there, as everywhere, only a word wider than the whole measure is divided. Design text — running heads, openers, part pages — follows each text element's own hyphenate flag, which the built-in full-page openers and part pages set, and uses the document's dictionary as well. Justified text ignores ragged and zone.
const config: PostextConfig = {
locale: 'es',
bodyText: {
textAlign: 'left',
// A little more hyphenation than the 3 em default.
hyphenation: { ragged: true, zone: { value: 2, unit: 'em' } },
},
};With optimalRagged (the default) ragged running text is broken with Knuth-Plass, and the zone keeps its meaning there: a word is divided only when it does not fit the rest of the line and sending it down whole would leave more than the zone empty. Two syllables in a row are not refused but cost what two hyphens in a row cost in justified text, so a third is rare. With optimalRagged: false, or optimalLineBreaking: false, each line is filled before the next, as up to postext 1.4.
Ragged hyphenation lays paragraphs out with the line breaker that paragraphs with inline formatting (bold, italics, links, maths) always use, so turning it on can move a few breaks other than hyphens. On a ragged line, besides spaces and the dictionary's syllables, that breaker breaks after a hard hyphen or a dash between two words (largas—separadas; with breakAfterDashes, any dash set closed between words, riddles.—I and riddles—*and* too), at URL joints and between ideographs, and it keeps a word set in several runs together (**Nota**:, (*véase*). Without ragged hyphenation, a ragged paragraph with no formatting is broken by Knuth-Plass when optimalRagged is on (the default): at word spaces, after a hard hyphen between two letters (meta- · analyses), as the other breaker does, and, with breakAfterDashes, after closed dashes. Set line by line, it goes through pretext's breaker, which differs in three ways: it may also break before a dash that closes an aside or after one that opens it (él · — y), after a slash when hyphenating (km/ · h), and it cuts a word wider than the line at any character without a hyphen, where the other breaker divides it at a syllable first.
In the Sandbox the switch is Hyphenate ragged text, under the paragraph alignment of the Body text section, with the zone below it. Under a justified body the same switch sits with the justification settings, for the ragged paragraph styles and boxes.
#Document language
The top-level locale is the language of the document as a whole. It takes the same values as hyphenation.locale and is the fallback when that field is unset, so a Spanish book only needs locale: 'es' to hyphenate in Spanish. It also picks the language of the built-in continuation strings of tables and split callouts ((cont.) / Continued versus Continúa, see Tables taller than the page and Marks on a split box) and of spelled-out heading numbers (Chapter One versus Capítulo uno, see Spelled-out numbers), and is the language tagged into an accessible PDF. Unset, the engine assumes 'en-us'; the Sandbox falls back to the interface language and sets the field under Design › Writing system.
It picks the built-in resource types too. When resourceTypes is not set, buildDocument numbers and captions with defaultResourceTypes(locale), so locale: 'de' alone gives Abbildung 1.1 and Tabelle 1.1. When locale is unset, the hyphenation locale stands in for it, for the resource types and the table strings alike. An explicit resourceTypes list always wins. Earlier versions used the English types whatever the locale unless you passed defaultResourceTypes(locale) yourself; a document that sets locale and wants to keep the English labels passes resourceTypes: defaultResourceTypes('en'). Sandbox books and bundles carry their own list, so they are not affected.
Any BCP 47 tag works here as in hyphenation.locale: 'de-AT' gets the German strings. The built-in strings exist in the eight languages hyphenation supports, in Chinese (in Simplified and in Traditional characters), in Japanese and in Arabic; any other language gets the English ones.
Chinese takes zh, zh-Hans, zh-Hant, zh-CN, zh-SG, zh-TW, zh-HK, zh-MO and the long forms (zh-Hant-TW), in any case and with - or _. The strings follow the script, read with Intl.Locale(tag).maximize(): zh, zh-CN and zh-SG are Simplified, zh-TW, zh-HK and zh-MO Traditional. The typographic defaults that depend on the language follow the region instead, as clreq §1.2 recommends: CN, SG and MY count as the mainland, TW as Taiwan, HK and MO as Hong Kong, and a tag without a region goes by its script (zh-Hant is Taiwan, zh and zh-Hans the mainland). Japanese takes ja, ja-JP, ja-Jpan and any other ja tag (isJapaneseLanguage(tag)); since postext 1.16 it has its own strings and its own region, japan, whose typographic defaults follow JLReq (see Japanese layout). A table of strings with no Japanese entry gives English, never Chinese. localeScript(tag), cjkRegionOf(tag), stringsKeyOf(tag) and sameContentLocale(a, b) give these readings, and DOCUMENT_LANGUAGES lists the languages with built-in strings, each named in its own language (日本語 between the Chinese entries and العربية), as the Sandbox's Document language select shows them.
| Language | Figure: name, plural, short label | Table: name, plural, short label | Table continuation: continuedSuffix, continuesMarker |
|---|---|---|---|
English (en) | Figure, Figures, Fig. | Table, Tables, Tab. | (cont.), Continued |
Spanish (es) | Figura, Figuras, Fig. | Tabla, Tablas, Tabla | (cont.), Continúa |
French (fr) | Figure, Figures, Fig. | Tableau, Tableaux, Tabl. | (suite), À suivre |
German (de) | Abbildung, Abbildungen, Abb. | Tabelle, Tabellen, Tab. | (Forts.), Wird fortgesetzt |
Italian (it) | Figura, Figure, Fig. | Tabella, Tabelle, Tab. | (segue), Continua |
Portuguese (pt) | Figura, Figuras, Fig. | Tabela, Tabelas, Tab. | (cont.), Continua |
Catalan (ca) | Figura, Figures, Fig. | Taula, Taules, Taula | (cont.), Continua |
Dutch (nl) | Figuur, Figuren, Fig. | Tabel, Tabellen, Tab. | (vervolg), Wordt vervolgd |
Chinese, Simplified (zh-Hans, zh, zh-CN) | 图, 图, 图 | 表, 表, 表 | (续), 接下页 |
Chinese, Traditional (zh-Hant, zh-TW, zh-HK) | 圖, 圖, 圖 | 表, 表, 表 | (續), 接下頁 |
Japanese (ja, ja-JP) | 図, 図, 図 | 表, 表, 表 | (続き), 次ページへ続く |
Arabic (ar, ar-EG, ar-MA…) | شكل, أشكال, شكل | جدول, جداول, جدول | (تابع), يتبع |
The caption prefix is the type's name (Figure 1.1.). The Chinese types number within the chapter with a hyphen, {h1}-{n} (图 1-1); with the caption settings labelNumberGap: '' and labelSeparator: ' ' the caption reads 图1-1 标题 (see Caption style). The back-of-book index follows the language too: 见 and 另见 before a cross-reference, 符号 and 数字 over the symbols and the numbers in Simplified; 見, 另見, 符號 and 數字 in Traditional. Japanese numbers its types the same way and sets its captions 図1-1 題 without the two settings, writes cross-references as 第3章, 2.3節 and 12ページ, titles the bibliography 参考文献, and its index prints 記号 and 数字 over symbols and numbers and writes its cross-references with an arrow, →夏目漱石 for see and →夏目漱石、森鷗外も見よ after the pages for see also, the labels upright. Arabic numbers its types the same way, {h1}-{n} (شكل 2-3), writes cross-references as الفصل 3, القسم 2-1 and ص 12, titles the bibliography المراجع, and its index prints انظر / انظر أيضًا, رموز and أرقام, with the Arabic comma and semicolon (، ؛).
A Chinese, Japanese or Korean document declares its language in the HTML output (lang on the .pt-doc root, zh-Hant-TW kept whole) and on the canvas it paints (ctx.lang, in Chrome 136 and later), so the browser draws the region's glyph forms: Unicode unifies the Han characters, and one code point looks different in a Taiwanese and in a Japanese font. Other documents carry no lang, as before. The PDF declares /Lang from locale for every document, tagged or not, with its script and region. A document in a language written right to left (Arabic, Persian, Urdu, Hebrew…) declares its language too: the font's language forms (locl) and the fallback font follow it.
const config: PostextConfig = {
locale: 'es',
bodyText: { textAlign: 'justify', hyphenation: { enabled: true } }, // hyphenates in Spanish
};Document digits
The top-level numerals sets the digits of every number the engine writes: page numbers and the page labels of the contents, the index and page references; ordered-list and footnote numbers; heading and chapter counters, {chapterNumber}; the {h1} and {n} of a figure number and of a reference to it; {totalPages}, {bookTotalPages} and {numberDecimal}. 'latn' writes 0–9, 'arab' the Arabic-Indic digits ٠–٩ and 'arabext' the Persian digits ۰–۹. Only the decimal format changes — decimal, the lists' arabic, or a setting left at its default — so a format the author names prints as named: lower-roman stays i, ii, iii, and arabic-indic in a Latin document still writes ١, ٢, ٣. The text of the document is never rewritten.
The default, 'auto', takes the digits of locale (defaultNumeralsFor(tag)): 'arab' for Arabic with no region or with any region outside the Maghreb (ar, ar-EG, ar-SA, ar-AE…), 'latn' for ar-MA, ar-DZ, ar-TN, ar-LY, ar-MR and ar-EH, 'arabext' for Persian (fa), Pashto (ps) and the Urdu of India (ur-IN), and 'latn' for everything else, the Urdu of Pakistan included. CLDR gives latn for a bare ar and for ar-AE; Arabic books of the Mashriq and the Gulf print ٠–٩, which is what Postext follows. A tag that names its digits keeps them: ar-MA-u-nu-arab. A page numbered in the document digits records arabic-indic or persian as its pageNumberFormat, so the PDF page labels show the same digits. An unknown value follows the language and is reported as unknownNumerals.
Numbers the author types are read in any of the three systems: a list item ٣. starts at 3, and {startAt=٥}, :::numbering{startAt=٥}, :::space{lines=٢} and :::part{number="٣"} (for {numberDecimal}) read the value.
const config: PostextConfig = { locale: 'ar' }; // ١، ٢، ٣
const maghreb: PostextConfig = { locale: 'ar-MA' }; // 1, 2, 3
const forced: PostextConfig = { locale: 'ar', numerals: 'latn' }; // 1, 2, 3Text direction
The top-level direction sets the base direction of the document: 'ltr', 'rtl', or 'auto' (the default), which is 'rtl' when the script of locale is written right to left (Arabic, Persian, Urdu, Hebrew, Syriac, Thaana, N'Ko, Adlam…, read by directionOf(tag)) and 'ltr' otherwise. A right-to-left document is laid out in a mirrored frame: its lines start on the right, its first column is the right one, its indents, list markers, floats, footnotes and boxes stand on the right, and page.binding: 'auto' binds it on the right. The resolved config carries direction: 'rtl' only for such a document, so a left-to-right document resolves as before. An unknown value is read as 'auto' and reported as unknownConfigValue. The Unicode Bidirectional Algorithm (UAX #9) orders each line's runs in either direction: an Arabic quotation in an English book reads right to left in place.
Inside the document, a heading or a ::: container takes {dir=ltr} or {dir=rtl}, and the inline :ltr[…] and :rtl[…] isolate a run of text (see Text direction in the markup); a table resource takes table.direction. A block set against the document's direction keeps its own start side: its indent, list markers and the flush end of its last line move to the side its text starts on.
A setting that names a side means a side of the text or of the body flow, never of the sheet, so a design made for an English book works when the book is switched to Arabic. 'start' and 'end' are accepted as explicit names:
| Setting | 'left' / 'right' | 'start' / 'end' |
|---|---|---|
textAlign of the body, headings, paragraph styles, parts, footnotes and callout bodies; caption and caption note align | The sides of the text: 'left' is the side a line starts on, the right of an Arabic paragraph, where a justified paragraph's last line goes. | Synonyms of 'left' and 'right'. Resolved configs carry 'left' / 'right'; saved configs keep what was written. |
Table cell align | The sides of the cell's text, read in the table's direction (table.direction). | Synonyms, read in the table's direction. |
placement.align (floats, narrow figures) | The sides of the body flow: in a right-to-left book 'left' is the sheet's right. | Synonyms. |
Callout stripe.side, icon.cornerSide, labelTab.position ('top-start', 'top-end') | The sides of the body flow, the same for every box of the page. | The box's own direction (a :::callout{dir=ltr} in an Arabic book starts on the sheet's left). |
| Header and footer slots; design elements anchored to the sheet | The sheet's sides. | Text elements only: the start and end of the element's own direction. |
placement.rotate keeps its physical meaning on a mirrored page: a figure turned clockwise is turned clockwise on the sheet. A host reading the layout finds the mirrored frame on each page (VDTPage.flow with direction: 'rtl', pageIsMirrored(page)) and the visual order of each line's segments in VDTLine.order; flowToPage and pageToFlow map between the flow and the sheet. See Arabic layout.
const arabic: PostextConfig = { locale: 'ar' }; // right to left, bound on the right
const english: PostextConfig = { locale: 'en', direction: 'rtl' }; // forced; rarely what you want#Languages and scripts
Postext sets alphabetic scripts written left to right, and Chinese and Japanese across the page and down it. Chinese layout and Japanese layout explain how they are set and which settings control them; the keys are under East Asian typography, Vertical writing and Binding. What each script gets:
- Chinese is set by the CJK composer when a paragraph holds more CJK characters than word spaces: its lines break between characters under the line-start and line-end rules of
cjk.lineBreak(no line starts with 。、」 or ー, none ends with 「 or (), keep —— and ……, a number with its signs and a Latin word whole, and a justified line is spread between its characters to the measure. Punctuation widths, hanging punctuation, the space between Han and Latin, the character grid, emphasis dots, the proper-name and book-title marks, ruby and warichu notes follow the region oflocale, across the line or down it (layout.writingMode: 'vertical-rl'). A Latin paragraph that quotes a few CJK words keeps optimal line breaking and may break next to them; a CJK bracket, the middle dot or a fullwidth sign quoted in Latin text (〈h〉, %) changes nothing. - Japanese goes through the same composer with rules of its own since postext 1.16, those of the W3C Requirements for Japanese Text Layout (JLReq) and JIS X 4051: a
jalocale gives thejapanregion, whose auto values set the JLReq kinsoku levels (small kana and ー never open a line), full-width punctuation with pair compression, an em after ?!, the bracket that opens a paragraph in the second half of the indent, sesame emphasis marks over the text, 『』 book titles, furigana spaced 1:2:1, Japanese counters, notes and an index sorted by reading. Earlier versions set Japanese with the mainland Chinese defaults. - Korean goes through the same composer and is set without hyphenation, but with the mainland Chinese defaults: its own rules (KLREQ) are not implemented. Korean text breaks between syllables as well as at its spaces.
- Arabic and other right-to-left scripts (Persian, Urdu, Hebrew…) are set right to left: Arabic layout explains how. The document's
directioncomes from the script oflocale, the Unicode Bidirectional Algorithm orders the Latin words and numbers inside each line, the book is bound on the right with its first column on the right, and every number the engine writes takes the region's digits. A word that holds an Arabic-script letter is never hyphenated, letter-spaced or cut, and a justified Arabic line is stretched at its spaces and with kashidas. Vowel marks, emphasis, footnotes and the strings of Arabic are under Arabic text. Persian, Urdu and Hebrew get the direction, the digits and the whole-word rules, but no built-in strings of their own.
Hyphenation patterns ship for eight languages (en-us, es, fr, de, it, pt, ca, nl); the built-in resource types and continuation strings exist in those eight, in Chinese, in Japanese and in Arabic. Chinese, Japanese, Korean and the languages written right to left are set without hyphenation. Any other language is hyphenated with the US English patterns, with a console warning, and gets the English strings. For such a document, set hyphenation.enabled: false and pass resourceTypes and the tableStyle continuation strings in its language.
#Arabic text
These settings serve text in Arabic script; none changes a document written in another script. Arabic layout explains them with the rest of an Arabic book: direction, binding, digits, verse, contents and index.
- Vowel marks and leading. The vowel marks of a vocalised text (fatḥa, kasra, shadda, tanwīn, the dagger alef, the Qurʾānic signs) stack over and under the letters, in the leading, which never grows for them. Each line that holds marks records how far their ink reaches (
VDTLine.markInk), and the renderers' column clip takes in the marks of a column's first and last lines. When a mark over a word meets the letters or marks hanging under the word above it, the build reports the paragraph (arabicMarksExceedLeading, in the Sandbox's Checks panel). Only words standing over each other are compared. Partly vocalised text wants about 1.7–1.85 em oflineHeight, fully vocalised verse 1.9–2.1 em. - Emphasis. Arabic type has no italics, so in a document whose
localeis written in Arabic script*…*is set in bold by default (bodyText.emphasis: 'auto').'color'sets it upright initalicColor, and'overline'draws a rule over the words, the khaṭṭ fawqī of Arabic books. The setting reaches every text set in the body's faces: paragraphs, lists, blockquotes, paragraph styles, callout bodies, notes and headings. Captions, table cells, the contents and the index keep their own italic settings. Whatever is chosen, the engine never slants Arabic letters: the Arabic words of a run set in italics stand upright and its Latin words keep their italics. A blockquote is upright by default in such a document. - Tashkīl.
bodyText.tashkil: 'strip'takes the vowel and Qurʾānic marks out of the text the layout sets, for an unvocalised edition made from a vocalised source: fatḥa, ḍamma, kasra and their tanwīn, sukūn, shadda, the dagger alef (هٰذا becomes هذا) and the marks U+0656–U+065F and U+06D6–U+06ED.'strip-vowels'keeps the shadda, as most modern books print it. Hamza and madda stay (أ إ آ are letters, also when typed with combining marks). The source keeps its marks; the lines, the headings and the contents are set without them, and every character set still maps to its place in the source. - Footnotes.
footnotes.markerTemplate: '({n})'writes the markers «(١)» in the document digits,numbering: 'page'starts them again on every page, andnoteNumberPosition: 'inline'sets the note's own number on the line. The separator rule and the note numbers stand at the start of the column, on the right in a right-to-left book. - Whole words. A word that holds an Arabic-script letter is never hyphenated, cut or letter-spaced, in an Arabic book or quoted in another. A style that sets
letterSpacingon Arabic text is reported (joiningScriptLetterSpacing), and a word wider than its line overflows it and is reported (unbreakableWordOverflow). See Arabic layout. - Kashida and verse. A justified line of Arabic is stretched with kashidas as well as at its spaces (
bodyText.kashida, see Kashida in Arabic text), and a classical poem is set one bayt a line in two hemistichs of one width with:::verse(see:::verse). - Index. An index in Arabic sorts in alphabetical order and ignores the article ال (
index.ignoreArticle), the vowel marks and the hamza seats; see Back-of-book index.
#Orphans, widows, runts, and keep-together rules
See Hyphenation & Justification for the mechanics behind these demerits. This section is the reference for the bodyText keys that drive them.
Beyond hyphenation and spacing bounds, the body-text configuration exposes the soft rules that prevent structurally awkward paragraph breaks. All of these are fed into the Knuth-Plass line-breaking algorithm as demerits — they bias the layout toward clean breaks without ever forcing a hard rule. Set the *Penalty values to 0 to effectively disable any one of them.
| Property | Type | Default | Description |
|---|---|---|---|
avoidOrphans | boolean | true | Discourage a paragraph from ending with fewer than orphanMinLines lines at the top of the next column. |
orphanMinLines | number | 2 | Minimum lines required at the top of the next column when a paragraph is split. Only active when avoidOrphans is true. |
orphanPenalty | number | 1000 | Demerit added when an orphan constraint is violated. Higher values bias the algorithm more strongly against orphans; 0 disables the penalty. |
avoidOrphansInLists | boolean | true | When true, list items also receive orphan protection (not just paragraphs). Only effective when avoidOrphans is true. |
avoidWidows | boolean | true | Discourage a paragraph from starting with fewer than widowMinLines lines at the bottom of the current column. |
widowMinLines | number | 2 | Minimum lines required at the bottom of the current column when a paragraph is split. Only active when avoidWidows is true. |
widowPenalty | number | 1000 | Demerit added when a widow constraint is violated. 0 disables the penalty. |
avoidWidowsInLists | boolean | true | When true, list items also receive widow protection. Only effective when avoidWidows is true. |
avoidRunts | boolean | true | Discourage paragraphs from ending with a very short last line — a runt, e.g. a single short word alone. Ragged text too, with optimalRagged. A Chinese, Japanese or Korean paragraph does not end on a line holding one character, alone or with its closing marks (孤字): the line above gives it its last character when that line can still be justified within the tracking cap. |
runtMinCharacters | number | 20 | Threshold for the last line of a paragraph, counted in word spaces, not letters: the line is a runt when it is narrower than runtMinCharacters × normalSpaceWidth pixels. A word space is a quarter to a third of an em in most text faces, about half a lowercase letter, so the default 20 catches last lines under 4 to 7 em — roughly 8 to 12 letters. To catch the last lines under about N letters, set it near 2 × N. |
runtPenalty | number | 1000 | Equivalent-badness injected into the Knuth–Plass squared demerit formula (same scale as line badness, which saturates at 10000). 0 disables the penalty. |
gradedRuntPenalty | boolean | false | Scale the runt penalty by how short the last line falls: a last line of width w under the threshold t costs runtPenalty × (1 − w / t) instead of the whole penalty. A two-word ending then costs less than a one-word one, and the breaker pulls a word down when a line above can spare it ('…sallies of' / 'our minds.' rather than '…sallies of our' / 'minds.'). Off by default: every runt costs the same, and the breaker keeps the tighter lines above. |
avoidRuntsInLists | boolean | true | When true, list items also receive the runt penalty. Only effective when avoidRunts is true. |
tightenRunts | boolean | true | When the penalty could not avoid a runt, set the paragraph one line shorter instead: the word spaces tighten (never past minWordSpacing) and, if that alone does not carry the line, a little negative tracking joins in. A shorter setting is refused, and the runt stays, when it would stretch a justified line past maxWordSpacing or, when the paragraph already has a looser justified line, past that line, or when it would set more lines ragged (past 3× the normal space) than the paragraph had. The word spaces of ragged text keep their width, so there only the tracking takes part. Needs optimalLineBreaking and avoidRunts (and optimalRagged for ragged text). |
maxRuntTracking | number | 10 | Most tracking a runt fix may take, in thousandths of an em (the InDesign unit: 10 = 0.01 em per character), applied as a tightening. 0 leaves the fix to word spacing alone. |
slackWeight | number | 10 | Weight applied to the squared "unused column space" cost. Higher values make the layout prefer filling columns tightly; 0 disables the slack pressure entirely. |
keepColonWithList | boolean | true | When a paragraph ends with a colon that directly introduces a list, keep the colon-bearing last line joined to the list: if placing the paragraph would leave no room for the first list item to start in the same column/page, the last line (or the whole paragraph, if it is a single line) is moved to the next column together with the list. How much room counts as enough is colonListRoom. When this rule would push the whole paragraph and a run of headings immediately precedes it in the column, those headings are pulled forward too so headings.keepWithNext keeps holding. |
colonListRoom | 'item' | 'line' | 'item' | The room keepColonWithList asks for under the colon line. 'item': what the orphan and widow rules for lists would leave of the first item at the foot of the column, a line when it may split there, all of it when they keep it whole (a two-line item, say). 'line': one line, as up to postext 1.4; a first item those rules keep whole then goes on to the next column alone and leaves the colon line at the foot. A configuration stored before configVersion 6, in a book that introduces a list with a colon, reads with 'line' (see Bundles written by postext 1.4 or earlier). Any other value reads as 'item'. |
hyphenateAcrossColumns | boolean | true | Let a column or a page end on a hyphenated word (InDesign's Hyphenate Across Column). false breaks the paragraph that crosses the column break again, so that its last line in the column ends on a whole word; the word spaces of the lines above take up the difference, within maxWordSpacing and minWordSpacing. It is a preference: where no break within those limits avoids it, the hyphen stays. It tries every column break of a paragraph: the first, and the later ones that fall where a full column ends, in one re-break; and a later break that falls elsewhere (a widow kept, a band cut level) in another, from that column, which keeps the lines already set in the columns before at their breaks and breaks only the rest. Box bodies are not affected. With optimalRagged, a ragged paragraph is broken again the same way: its word spaces keep their width, so only the ends of its lines move. Each re-break measures the paragraph once more, so a book with many column-end hyphens lays out a little slower. Needs optimalLineBreaking, and optimalRagged for ragged text. |
paragraphContainerSpacing | 'collapse' | 'add' | 'collapse' | The space under a :::paragraphs container that closes on a paragraph, between that paragraph and the block after it (see The :::paragraphs container). 'collapse': the larger of the style's spaceBetween and marginBottom and the paragraph spacing of the text around the container (a line with paragraphSpacing; in a box, the box's), merged with the space the next block keeps above itself, as between two paragraphs of running text: a heading under a bibliography sits its own marginTop below it, not that margin plus the entries' spacing. 'add': as up to postext 1.4, the style's space alone is set under the last line before the grid snap, the next block's own space above is added under it, and the paragraph spacing is left out, so the paragraph after the container could sit closer to it than to any other. A configuration stored before configVersion 8 that declares a paragraph style, in a book that holds such a container, reads with 'add' (see Bundles written by postext 1.4 or earlier). A negative marginBottom pulls the next block up either way. |
About runts. A runt is a paragraph whose last line is too short to feel like a proper line of text — typically one or two short words marooned at the end of a paragraph. Because the check is based on the pixel length of the line relative to the normal space width, runtMinCharacters adapts automatically to the current font size. A short word that is visually wider than runtMinCharacters × spaceWidth is fine; a word that is narrower than that (or truly alone) attracts the runt penalty. The threshold counts word spaces, which are about half as wide as letters: the default 20 is a last line of roughly 8 to 12 letters. Every runt costs the whole penalty, so among two endings under the threshold the breaker keeps the tighter lines above; gradedRuntPenalty prices each by its shortfall instead, and the longer ending wins. For the mathematically curious: at the default runtPenalty of 1000, avoiding a runt dominates any alternative break set requiring up to roughly r≈2.15 word-spacing stretch.
Soft, not hard. None of these rules can prevent a break — the engine will always produce a layout. They are demerits: the algorithm trades off badness, hyphenation cost, fitness-class smoothness, and these structural penalties in a single global optimisation and picks the break set with the lowest total cost. If you need a harder guarantee, raise the penalty; if a given document reads better with the penalty relaxed, lower it.
#Headings
The headings property controls typography for all heading levels (H1–H6). You can set general defaults that apply to all levels, then override specific properties per level.
#General defaults
| Property | Type | Default | Description |
|---|---|---|---|
fontFamily | string | 'Open Sans' | Font family for all headings. |
lineHeight | Dimension | 1.2 em | Line height for headings. Tighter than body text. |
color | ColorValue | Main Color (#295AA3) | Heading text color. Bound to the default palette's main-color entry, so swapping the palette colour retints every heading. |
textAlign | 'left' | 'justify' | 'center' | 'right' | 'start' | 'end' | 'left' | Alignment of every heading level (there is no per-level value): ragged right, justified, centred or ragged left. A justified heading sets its last line flush left, like a paragraph, so a one-line heading reads as 'left'. Canvas, HTML and PDF place the lines the same way, a number prefix included. The default opener of a span: 'page' heading without an advanced design follows it too (justified sets flush left); an advanced design aligns its own text elements with their align. |
fontWeight | number | 700 | Font weight for headings (100–900). |
marginTop | Dimension | 1.5 em | Space above headings. |
marginBottom | Dimension | 0.5 em | Space below headings. |
keepWithNext | boolean | true | When true, a heading is never placed as the last element of a column or page. If the following block would not have at least bodyText.widowMinLines lines of room after the heading (or one line when bodyText.avoidWidows is false), the heading is pushed forward so it stays joined to its text. Interacts with bodyText.keepColonWithList: if that rule has to push a colon-paragraph whole, any trailing heading(s) in the column travel with it rather than being left stranded. |
keepWithNextSpread | boolean | false | With keepWithNext, still let a heading close the last text column of an even page, since its text then opens the facing odd page of the same spread (JLReq §4.1.7). The spreads are page 1 alone, then 2–3, 4–5…, counted with pageIndexOffset, in left- and right-bound books alike. Since postext 1.16. |
keepWithNextSplit | 'rules' | 'fill' | 'rules' | How the paragraph under a heading splits when the heading ends up at the foot of a column and pushing the paragraph whole would leave the heading behind. 'rules': the most lines that fit, as long as at least bodyText.widowMinLines stay under the heading and at least bodyText.orphanMinLines go on to the next column; when no split keeps both, the heading moves on with its paragraph, and column balancing fills the room it leaves. 'fill': as many lines as fit, however few go on, so a four-line paragraph with room for three splits 3 + 1. Up to postext 1.4 every heading split this way, and configurations stored before configVersion 8 keep it (see Bundles written by postext 1.4 or earlier). With avoidWidows or avoidOrphans off, that side of the rule is dropped. |
snapToGrid | boolean | true | Whether the flow snaps back onto the baseline grid under a heading. With true the heading's marginBottom is rounded up to whole grid lines; with false the exact margin is kept and the text under the heading may sit off the grid until the next snap point (the end of a list, a :::paragraphs tail, display math) — the way many books set a line and a half under a heading. A level (levels[].snapToGrid) or a heading style can set its own value; this one is what they inherit. |
inlineMarks | boolean | true | Whether a heading reads its inline marks as a paragraph does: italic, bold, ^superscript^, ~subscript~, :smallcaps[…] and links. An italic run flips the heading's slant, so it comes out upright in an italic heading; a bold run takes bodyText.boldFontWeight, or the heading's own weight when that is heavier. The contents list the bold and italic runs too; the running heads and the PDF bookmarks print the text only. With false the markers are dropped and the words print in the heading's own style, as up to postext 1.4. The default opener of a span: 'page' heading without a design sets the bold, italic, superscript and subscript runs too; a heading design (a designed opener band or an in-column advancedDesign) prints as plain text either way, unless its text element reads inline marks (inlineMarks: true): there keeps the heading's bold, italic and script runs (since postext 1.19). Configurations stored earlier whose headings carry marks are read with false (see Bundles written by postext 1.4 or earlier). |
balancing | ColumnBalancingConfig | enabled | Vertical column balancing — extra space above headings so columns end flush with the page bottom. See below. |
Centred headings for poems or the acts of a play, with no design slot:
headings: {
textAlign: 'center',
levels: [{ level: 2, textTransform: 'uppercase' }],
}Passing a headings object keeps every level default you do not restate, the H1 page break included (see Per-level overrides).
#Column balancing
Publishers expect every column to start at the top of the page and end flush with its bottom. Break rules (orphan/widow protection, headings kept with their text, unsplittable figures) naturally leave short columns — one or more empty baseline-grid lines at the bottom. When balancing is enabled, the engine does what a compositor would, applying its levers in editorial priority order:
- A box closing the column — a callout that ends a short column is pushed down by the exact room under its foot, so its bottom edge lands on the last grid slot of the page, level with the last line of the column beside it. It takes that room even when it is less than a line, as long as nothing moves into another column. By default it runs before every other lever and takes the whole gap, so a box that annotates the paragraph above it can end up several lines below it;
closingBox: 'last'lets the headings, the list ends and the other spacing levers take the whole lines first, and the box only what they leave, andclosingBox: 'off'never moves it. - Pictures with a safe area — a picture with a safe area (
Resource.safeArea, see Document format › Safe area) set inline in the short column, or floated at its head or its foot over that column alone, grows by whole grid lines, cropped within its safe area, until the column is full or the crop reaches the safe area. A taller picture leaves no hole on the page, so it runs before any space is added; a float at the head of a column on a page or band whose column heads stay level (see below) does not grow. Recorded asflexFigure. It has no setting of its own: only a picture whose resource marks a safe area can grow. - Headings — whole grid lines are added to the top margin of the headings inside the short column. When several lines are needed and the column holds several headings, the lines are distributed among them, always giving the largest share to the most important heading (an
h2receives more than anh3). Headings sitting at the very top of a column never receive extra space, so columns keep starting at the page top — except a heading right under a figure or table that heads its column on a page that flows on: the room goes above that heading, under the figure. - List ends — when the headings cannot absorb the whole gap, a grid line is added where a list/enumeration ends (space after a list reads naturally), capped per list end.
- Loose paragraphs — as a last resort, one paragraph of the column is re-broken one line longer (TeX's
\looseness=+1), choosing the longest paragraph so the extra word spacing dilutes invisibly. The loose solution is only accepted when every line stays belowbodyText.maxWordSpacing— type colour never exceeds the limit you already configured. RequiresbodyText.optimalLineBreaking.
On a character grid (cjk.grid, see Chinese layout › The character grid) every character stands in a cell one em wide and every line on the grid's line pitch, which is also the baseline grid. Balancing is off there by default, as in vertical text: a page set line by line on a grid, the way GB/T 9704 counts 22 lines of 28 characters, leaves a short column short. A configuration that sets enabled: true gets levers that keep the grid: headings, list ends, displays and figures take whole grid lines, so the text below them moves by whole rows, and gridLines: 'off' keeps those out for a standard that has no empty row to give. A Chinese or Japanese paragraph runs a line long only when no gap between two of its characters spreads past maxTracking, and takes no tracking on its letters: a line a character short spreads an em over its gaps, far past the default 0.01 em, so on a grid of whole cells the loose lever usually finds nothing. The box closing a column and the pictures with a safe area are off the grid by design and work as anywhere.
The last column of a page is only balanced when the page flows naturally into the next one — a chapter's closing page legitimately ends short. Such a page, and a closing band cut level by trailing, also keeps its column heads level: no line is added under a figure or table heading one of its columns (the after-float lever), and a heading or a box that opens a column under such a figure stays at its head, whatever stretchAfterFloats says, so a column never starts lower than the one beside it just to line the feet up.
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Whether to balance column bottoms. Off by default in vertical text (layout.writingMode: 'vertical-rl') and on a character grid (cjk.grid.enabled, since postext 1.25); true turns it on there too. |
maxLinesPerHeading | number | 4 | Maximum extra grid lines that may be added above a single heading. |
stretchAfterLists | boolean | true | Allow extra grid lines where a list ends, when headings cannot absorb the whole gap. |
maxLinesAfterList | number | 1 | Maximum extra grid lines after a single list end. |
stretchAfterFloats | boolean | true | Allow extra grid lines under a figure or table that heads the short column (a top float), after the list-end lever, so the text below it moves down instead of the column ending short. Never on a page that does not flow on (a chapter's closing page) nor in a closing band cut level by trailing: there the column heads stay level and the last column may end a line short. The first block under the figure may be the rest of a paragraph begun on the page before: it moves down all the same, by the line the break rules left free at the column's foot (a line the widow rule kept empty, or a paragraph space with no room for text after it). Set it to false to keep the text right under the figure. |
maxLinesAfterFloat | number | 1 | Maximum extra grid lines under a single top float. |
looseParagraphs | boolean | true | Last resort: re-break paragraphs of a short column one line looser (one extra line each), within bodyText.maxWordSpacing. |
maxLooseParagraphs | number | 2 | How many paragraphs of one short column may run a line long, longest first. |
trackParagraphs | boolean | true | When word spacing alone cannot gain the line, a loose paragraph may also take the smallest positive tracking (letter spacing) that does. |
maxTracking | number | 10 | Upper limit for that tracking, in thousandths of an em per character (10 = 0.01 em). |
trailing | boolean | true | Level the closing band of a chapter and of the document: when the flow ends before the page is full (at a chapter opener, a :::part, a chapter-closing placement: 'fixed' box or the end of the document) with its columns uneven, they are cut level — a band cap of ceil(Σ used / N / grid) lines, resolved after the levers above have settled the earlier pages — so a short bibliography ends at the same height in every column instead of filling the first column and leaving the last one half empty. The cut keeps the rules an uncut band keeps: when a block that cannot split across it (a paragraph tail the orphan and widow minimums keep whole, a keep-together box) would run past it — and lose its last lines, since the renderers clip a column to its box — or a heading would close a column while its text opens the next (with headings.keepWithNext, the default), the cut is taken a line lower, up to three times, and otherwise dropped. A block that cannot start in the lines the cut leaves under a figure heading a column (a paragraph needs its orphan minimum there) goes on to the next column of the band instead, as it would from an uncut column, and the figure stands alone in its column. Columns whose feet differ by one grid line or less are left as they are: a closing band whose last column ends a line short is the usual finish, and cutting it would only move a line. The cut goes to the band it was measured in, and so does the cut a page-span box asks for mid-page: up to postext 1.4, when the first text of the closing page had first been offered to a band with no room for it (a page a float takes whole, the thin band a page-span box leaves at the foot of the page before), the cut was spent on that band and the closing page kept every line in its first column. Columns of different widths (a one-and-a-half layout with text in both) are cut by area, each column's height weighted by its width, since a line of the narrow column holds less of the text. Only active when enabled is true. |
beforeSpan | boolean | true | Level the band a page-span block leaves behind: when a span: 'page' callout does not fit under the current columns even after a level cut, and has to move to the next page or split (calloutStyles[].keepTogether: false), the columns it interrupts are cut level — the same trailing cap a closing band gets — instead of the first filling the page and the last ending short. The page is left as an explicit break so the levers above do not stretch its last column back to the page bottom; the box, or the part of it that fits, then sits under the levelled columns. Only active when enabled is true. |
closingBox | 'first' | 'last' | 'off' | 'first' | When a box closing a short column takes the room under its foot (lever 1, recorded as trailingCallout). 'first': before any other lever, as up to postext 1.4 — the box takes the whole gap, even several lines, and the headings above it get none. 'last': after the heading, list-end, display and float levers, which take whole lines first; the box moves down with the text above it and then takes only what they left, usually a fraction of a line, so its foot still meets the last grid slot and it stays close to the text it annotates. 'off': never; the box keeps the room under its foot, though the other levers can still move it down by whole lines when they add space above it, and the fraction of a line left under it stays. On a closing page, where the box is the only lever that lines its foot up with the column beside it, 'off' leaves it where the flow put it. Any other value reads as 'first'. A box closing the only text column of its band (a one-column page that ends early, its next block opening the next page) is never moved: there is no column beside it to line up with (since postext 1.19). |
gridLines | 'allow' | 'off' | 'allow' | On a character grid: whether the levers that add whole grid lines (above a heading, where a list ends, under a display or a box, under a figure heading the column) may run. They keep every character in its cell; 'off' suits a standard that counts the lines of a page, and leaves the column short instead. No effect off the grid. Any other value reads as 'allow'. Since postext 1.25. |
Which lever fired
The layout records every lever it applied, on the block it applied to: block.balancing in the VDT that buildDocument returns. A proof sheet, a test or a report can say why a column ends flush — and which columns no lever could close. Blocks the balancing left alone carry no balancing, and a document built with enabled: false has none.
On a character grid the document says why a column may end short: doc.gridBalancing is { off: true } when balancing is off because the page is on a grid and the configuration does not turn it on, and { gridLines: 'off' } when it runs without the whole-line levers; a column still short carries column.gridRefused, the levers the grid kept from it ('looseParagraph' when a paragraph could gain its line only by spreading its characters off their cells, the whole-line levers under gridLines: 'off'). Both are absent off the grid.
interface VDTBalancing {
levers: BalanceLever[]; // usually one: a paragraph after a list can take the list-end line and run a line long too
spaceAbove: number; // px the spacing levers added above the block (0 when only looseParagraph or flexFigure fired)
bodyGrowth?: number; // flexFigure: px the picture grew, cropped within its safe area
extraLines?: number; // looseParagraph: lines the paragraph gained
tracking?: number; // looseParagraph: the tracking that gained them, in thousandths of an em (0 = word spacing alone)
}
type BalanceLever = 'trailingCallout' | 'flexFigure' | 'heading' | 'listEnd' | 'afterDisplay' | 'afterFloat' | 'looseParagraph';| Lever | Recorded on | What it did |
|---|---|---|
trailingCallout | the callout's frame block | A box closing the column moved down by the exact room under its foot (spaceAbove; a fraction of a line is fine). |
flexFigure | the picture's block (inline) or float | A picture with a safe area set bodyGrowth px taller, by whole grid lines, cropped within the safe area. The resource block's bodySource is the part shown and bodyFlex.delta the same growth. |
heading | the heading | Whole grid lines above the heading, up to maxLinesPerHeading. |
listEnd | the first block after the list | A grid line where a list ends, up to maxLinesAfterList. |
afterDisplay | the block after the formula or box | A grid line under a display formula or a callout box. |
afterFloat | the column's first block | A grid line between a float band heading the column and its text, up to maxLinesAfterFloat. |
looseParagraph | the paragraph | The paragraph re-broken extraLines longer, with the smallest tracking that did it (tracking; block.letterSpacing is the same value in px). |
The level cuts are recorded on the columns they cut: column.bandCapped is true on every column cut level (the band a page-span box leaves, a closing band), and column.trailingCap also marks the closing band of a chapter or of the document (trailing).
import { buildDocument } from 'postext';
const doc = buildDocument(content, config);
for (const page of doc.pages) {
page.columns.forEach((column, i) => {
const levers: string[] = column.blocks.flatMap((block) => block.balancing?.levers ?? []);
if (column.trailingCap) levers.push('closing band cut level');
else if (column.bandCapped) levers.push('band cut level');
if (levers.length > 0) console.log(`page ${page.index + 1}, column ${i + 1}: ${levers.join(', ')}`);
});
}
// page 3, column 1: heading, heading
// page 3, column 2: listEnd, looseParagraph#Per-level overrides
Each heading level can override the general defaults via the levels array. Only fontSize (plus H1's breakBefore, noted below) differs by default — all other properties inherit from the general heading settings.
| Level | Default font size | Default breakBefore |
|---|---|---|
| H1 | 18 pt | { enabled: true, parity: 'always-odd' } |
| H2 | 15 pt | { enabled: false, parity: 'any' } |
| H3 | 12 pt | { enabled: false, parity: 'any' } |
| H4 | 10 pt | { enabled: false, parity: 'any' } |
| H5 | 9 pt | { enabled: false, parity: 'any' } |
| H6 | 8 pt | { enabled: false, parity: 'any' } |
The H1 default models a book-style chapter layout: every top-level heading opens on a fresh right-hand (odd) page, with a mandatory blank separator page from the previous chapter. Override it on levels[0].breakBefore if your document is flatter than a book.
A level's breakBefore is merged field by field over that default, and a headings object that does not mention it keeps it. { parity: 'odd' } on H1 keeps the break and changes only the parity; { enabled: false } lets chapters run on:
headings: {
fontFamily: 'Merriweather', // H1 still breaks to a fresh recto (always-odd)
levels: [{ level: 1, breakBefore: { parity: 'odd' } }], // …or: a recto, with no mandatory blank
}
headings: { levels: [{ level: 1, breakBefore: { enabled: false } }] } // chapters run onChanged in postext 1.5. Up to postext 1.4, any headings object turned the H1 break off unless levels[0].breakBefore was restated, and a partial breakBefore filled its missing field from the no-break default. So a configuration written in code for 1.4 with a headings object and no H1 break now opens every chapter on a fresh recto, with blank versos where needed. To keep the chapters running on, give the H1 entry of levels one field:
headings: { fontFamily: 'Merriweather', levels: [{ level: 1, breakBefore: { enabled: false } }] } // as 1.4 laid it outWhere the configuration was stored, the engine can tell and does it for you: the Sandbox for the books and configurations it saved then (see Sandbox → Persistence), and openBundle / readBundle for a .postext bundle written by postext 1.4 or earlier (see Bundles written by postext 1.4 or earlier). A configuration you stored yourself can go through migrateConfig(config) from postext/bundle, which writes out the breaks 1.4 laid out (pinLegacyHeadingBreaks): enabled: false on an H1 that had no break, and parity: 'any' beside an enabled: true that named no parity, on H1 and on heading styles. It also pins the maths size, the space around inline figures (in boxes too), the headings' inline marks, the drop-cap sizes, the room under a colon line that introduces a list, the lines a box cut leaves of a paragraph or list item, the line breaks at dashes, the line-by-line breaking of ragged text, the split of a paragraph under a heading, the line breaks at a compound's hyphen and the space under :::paragraphs containers, as 1.4 set them. Pass the book's markdown as a third argument, { content }, and it leaves out the maths pin when the text has no $, the gap pins when no line embeds a resource (the box-gap pin when none does inside a box), the heading-marks pin when no heading carries a mark, the colon-line pin when no list follows a line ending in a colon, the box-cut pin when the text opens no :::callout, the dash pin when no dash is set closed between words, the heading-split pin when the text has no heading, the container pin when the text opens no :::paragraphs container, and the compound pin when no hyphen stands between two letters; the ragged-breaking pin goes only to a configuration that sets some running text ragged, and the container pin only to one that declares a paragraph style (see Bundles written by postext 1.4 or earlier). For the breaks alone, call pinLegacyHeadingBreaks(config).
Per-level overrides support the same properties as the general defaults — fontSize, lineHeight, fontFamily, color, fontWeight, marginTop, marginBottom, snapToGrid — plus these level-only fields (a heading style takes them too):
| Property | Type | Default | Description |
|---|---|---|---|
italic | boolean | false | Render the heading in italic. Applied on top of fontWeight. |
textTransform | 'none' | 'uppercase' | 'none' | Upper-cases the heading title (a numbering prefix is kept as written, and so are a chip and the label of a :ref in it). Length-preserving so the editor's source map stays 1:1: characters whose upper-case form expands (ß → SS) are left as they are. The transformed title also feeds the {titleText} placeholder of advanced designs and chapter openers. The PDF bookmarks keep the title as written — Author contributions, not AUTHOR CONTRIBUTIONS — the way CSS text-transform leaves the text itself alone (up to postext 1.4 they took the capitals). |
letterSpacing | Dimension | 0 | Tracking after every glyph of the heading — spaces and the numbering prefix included — as CSS letter-spacing. Positive spreads the letters (capitals set with textTransform: 'uppercase' usually want a little: { value: 0.12, unit: 'em' }), negative tightens a display size. An em value is relative to the level's fontSize. The heading's lines are measured with it, so they wrap where the tracked text ends, and canvas, HTML and PDF paint it the same. A centred or right-aligned line is placed by its letters: the tracking after its last glyph is left out, as in design text, so a centred heading lines up with the default opener of a span: 'page' heading. A level drawn from its advancedDesign ignores it, like the other typography fields here: each design text element has its own letterSpacing. A heading style sets it too, for the headings that use it. Up to postext 1.4 headings had no tracking, and the key was dropped without a word. |
lineSpan | number | unset | Lines taken (行取り, JLReq §4.1.6): the heading is set in a band of this many body lines, its characters centred in it, in place of marginTop and marginBottom; 3 is a 3行取り heading. The band starts on a body line when the heading snaps to the grid, and the text after it stays on the grid, in both writing modes and with cjk.grid; a heading whose lines need more takes the next whole number. The centre is that of the characters (the baseline less the face's ideographic centre), as JLReq measures it, not that of the line boxes. Not applied to openers (span: 'page'), headings drawn from an advancedDesign, hidden headings or headings inside boxes. A heading style may set 0 to take its level's off. Since postext 1.16. |
indent | Dimension | 0 | Indent of the heading from the line start (字下げ), em being the body size, as Japanese books count heading indents in body characters (JLReq §4.1.3): { value: 6, unit: 'em' } is 6字下げ whatever the heading's size. The measure narrows by it, and a centred heading centres in the rest. Since postext 1.16. |
firstLineIndent | Dimension | 0 | Indent of the heading's first line only, measured from indent and, like it, in body ems: the lines a long heading wraps onto start at indent (at the line start when it is unset), as GB/T 9704 sets every level of head two cells in with its turnover at the margin: { value: 2, unit: 'em' }. With cjk.grid on, two body ems are two cells of the grid. A numbered heading prints its number after the indent, so the numbering template needs no ideographic spaces, which would also print in the contents and the PDF bookmarks. It works in vertical text (from the top of the line), with the CJK composer (an opening bracket at the start of the heading follows the line-start rules, as in a paragraph) and with Knuth–Plass. A centred heading takes it as a centred paragraph does: its first line centres in the room after the indent. {firstLineIndent=N} after a heading's title sets one heading, and {firstLineIndent=0} takes its level's off. Since postext 1.24. |
jidori | number | unset | Even spacing (字取り): a one-line heading narrower than this many of its own ems is spaced evenly to exactly that width, so 3 sets 序章 as 序 章. Wider titles and headings of several lines are left as they are. {jidori=N} after a heading's title sets one heading, and {jidori=0} turns its level's off. Since postext 1.16. |
dropCap | ParagraphDropCap | false | none | A drop cap opening the first body paragraph after a heading of this level (see Drop caps): one setting gives every chapter its initial. A heading turns it off with {dropcap=false} or sets its lines with {dropcap=2}. Since postext 1.23. |
numberingTemplate | string | '' | Template of the level's automatic number. A token … prints the running counter of that heading level, optionally formatted by a suffix — upper roman, lower roman, / alphabetic, zero-padded, spelled out (twenty-one), as an ordinal (twenty-first), in Chinese numerals (Japanese ones in a Japanese document: 第百一章), digit by digit, in financial numerals and circled, or any name of Numbering format spellings — and any other text is literal ('Chapter . ', '.', '第回' for 第一百二十回; a backslash escapes a literal brace). A token whose counter is still empty collapses together with its adjacent separator. Empty (the default) means no automatic number. The rendered number is prepended to the title in the flow, feeds the placeholder of an advanced design slot (where the prefix itself is not prepended) and is printed in the table of contents. See Spelled-out numbers for the words, and Heading styles for a template of its own on some headings of a level. |
numberSeparator | string | ' ' | What stands between the number and the title: in the column, in the default opener of a span: 'page' level, in the running heads that print the heading line and in the PDF bookmarks. The level-1 separator also joins a part's number and title in the default part page and in the default part row of the contents. Chinese chapter heads take an ideographic space or nothing (' ': 第一回 甄士隱夢幻識通靈). The contents keep their own number column (toc.levels[].numberGap). A heading style may set its own. When a title is broken in two with , as the couplet titles of Chinese novels are, the single-line forms (the column, the contents, the running heads) join the halves with an ideographic space when both sides are Chinese or Japanese characters, and with a space otherwise. |
numberPosition | 'before' | 'replace' | 'before' | Where the generated number stands. 'before': before the title, joined by numberSeparator. 'replace': the number is the whole title and the title written in the source is not printed, so # Night under numberingTemplate: 'الليلة {1:ordinal-feminine}' prints الليلة الثانية. The contents list the number as the entry's title (no number column), and the running heads (, ) and the PDF bookmarks read it too; is empty for such a heading. Only for numbered headings with a template (the level's or their style's); any other keeps its title. A heading style may set its own, 'before' to keep the titles its level would replace. |
breakBefore | HeadingBreakBeforeConfig | H1: { enabled: true, parity: 'always-odd' }H2–H6: { enabled: false, parity: 'any' } | Force a page break before every heading of this level. parity: 'odd' / 'even' further constrains which side of the spread the heading opens on — a blank padding page is inserted when needed (still counted in the page numbering). 'always-odd' / 'always-even' additionally guarantee at least one mandatory blank separator page between the previous content and the new heading (the separator belongs to the previous chapter; any further parity padding belongs to the new one). When the heading is the very first block of the document and the first page is still empty, parity enforcement is skipped — the heading lands on page 1 as written. A field left unset keeps the level default: H1's { parity: 'odd' } still breaks. |
hidden | boolean | false | A structural heading: it prints nothing and takes no room in the column or in a callout box — no text, no margins, no opener band — but does everything else a heading does. Its breakBefore still opens a page, it opens its style's section, it counts (unless its style says numbered: false), and it is listed by :::toc, named by running heads and bookmarked in the PDF. For a dedication, an epigraph page or a colophon that the contents and the reader's bookmarks need, but the page does not show. Set it on a heading style rather than a whole level; a heading overrides it with / . |
headings: {
fontFamily: 'Merriweather',
levels: [
// Canonical book preset: chapters on a right-hand (odd) page.
{ level: 1, fontSize: { value: 24, unit: 'pt' }, breakBefore: { enabled: true, parity: 'odd' } },
{ level: 2, fontSize: { value: 18, unit: 'pt' }, italic: true },
]
}snapToGrid works per level too. Unset, a level follows headings.snapToGrid; set, it overrides it — so one document can set its H2s a line and a half above their text, off the grid until the next snap point, while its H3s round the space under them up to whole grid lines. A heading style can set it as well, for the headings that use it.
headings: {
marginBottom: { value: 1.5, unit: 'em' },
levels: [
{ level: 2, snapToGrid: false }, // exactly 1.5 em under every H2
{ level: 3 }, // inherits headings.snapToGrid: true
],
}#Break before
breakBefore is orthogonal to the numbering controls: turning it on forces a page break, but the numeric counter only resets when you explicitly insert a :::numbering directive. Blank parity pages count as real pages in the sequence and receive headers/footers according to their normal odd/even rules.
A :::pagebreak right before such a heading does not replace its break: the heading still applies its parity after the page the directive opened, which can add a blank page. See Heading styles for a heading that should start right after a manual break.
Parity values
| Value | Behavior |
|---|---|
'any' (default) | No parity constraint. The heading just opens on the next page. |
'odd' | Ensure the heading opens on an odd (right-hand) page. A single blank is inserted only when the natural next page is even. |
'even' | Same but for an even (left-hand) page. |
'always-odd' | Guarantee at least one mandatory blank separator page between the previous content and the new heading, then ensure odd parity. Useful when every chapter must start on a fresh spread. |
'always-even' | Same but for an even page. |
Blank-page ownership
Blank pages inserted by breakBefore carry chapter-title headers based on why they were inserted:
- Pages inserted to satisfy a parity constraint (
'odd','even', or the parity tail of'always-*') belong to the upcoming chapter. Their{chapterTitle}header placeholder resolves to the new chapter's title — because the blank exists only to push the new chapter onto the correct parity. - The mandatory leading separator inserted by
'always-odd'/'always-even'belongs to the previous chapter. It's a deliberate end-of-chapter breath, so the{chapterTitle}header still shows the old chapter's title.
The running heads and palette of a styled section (see Heading styles) follow the same two rules on blank pages.
Document-start exception
When the very first block of the document is a heading with breakBefore enabled — or the source opens with :::pagebreak — parity enforcement is skipped while the first page is still empty. The heading lands on page 1 as written, regardless of the configured parity, so a document that begins with a # Chapter 1 configured parity: 'odd' doesn't inherit a spurious leading blank. Once any content has been placed, parity enforcement behaves normally.
#Span and advanced design
Each heading level accepts two additional fields that control how the heading renders as a full page-wide chapter opener.
| Property | Type | Default | Description |
|---|---|---|---|
span | 'column' | 'page' | 'column' | When 'page', the heading is treated as a chapter opener and its advanced design (if enabled) is attached to the page as an opener band above the body. Pair with breakBefore.enabled: true so the opener reliably begins a new page. Without a design of its own, the title is painted by a default opener across the whole content area, in the level's typography and leading, with its bold, italic and script runs (headings.inlineMarks), and the band is measured at that width. The band holds every line the opener paints: where the opener takes more lines than the heading's own measure (a justified title whose spaces would shrink onto one line, a forced break ), the band takes them too. Up to postext 1.4 the band was measured with the title wrapped at the column width, so a title that fits the content area on one line took a band two lines tall, with the line centred in it. The other columns start under the band as the heading's own column does. Text that opens one of them starts where text right under the opener would start, whatever follows the opener in its own column: the opener's marginBottom below its title or design, taken to the next grid line when the level snaps. A heading, a display formula or a contents row that opens one is set level with the first heading or display formula under the opener, margins included as there; when the opener's column goes on with anything else, it starts where that text does. A hidden heading under the opener does not count, and the space column balancing adds above the heading under the opener is not repeated in the other columns. What these columns snap to the grid lands on the page's grid. Up to postext 1.4 their first block sat at the band's foot: off the grid when the band ended between two lines, and right against the band, with no margin, when a heading followed the opener (a line higher than now when the band ended on the grid); a heading there also dropped the top margin the first column's heading kept. |
spanBreak | boolean | true | Whether a span: 'page' heading starts a new page. true opens the next page (its side picked by breakBefore). false, with breakBefore.enabled: false, opens it where the text reaches, as a page-span box does: the columns above it end level (the band is balanced when headings.balancing.trailing is on), the heading's design or default opener is set across the content area under them, and the text goes on in every column below: a second article of a bulletin under the end of the first. When the room left would not hold the heading and the widow minimum of lines under it, it opens the next page as with true. The page's role stays the one its first block gives it. No effect on a span: 'column' heading. Also on a heading style. Since postext 1.19. |
advancedDesign | HeadingAdvancedDesignConfig | | Free-composition design slot for this level. When enabled, the slot's elements compose the opener. Use inside a text element to render the heading's title text; use , , etc. to insert the formatted heading number. |
advancedDesign.minHeight | Dimension | — | Minimum height reserved for the heading in the column flow. The heading takes max(design content bottom, minHeight), then the heading's marginBottom under it (from its heading style, its level or headings.marginBottom; 0.5 em of the heading's size by default), and the sum is rounded up to the baseline grid when headings snap. So an opener can push the body text down (or claim the whole page) even when its elements are short or anchored to the page/bleed frames above the heading. For a band exactly minHeight tall, set that marginBottom to 0 and make minHeight a whole number of grid lines. Applies whenever enabled is true, even with an empty slot. See Reserved height for what the design content bottom counts. |
An opener as tall as the page. When the reserved height reaches past the foot of the column — a cover whose minHeight is the page height, an image or a box anchored to the page or the bleed and running down to the trim — the opener claims the rest of the page: the text after it starts on the next page, in every column of a two-column or column-and-a-half layout alike. A cover therefore needs no :::pagebreak after it (one does no harm: it adds no blank page). An in-column heading (span: 'column') whose design is taller than its column claims that column, and the text starts at the head of the next one. The heading's block then runs down to the foot of the column it claims, and its design is laid out against that band: elements anchored to the top stay where they are anchored, and those that follow the band — anchored to its middle or foot, or 'fill' tall — keep to the room the heading claims, as they keep to the reserved height when it fits (up to postext 1.4 the block kept the height of its text, so those elements were laid out against a band one title tall, and the text after it ran on under the design). Page furniture that should not claim the page — a fore-edge stripe down the whole page, an ornament at the foot of the page — belongs in the header or footer design instead: anchored to 'page' or 'bleed' and shown with pages: 'opener', it paints on the opener page and reserves no body space.
Example — a minimalist chapter opener that shows "Chapter N" above the title:
{
"headings": {
"levels": [
{
"level": 1,
"span": "page",
"breakBefore": { "enabled": true, "parity": "always-odd" },
"advancedDesign": {
"enabled": true,
"slot": {
"elements": [
{
"kind": "text",
"id": "chapterLabel",
"placement": {
"anchor": { "to": "container", "edge": "top" },
"offset": { "y": { "value": 48, "unit": "pt" } },
"size": { "width": "fill" }
},
"content": "Chapter {numberRoman}",
"fontSize": { "value": 10, "unit": "pt" },
"align": "center",
"overflow": "ellipsis-end"
},
{
"kind": "text",
"id": "chapterTitle",
"placement": {
"anchor": { "to": "#chapterLabel", "edge": "below" },
"offset": { "y": { "value": 12, "unit": "pt" } },
"size": { "width": "fill" }
},
"content": "{titleText}",
"fontSize": { "value": 24, "unit": "pt" },
"fontWeight": 700,
"align": "center",
"overflow": "wrap",
"hyphenate": true
}
]
}
}
}
]
}
}Heading placeholders available inside a level's design slot:
{titleText}— the heading's plain text (without numbering prefix). A forced break in the title (\\) is a line break here, in an opener band and in an in-column design alike (up to postext 1.4 an in-column design printed a space there, while its height was measured with the break). The lines the hidden heading wraps into in its column are joined back into the title as written: a word broken after its own hyphen keeps the hyphen with no space after it, a word the column divided is whole again without the hyphen the break added, a group glued by a no-break space that was wider than the column and was parted at that space gets its no-break space back (Capítulo XVIII, notCapítuloXVIII), and each other break gives back its one space. Up to postext 1.4 every line break became a space, so a title that wrapped at a hyphen printedWord- Book, and a forced break in such a title was dropped.{number}— the formatted number per the level'snumberingTemplate.{numberDecimal},{numberRoman},{numberRomanLower},{numberAlpha},{numberAlphaLower}— the heading's counter (its level's running count, whatever the template prints) in other numeral formats: the third chapter gives3,III,iii,C,c, with or without anumberingTemplate. An unnumbered heading (a style withnumbered: false) leaves them empty.{numberWords},{numberWordsLower},{numberOrdinalWords},{numberOrdinalWordsLower}— the same counter spelled out, with a capital or in lower case: Three / three, Third / third (see Spelled-out numbers).{numberHan}— the same counter in Chinese numerals, in the script of the document'slocale: an opener set with第{numberHan}回prints 第十二回 for the twelfth chapter while the contents list12.{chapterNumber},{chapterTitle},{pageNumber},{totalPages},{bookTotalPages},{title},{subtitle},{author},{publishDate}— shared metadata placeholders.{attr.<key>}— an attribute written on the heading line itself (# Title {author="I. Zango Martín"}), falling back to the current chapter's H1 attribute. Missing attributes resolve to an empty string with no warning.
{chapterNumber} prints what the running heads print for the chapter: the H1's number when its level (or style) has a numberingTemplate, else the chapter ordinal — 1, 2… continuing past the chapters laid out before this one — and nothing for an unnumbered chapter or a style whose template is ''. A heading design reads the chapter its heading belongs to: a level-1 heading its own, a lower heading the last level-1 heading before it. That holds where two chapters meet on one page too, while the running heads of that page print the later chapter. The height an opener reserves is measured with that same value. Up to postext 1.4 it was measured with the heading's number prefix (empty without a template), so a design whose height depends on {chapterNumber} could paint taller than the room it took; and the design printed the page's chapter, so the first of two chapters sharing a page showed the second one's number.
The counter placeholders follow the book across chapters laid out one at a time (the counters continuationAfter() hands on), and a heading's startAt attribute restarts them (see Document format → Heading attributes). An opener that says Chapter III over a title numbered 3. in the flow and the contents:
{
level: 1,
span: 'page',
numberingTemplate: '{1}.',
advancedDesign: {
enabled: true,
slot: { elements: [
{ kind: 'text', id: 'label', content: 'Chapter {numberRoman}', fontSize: { value: 10, unit: 'pt' }, overflow: 'ellipsis-end',
placement: { anchor: { to: 'container', edge: 'top-left' }, size: { width: 'fill', height: 'auto' } } },
{ kind: 'text', id: 'title', content: '{titleText}', fontSize: { value: 24, unit: 'pt' }, overflow: 'wrap',
placement: { anchor: { to: '#label', edge: 'below' }, size: { width: 'fill', height: 'auto' } } },
] },
},
}Reserved height
A heading with an advanced design takes room in the column like any block: the body text after it starts below that room. The room is the tallest of three heights, all measured down from the heading's top (the top of the content area for an opener that starts its page):
- the heading's own text, set in the level's typography (it is hidden under the design, but keeps its lines);
- the design content bottom — the lowest bottom edge among the elements that count (below);
advancedDesign.minHeight.
The heading's marginBottom is added after it (from its heading style, its level or headings.marginBottom; 0.5 em by default), and the result is snapped up to the baseline grid when headings snap. For an opener (span: 'page') the same band is kept free in every column of the page. For an in-column heading that fits its column, the design is laid out in the heading's box, which is exactly that tall.
Which elements count. Every element of the design counts — text, rules, boxes and pictures alike (up to postext 1.4 an image element never counted, so the text could start over a band picture unless minHeight held it off) — except:
- elements with
reserve: false— decoration that may sit under the text; - elements that follow the band itself — anchored to the container's middle row (
left,center,right) or bottom row (bottom-left,bottom,bottom-right), with a'fill'height against the container (a box or a vertical rule with no height fills it by default), and any element anchored to one of those. The container is the reserved band, so these sit on its foot or span it: a rule under the band, a tinted panel behind the title. They follow the height; they never set it. A text among them that keeps its own height still needs room: a title anchored to the foot of the band makes the band at least as tall as the title, so the title never starts above the heading's top. WithminHeight: 36mmand the title anchoredbottom-left, the heading's box is 36 mm plus itsmarginBottom, snapped up to the grid, and the title sits at the foot of that box, right above the text that follows; withoutminHeight, a title that takes more lines than the heading's own text makes the band as deep as the title. Up to postext 1.4 such a title was painted up over the text above the heading. Boxes, rules and pictures that follow the band set no such floor, so a panel anchored to the foot can reach above the heading.
Page- and bleed-anchored elements count by how far they reach below the heading's top. A band across the top of the page that ends above the heading counts nothing; a full-bleed picture that runs past it pushes the text down to its bottom edge. So does anything low on the page: a seal 25 mm above the page foot, a full-height side band or a frame reserves the page down to its bottom edge, and the text usually starts on the next page. Mark such decoration reserve: false — it still paints and elements can still anchor to it — and give the heading the room it needs with its text elements or minHeight:
{
"kind": "image", "id": "seal", "resourceId": "seal", "reserve": false,
"placement": {
"anchor": { "to": "page", "edge": "bottom-right" },
"offset": { "x": { "value": -25, "unit": "mm" }, "y": { "value": -25, "unit": "mm" } },
"size": { "width": { "value": 30, "unit": "mm" } }
}
}An element anchored to a non-reserving one still counts unless it is marked too (a caption set on the seal needs its own reserve: false).
Where it paints. An opener's design (span: 'page') is drawn before the body, so decoration that reserves nothing lies under the text. An in-column heading's design is drawn with the heading block — over the blocks above it in the column, under those after it — wherever it is placed above the foot of its column: in the side margins, the top margin, the bleed and the neighbouring columns too. The foot of the column cuts it, in the canvas and the PDF, since the flow ends there (see Taller than the column below). Up to postext 1.4 the canvas and the PDF also cut it at the top of its column, so a band anchored to the top of the page or of the bleed ran into the side margins but stopped at the top margin. Decoration anchored to the foot of the page belongs in an opener, or in the footer design with pages: 'opener'.
Taller than the column. When the room reaches past the foot of the column — a minHeight as tall as the page, a picture or a frame running down to the trim — the heading claims the rest of its page (an opener, in every column) or of its column (an in-column heading), and the text after it starts on the next page or column. Its block then runs to the column's foot, never cut back to the height of its text, so the band its design is laid out against is the room it claims (minHeight included, up to the foot). A design taller than the page itself — a long lead on a small screen page — is still cut at the page's foot (an in-column design, at its column's): the Sandbox lists it in the Checks panel as Heading design cut off. The layout raises no warning for it — a cover that claims its page is the usual case and loses nothing — but any host can run the same check on the finished layout: collectHeadingDesignCuts(doc) returns one { kind: 'headingDesignCut', pageIndex, level, where, overflowPx, sourceStart, sourceEnd } for each heading whose design text lies past the foot of the page's trim (where: 'page', an opener) or of its column (where: 'column'), and formatWarning describes each one. See An opener as tall as the page under Span and advanced design.
The side column. In a one-and-a-half layout whose side column holds floats (sideColumnRole: 'floats'), an element of an in-column heading's design that stands in the side column — a chapter numeral anchored to the page in the outer margin column of a textbook opener — keeps the side stack off it. Each span: 'side' figure, table or box the page sets after the heading keeps one float gap clear of every such element: it stays where the stack puts it when it fits above the element, and otherwise goes under it — or waits for the next page when the rest of the side column cannot hold it there. So a numeral at the head of the channel holds the whole stack under it, while a section number hung in the margin beside a heading further down the page leaves the head of the channel to the figures the page cites (the marginal figure still sits at the top of its page). What the side column already holds when the heading is placed is not moved: a figure stacked earlier on the page that reaches down to the heading's element stays where it is, under the element, so a design whose element stands in the side column mid-page wants its figures cited after the heading. Elements with reserve: false leave the side column free, as they leave the text. An opener (span: 'page') needs nothing of the kind: its band is reserved in every column, the side column included. Up to postext 1.4 a side figure cited on such an opener was set at the head of the side column, over the numeral.
Covers. A cover heading that fills its page — minHeight as tall as the page, or a full-page picture in its design — therefore sends the text after it to the next page by itself, in single- and multi-column layouts alike. A :::pagebreak right after it is optional and does no harm: a page break on a page that is still empty does nothing, so it never adds a blank page. It is needed only when the cover's design stops short of the page foot and the text should still start on a fresh page.
# Annual report 2026 {style="cover"}
:::pagebreak
# Letter from the chair#Spelled-out numbers
Two numbering-template suffixes write a counter in words, in the document language (the top-level locale, else the hyphenation locale — see Document language): words for the cardinal and ordinal for the ordinal. The case of the suffix sets the case of the words, as A / a does for letters:
| Token | English (21) | Spanish (21) | Chinese (21) |
|---|---|---|---|
| twenty-one | veintiuno | 二十一 |
| Twenty-one | Veintiuno | 二十一 |
| TWENTY-ONE | VEINTIUNO | 二十一 |
| twenty-first | vigesimoprimero | 第二十一 |
| Twenty-first | Vigesimoprimero | 第二十一 |
| TWENTY-FIRST | VIGESIMOPRIMERO | 第二十一 |
English, Spanish, Chinese and Arabic are spelled out; any other language takes the English words, as the built-in table continuation strings do. Chinese writes the informal numerals of simp-chinese-informal or trad-chinese-informal, after the script of locale (一万 / 一萬), with 第 before an ordinal; Han characters have no case, so the three spellings of a suffix print alike. English follows American usage (one hundred five, no and). Spanish uses masculine forms, as a capítulo or a libro is numbered (capítulo primero, tercero, veintiuno), and writes the ordinals from 13 to 29 as one word, as the RAE prefers (decimotercero, vigesimoprimero). Cardinals are spelled up to 999 999 and Spanish ordinals up to 999; larger numbers print in digits.
Arabic numbers agree in gender with the noun they count, so the suffix takes a modifier: -feminine (or -f) for a feminine noun, -masculine (-m, the default) for a masculine one; -classical spells the hundreds مائة, as Bulaq and most Egyptian prints do, instead of the modern مئة. {1:ordinal} writes the definite nominative ordinal a heading uses: الفصل {1:ordinal} gives الفصل الأول, الفصل الحادي عشر, الفصل الحادي والعشرون; الليلة {1:ordinal-feminine} gives الليلة الأولى, الليلة الحادية عشرة, الليلة الحادية والعشرون, الليلة المئتان, and above a hundred the classical "after" formula: الليلة الخامسة والأربعون بعد الثلاثمئة, الليلة الحادية بعد الألف. {1:words} writes the cardinal (واحد وعشرون; feminine إحدى عشرة, واحدة وعشرون). Ordinals are spelled up to 9 999 and cardinals up to 99 999; the modifiers combine ({1:ordinal-f-classical}), and other languages ignore them. Arabic has no capitals, so the case of the suffix changes nothing. The design placeholders {numberWords} and {numberOrdinalWords} write the masculine forms; for a feminine opener, put the ordinal in the level's template and print it with {number}.
In a heading design, {numberWords} / {numberWordsLower} and {numberOrdinalWords} / {numberOrdinalWordsLower} spell the heading's counter the same way, so the opener can say Chapter One while the contents list 1. A text element's textTransform: 'uppercase' gives the capitals:
// Spanish novel: "CAPÍTULO PRIMERO" above the title, "1." in the contents.
{ level: 1, numberingTemplate: '{1}.', span: 'page',
advancedDesign: { enabled: true, slot: { elements: [
{ kind: 'text', id: 'n', content: 'Capítulo {numberOrdinalWordsLower}', textTransform: 'uppercase', /* … */ },
{ kind: 'text', id: 't', content: '{titleText}', /* … */ },
] } } }#Unordered Lists
The unorderedLists property controls how bullet lists (-, *, +) and GFM task lists (- [ ], - [x]) are rendered. Up to five levels of nesting are supported.
#Unordered list defaults
| Property | Type | Default | Description |
|---|---|---|---|
fontFamily | string | inherits bodyText.fontFamily | Font used for the item text. |
color | ColorValue | Main Color (#295AA3) | Text and bullet color for items. Bound to the default palette's main-color entry. |
fontWeight | number | 700 | Weight for item text (100–900). Bullets inherit this weight unless overridden per-level. |
italic | boolean | false | Render item text in italic. |
bulletChar | string | '•' | Glyph used as the bullet marker. |
bulletFontSize | Dimension | 1 em | Size of the bullet glyph. Relative units scale with the body font size. |
gap | Dimension | 0.5 em | Horizontal space between the bullet and the item text. |
indent | Dimension | 0 em | Base indent for level 1. Deeper levels cascade from the parent's text-start unless overridden (see below). |
bulletVerticalOffset | Dimension | 0 em | Fine-tune bullet vertical position. Negative values move the bullet up, positive values move it down. |
marginTop / marginBottom | Dimension | 1.5 em | Space before and after the list as a whole. |
itemSpacing | Dimension | 0 em | Extra vertical space inserted between items on top of the line height. Around a list nested in an item of another, the outer list's spacing applies on both sides, before the nested list's first item and after its last (up to postext 1.4 the item after a nested list took the nested list's spacing). |
snapTopToGrid | boolean | false | Round the space above the list up so its first bullet sits on the baseline grid, as the text under a heading does; marginTop is then a minimum. The end of a list snaps the flow back onto the grid either way, so with itemSpacing at 0 every item lines up with the text in the column beside it. Off by default, as up to postext 1.4: a marginTop that is not a whole number of lines leaves the items off the grid until the list ends. Lists inside callout boxes, whose interiors are off the grid, are not affected. |
hangingIndent | boolean | true | When enabled, wrapped lines align with the first text character rather than under the bullet. |
levels | UnorderedListLevelConfig[] | — | Per-depth overrides for levels 1–5. See below. |
#Task list extensions
GFM task items (- [ ] …, - [x] …) are rendered as unordered items with a checkbox glyph replacing the bullet. The following fields only apply to task items:
| Property | Type | Default | Description |
|---|---|---|---|
taskCheckboxChar | string | '☐' | Glyph used for unchecked tasks. |
taskCheckedChar | string | '☑' | Glyph used for completed tasks. |
taskCompletedStrikethrough | boolean | true | Draw a strikethrough line across the text of completed tasks. |
taskCompletedColor | ColorValue | inherits item color | Optional color applied to completed task text. When omitted, the regular item color is used. |
#Unordered per-level overrides
Each entry in levels targets one depth (1–5) and can override any of the following:
| Property | Type | Description |
|---|---|---|
bulletChar | string | Bullet glyph for this depth. |
fontFamily | string | Item font family for this depth. |
fontSize | Dimension | Bullet glyph size for this depth. |
color | ColorValue | Item color. |
fontWeight | number | Item weight. |
italic | boolean | Italic toggle. |
indent | Dimension | Explicit indent for the bullet at this depth. See the cascade rule below. |
verticalOffset | Dimension | Vertical fine-tune for the bullet at this depth. |
Indent cascade. Level 1 always starts at the general indent value (by default 0 em — bullets are pinned to the column edge). For levels 2–5, if you leave indent undefined the engine places the bullet at the previous level's text-start (parent indent + bullet width + gap). Set an explicit indent on a level to break the cascade and pin that depth anywhere you like.
unorderedLists: {
bulletChar: '—',
gap: { value: 0.4, unit: 'em' },
hangingIndent: true,
levels: [
{ level: 2, bulletChar: '·' },
{ level: 3, bulletChar: '◦', color: { hex: '#666666', model: 'hex' } },
],
}#Ordered Lists
The orderedLists property controls numbered lists (1., 2), etc.). Up to five levels of nesting are supported and each depth can use a different number format.
#Ordered list defaults
| Property | Type | Default | Description |
|---|---|---|---|
fontFamily | string | inherits bodyText.fontFamily | Font used for the item text and the number marker. |
color | ColorValue | Main Color (#295AA3) | Text and number-marker color for items. Bound to the default palette's main-color entry. |
fontWeight | number | 700 | Weight for item text and number markers (100–900). |
italic | boolean | false | Render item text in italic. |
numberFormat | OrderedListNumberFormat | 'arabic' | Number style: 'arabic', 'lower-alpha', 'upper-alpha', 'lower-roman', 'upper-roman'. The other settings' spellings work too ('decimal', 'roman-lower', 'i'…; see Numbering format spellings); an unknown value numbers in arabic and is reported. |
prefix | string | '' | Text set before the number, in the separator's style: with '(' here and ')' as the separator a Chinese list reads (一), (二). When the separator is drawn as its own run, the prefix is one too, just before the number. |
separator | string | '.' | Character placed between the number and the text — typically '.' or ')'. |
separatorFontFamily | string | inherits fontFamily | Font of the separator. When any separator style differs from the number's, the separator is drawn as its own run after the (right-aligned) number — e.g. 1 in Optima Bold black followed by • in DIN Pro Bold blue. |
separatorFontWeight | number | inherits fontWeight | Weight of the separator (100–900). |
separatorItalic | boolean | inherits italic | Render the separator in italic. |
separatorColor | ColorValue | inherits color | Colour of the separator. Palette references are honoured. |
separatorGap | Dimension | 0 em | Space between the number and the separator. The item text still starts gap after the separator. |
numberFontSize | Dimension | 1 em | Size of the number marker. |
gap | Dimension | 0.5 em | Horizontal space between the number and the item text. |
indent | Dimension | 0 em | Base indent for level 1; deeper levels cascade from the parent's text-start unless overridden. |
numberVerticalOffset | Dimension | 0 em | Fine-tune the vertical position of the number marker. |
marginTop / marginBottom | Dimension | 1.5 em | Space before and after the list as a whole. |
itemSpacing | Dimension | 0 em | Extra vertical space between items. Around a list nested in an item of another, the outer list's spacing applies on both sides, before the nested list's first item and after its last (up to postext 1.4 the item after a nested list took the nested list's spacing). |
snapTopToGrid | boolean | false | Round the space above the list up so its first number sits on the baseline grid, as the text under a heading does; marginTop is then a minimum. The end of a list snaps the flow back onto the grid either way, so with itemSpacing at 0 every item lines up with the text in the column beside it. Off by default, as up to postext 1.4: a marginTop that is not a whole number of lines leaves the items off the grid until the list ends. Lists inside callout boxes, whose interiors are off the grid, are not affected. |
numberWidth | 'run' | 'level' | 'run' | How wide the number column of an item is, which sets where its text starts; the numbers are set flush right in it. 'run': the widest number of the item's own run, the items of one depth with nothing but deeper items between them. A figure, a paragraph or a box between two items starts a new run, so ii) after a table can start its text a little further right than i) before it, and a list of nine items sets its text left of a list of twelve. 'level': the widest number at the item's depth in the whole document (the chapter, in a book), so every list, and every part of an interrupted one, starts its text at the same place, as the indents of the deeper levels already do. |
numberAlign | 'end' | 'start' | 'end' | How a number sits in its column. 'end': against the text, so the numbers of a run end together and 9. and 10. line up on their full stops. 'start': at the start of the column (the left on a left-to-right page, the right on a right-to-left one, the head of the line in vertical text), so labels of different lengths begin together, as the articles of a statute do (第九條, 第十一條). The column keeps the width numberWidth gives it, so the text of the items starts at the same place either way; with a separator in a style of its own, the separator follows its number. Since postext 1.26. |
hangingIndent | boolean | true | Wrapped lines align with the first text character rather than under the number. |
levels | OrderedListLevelConfig[] | — | Per-depth overrides for levels 1–5. |
#Ordered per-level overrides
Each entry in levels can override numberFormat, prefix, separator, fontFamily, fontSize, color, fontWeight, italic, indent, verticalOffset and the separator style (separatorFontFamily, separatorFontWeight, separatorItalic, separatorColor, separatorGap) — the same indent cascade as unordered lists applies. A level's separator style inherits its own number style unless the list-wide separator setting is given.
Right alignment. The pipeline measures the widest formatted number within a run and indents all items in that run so the number markers line up on their right edge. A list of ten items rendered as 1. – 10. has the single-digit numbers right-padded so the separator stays in the same column.
orderedLists: {
numberFormat: 'arabic',
separator: '.',
levels: [
{ level: 2, numberFormat: 'lower-alpha' },
{ level: 3, numberFormat: 'lower-roman', separator: ')' },
],
}This yields the classic nested mix:
1. First item
a. Sub-item
i) Deep note
b. Sub-item
2. Second item
The hierarchy of Chinese documents (GB/T 15834—2011, Annex B.3) takes five levels, 一、 then (一) then 1. then (1) then ①:
orderedLists: {
levels: [
{ level: 1, numberFormat: 'simp-chinese-informal', separator: '、' },
{ level: 2, numberFormat: 'simp-chinese-informal', prefix: '(', separator: ')' },
{ level: 3, numberFormat: 'arabic', separator: '.' },
{ level: 4, numberFormat: 'arabic', prefix: '(', separator: ')' },
{ level: 5, numberFormat: 'circled-decimal', separator: '' },
],
}#Math
The math property controls how LaTeX formulas inside $...$ (inline) and $$...$$ (display) delimiters are parsed and rendered. The underlying engine is MathJax (the mathjax-full package) in SVG output mode, rasterised to the canvas and embedded as scalable glyphs in the PDF.
interface MathConfig {
enabled?: boolean; // Render LaTeX. When false, spans pass through as literal TeX.
fontSizeScale?: number; // × the surrounding text's size (the body size for display maths).
color?: ColorValue; // Formula colour; inherits body colour if omitted.
marginTop?: Dimension; // Space above display math blocks.
marginBottom?: Dimension; // Minimum space below; baseline grid snap may enlarge it.
indentAfterDisplay?: boolean; // Indent a paragraph that follows a display formula.
keepWithLeadIn?: boolean; // Keep a display formula with the line that leads into it.
equationNumbering?: { // Number the display formulas that carry a \label.
enabled?: boolean; // default true
numberingTemplate?: string; // '{n}'; '{h1}.{n}' numbers by chapter
resetOn?: ResourceCounterReset; // 'never' | 'h1' … 'h6'
counterFormat?: ResourceCounterFormat; // 'decimal'
format?: string; // '({n})': what the formula and \eqref print
};
}| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | When false, $...$ and $$...$$ spans are still parsed (so unclosed-delimiter warnings still fire) but are rendered as their literal TeX source. Useful when the content intentionally contains dollar signs or when you want to disable math rendering entirely. |
fontSizeScale | number | 1.0 | Multiplier applied to the size of the surrounding text before rendering: one em of the formula's TeX font is that size × fontSizeScale — bodyText.fontSize for a display formula and for inline maths in body text, the enclosing block's size for inline maths in a heading, a paragraph style, a caption or a callout body. 1.0 matches the surrounding text; values in the 0.9–1.1 range are typical when the math font looks slightly larger or smaller than the prose font. Changed in postext 1.5: up to 1.4 formulas came out about 13% larger than this (see below). |
color | ColorValue | inherits body colour | Colour of the rendered formula. Omit to inherit bodyText.color. Set explicitly when you want formulas tinted differently from prose — e.g. matching a heading accent. |
marginTop | Dimension | 0.8em | Space above a display math block. Ignored for inline math. |
marginBottom | Dimension | 0.8em | Space below a display math block. It is a minimum: the grid snap may extend it so the next baseline falls on a grid line (whether or not page.baselineGrid draws the grid). |
indentAfterDisplay | boolean | true | Indent the first line of a paragraph that follows a display formula, as any other paragraph. false sets every paragraph right after a display formula flush, as the continuation of the sentence the formula interrupted ("where L is…"). A formula written inside a paragraph — with no blank line above it nor below it — is always followed flush: the text under its closing $$ continues that paragraph and is never indented (see Mathematical formulas). |
keepWithLeadIn | boolean | false | Keep a display formula in the column of the line that leads into it — TeX's predisplay penalty. When the formula does not fit under the last line of the paragraph before it, that line goes to the next column or page with the formula; when the lines left behind would be fewer than bodyText.widowMinLines (fewer than one when bodyText.avoidWidows is off), a paragraph that opens in that column moves on whole (with the headings closing the column above it, under headings.keepWithNext). The line carried over stands alone at the head of the next column, whatever the orphan rule asks. With false the formula alone moves on, and the line that introduces it can close the column above, or sit over a figure heading the next one. |
equationNumbering | { enabled, numberingTemplate, resetOn, counterFormat, format } | true, '{n}', 'never', 'decimal', '({n})' | How the display formulas that carry a \label are numbered (see Numbered equations below). numberingTemplate, resetOn and counterFormat work as a resource type's: '{h1}.{n}' with resetOn: 'h1' numbers (2.1), (2.2)… by chapter, '{h1}.{h2}.{n}' with 'h2' by section. format is the number as the formula and \eqref print it, {n} standing for the number: '[{n}]' sets [3]. enabled: false numbers nothing: a \label is dropped and a reference to it prints its page. |
math: {
enabled: true,
fontSizeScale: 1.0,
color: { hex: '#295AA3', model: 'hex' },
marginTop: { value: 1, unit: 'em' },
marginBottom: { value: 1, unit: 'em' },
}Formula size, changed in postext 1.5. MathJax gives a formula's box in ex, and one ex of its TeX font is 0.442 em. Up to postext 1.4 the engine took it as half an em, so every formula was set about 13% larger than bodyText.fontSize × fontSizeScale. Formulas now come out at the documented size, and lines and pages with maths reflow. A configuration written in code for 1.4 keeps the 1.4 formula size by passing through pinLegacyMathSize from postext/bundle, once, as written:
import { pinLegacyMathSize } from 'postext/bundle';
config = pinLegacyMathSize(config); // formulas, and the space around display formulas, as 1.4 set themOther rule changes in 1.5 can also move its pages: the heading breaks, the space around an inline figure (in the running text and in boxes), the inline marks of its headings, the size of its drop caps, the room kept under a colon line for the list it introduces, the lines a box cut leaves of a paragraph or list item, the line breaks after a dash, the breaking of ragged text, the split of a paragraph under a heading, the line breaks after a compound's hyphen and the space under a :::paragraphs container. migrateConfig, run once with the markdown the configuration lays out, pins those this text needs, the formula size included (see Bundles written by postext 1.4 or earlier):
import { migrateConfig } from 'postext/bundle';
config = migrateConfig(config, undefined, { content: markdown }); // heading breaks, formulas, inline gaps, heading marks, drop caps, colon lines, box cuts, dash breaks, compound breaks, ragged breaking, splits under a heading and container space as 1.4 set themThat holds back what these rule changes would move, but it does not keep every 1.4 page. Version 1.5 also fixes layout bugs, and a fix has no pin: an old configuration gets it as a new one does, so a page it touches can still move. Among them: a page-span heading without a design of its own is measured across the page and set in its level's leading; nothing is reserved under a drop cap's baseline in a heading design; a drop cap takes the section's palette colour, and is set even on a design text whose overflow is not 'wrap', which then wraps; a paragraph in a box paints the tracking it was measured with; a split box keeps its icon column on every fragment; a box line that would stretch its spaces past 3× is set ragged, as in running text; the loose-paragraph lever never sets a justified line wider than maxWordSpacing allows; a floated box keeps a marginBottom (in a top band) or marginTop (in a bottom band) wider than the float gap; a centred or right-aligned design text with tracking (a running head, an opener title) is placed by its letters, without the tracking after the last one; under a page-span opener, the text that opens the second column starts where text right under the opener would, also when a heading follows the opener; the contents' dots stop before the page number in a face that kerns a run of dots apart; a running head reads a heading as written, with no space where one of its lines ends after a hyphen or a dash or inside a word cut for width (MEDIOAMBIENTALES, not MEDIOAMBIENTALE S), and a heading design's {titleText} reads it the same way (thousand-colour, not thousand- colour); a text anchored to the foot or the middle of a heading design's band keeps the band tall enough to hold it, so it no longer rises over the text above the heading; a word wider than its line, cut next to a hyphen it carries, is cut after that hyphen and gets no second one; the item after a list nested in another takes its own list's itemSpacing, not the nested list's; and the rest of a word cut for being wider than its line keeps its own break points, so a web address goes on breaking at its joints and a compound at its hyphens instead of at the dictionary's syllables.
pinLegacyMathSize multiplies the scale by 1.1312 (0.5 ÷ 0.442) and divides the display margins in em by the same factor. The scale alone is not enough for a book with display formulas: their margins are lengths of the formula's own size, so they would grow by the same 13% and push the text below them down. Written out for the default margins, the three values are:
math: {
fontSizeScale: 1.131,
marginTop: { value: 0.7072, unit: 'em' },
marginBottom: { value: 0.7072, unit: 'em' },
} // formulas as large as 1.4 set them, with the space 1.4 left around themWhere the configuration was stored, the engine can tell and does it for you: openBundle / readBundle for a .postext bundle written before 1.5 (see Bundles written by postext 1.4 or earlier), and the Sandbox for the books, working copy and postext-config.json files saved then (see Sandbox → Persistence). They are read through migrateConfig, which pins the size (pinLegacyMathSize): fontSizeScale becomes the stored scale (1 when unset) × 1.1312, and a display margin in em or rem — a length of the formula's own size — is divided by the same factor, so the space around a display formula stays as 1.4 left it (the default 0.8 em becomes 0.7072 em). A margin in a page unit (pt, mm…) is left as it is, and so is a configuration with enabled: false or one whose book has no $. The old book then lays out as 1.4 laid it out, and its math section shows the size it is set at. To set such a book at today's size instead, reset those values — Formulas → Size scale, Display margin top and Display margin bottom in the Sandbox, or in code:
math: { ...config.math, fontSizeScale: 1, marginTop: undefined, marginBottom: undefined } // today's size and marginsNumbered equations. A display formula with a number spans its measure (the column, or the inner width of the callout it sits in): the equation is centred and its number set flush right on the equation's line, on every numbered row of an align. A number comes from a \label{eq:x} (since postext 1.19): the labelled formulas, and the labelled rows of an align, gather, alignat, flalign or eqnarray, are numbered in reading order with equationNumbering, unless a row says \nonumber or \notag. Environments such as equation are not numbered by themselves: a formula without a label has no number. \tag{…} prints the label you give it, in parentheses, and \tag*{…} as written; neither is counted, and a \label beside one names it. A numbered equation wider than its measure overflows to the right, like any display formula. (Up to postext 1.4 a formula with \tag was not drawn at all; up to 1.18 only \tag numbered one.)
\eqref{eq:x} in the text prints the number in its format, (3), and \ref{eq:x} the bare number; so do :ref{id="eq:x"} and @eq:x (see Cross-references), and inside a formula \eqref prints it as text. In a book laid out chapter by chapter the counter goes on from the previous chapter: continuationAfter carries it in LayoutContinuation.statementCounters (equation), and the book outline gives every label its number (OutlineEntry.numberLabel), so a reference to an equation of another chapter prints it.
math: {
equationNumbering: { numberingTemplate: '{h1}.{n}', resetOn: 'h1' }, // (1.1), (1.2)… (2.1)
}Resolver and stripper match the other sections:
import {
DEFAULT_MATH_CONFIG,
resolveMathConfig,
stripMathDefaults,
} from 'postext';
const resolved = resolveMathConfig(config.math);
const minimal = stripMathDefaults(config.math);#Starting the math engine
MathJax is loaded on demand, not with the rest of the engine. When you lay out on the main thread, start it before building a document that has formulas:
import { buildDocument, initMathEngine, renderPage } from 'postext';
await initMathEngine(); // loads MathJax once; later calls resolve at once
const doc = buildDocument({ markdown: 'Euler: $e^{i\\pi}+1=0$.' }, config);
document.body.append(renderPage(doc.pages[0], doc));- Until the engine runs, formulas are placeholders. Each one is laid out as a grey box of an estimated size. If a document with math is laid out before
initMathEngine()was ever called, the console shows one warning saying so. - The layout worker starts it for you. A build in
postext/workercallsinitMathEngine()itself when the markdown contains a$. - Lay out now, again when it is ready.
isMathReady()tells whether the engine is running.onMathReady(fn)callsfnonce it is (at once if it already is) and returns a function that cancels the call. An editor can show placeholders right away and lay out again when MathJax arrives; no warning is printed whileinitMathEngine()is in flight. - Failure.
initMathEngine()rejects when MathJax cannot be loaded, and a later call tries again. - Any bundler, Node or a CDN. MathJax ships inside the package as one pre-bundled module (about 1.8 MB before compression, fetched only by
initMathEngine). The plainimport { initMathEngine } from 'https://esm.sh/postext'works; no?bundleis needed. Themathjax-fullpackage is only needed to build postext, so installing postext does not install it. MathJax and the mhchem parser it bundles are Apache-2.0 licensed: their notices and the licence text ship next to the module, asdist/math/THIRD_PARTY_LICENSES.txt. - One engine per page. The engine and its cache of rendered formulas are shared by everything that imports
postextin the same JavaScript realm (see Global state shared in one page).
For the document-side grammar ($...$, $$...$$, escaping a literal dollar), see Document format.