In short
A short edition of the household accounts Thoreau printed in Walden. The numbers in the text are small like lower-case letters, the years in the headings stand as tall as capitals, and the sums in the accounts line up in columns.
What you'll build
A short edition of the accounts Thoreau printed in the first chapter of Walden: what his house cost, what the bean-field brought in, what he ate in eight months. It is set on a 140 × 216 mm page in the Alegreya family, with the 1854 engraving of the cabin over the opening. The figures change with the job they do. In the prose, March, 1845 and $13.34 use old-style figures that rise and fall like lower-case letters, so the dates do not stand out of the line. The headings are small capitals, and their years stand at the height of the capitals. In the accounts every figure is the same width, so the cents of thirteen lines of boards, laths and nails sit under one another down to the total.
This recipe answers
- How do I set old-style figures in the running text, lining figures in the headings and tabular figures in the tables?
- How do I set unnumbered artwork: ornaments, vignettes, logos?
The short answer
// Alegreya's own figures are old-style, so the prose asks for none: the cut Fontsource
// serves has no onum, and 'oldstyle' would change nothing (see the write-up).
const bodyText = {
fontFamily: TEXT, fontSize: pt(10.8), lineHeight: pt(LEAD), color: col('ink'),
boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
textAlign: 'justify', firstLineIndent: mm(4.5), indentAfterHeading: false,
hyphenation: { enabled: true }, optimalLineBreaking: true,
avoidWidows: true, avoidOrphans: true, avoidRunts: true,
};
// Small capitals take lining figures (lnum), at the height of the capitals around them.
const section = { level: 2, fontFamily: CAPS, fontSize: pt(12.5), lineHeight: pt(LEAD),
fontWeight: 400, letterSpacing: pt(0.6), color: col('accent'), figureStyle: 'lining',
marginTop: pt(LEAD), marginBottom: pt(0) };
// The accounts: every figure on one width (tnum), so the cents stand in columns.
// 'tabular' keeps the font's form, old-style; 'lining-tabular' would set them at cap height.
const tableStyle = {
figureStyle: 'tabular', bodyFontFamily: TEXT, bodyFontSize: pt(9.8), bodyColor: col('ink'),
rules: 'booktabs', groupRules: true, borderColor: col('rule'), lightRuleWidth: pt(0.4),
heavyRuleWidth: pt(0.7), headerBackgroundEnabled: false, headerFontFamily: TEXT,
headerFontSize: pt(9.8), headerColor: col('ink'), headerBold: false, headerItalic: false,
cellPadding: mm(0.7),
};
One figure style per text style
Ingredients
- Features
- Figure stylesTable styleTables from dataCustom resource typesFigure placementDesigned openersPictures in page designsHeading attributesRunning heads per sectionRunning heads and foliosMirrored marginsParagraph stylesSemantic colour palettePDF exportFonts embedded in the PDF
- Also uses
- Figures exactly hereFigure and Table in your languageHeads by page roleFigures and tables as resources
- Type
- Alegreya, Alegreya SC, Alegreya Sans (SIL OFL 1.1)
- Assets
cabin-860.jpg- The vignette of Thoreau’s cabin from the title page of the first edition (1854), cropped and levelled to black on white (Ticknor and Fields, Boston, 1854 (engraver unknown), public domain)
Method
#1 · One figure style per text style
The code is the short answer above. figureStyle asks a style's font for its OpenType figures (figure styles). The body asks for nothing, because Alegreya's own figures are already old-style and proportional: a 1 is 342 units wide and a 0 is 533. The level-2 headings ask Alegreya SC for 'lining', its lnum figures, which stand as tall as the capitals around them. The table style asks for 'tabular', the font's tnum: the figures keep their old-style form and all take the same width, 450 units each, so a column of prices reads straight down. 'lining-tabular' would set them at cap height instead (see Variations).
#2 · The accounts are tables of five columns
// A row starting '=' is a total (header cells: a light rule above it), '+' heads a group.
const C = (content, align = 'left', extra = {}) => ({ content, align, ...extra });
function account(tsv) {
const rows = tsv.replace(/^\n+|\n+$/g, '').split('\n').map((line) => {
const total = line.startsWith('='), group = line.startsWith('+');
const [item, dollar, amount, fraction, remark] = line.replace(/^[=+]/, '').split('\t');
if (group) return [C(item, 'left', { colSpan: 5, isHeader: true }),
...[1, 2, 3, 4].map(() => C('', 'left', { hiddenBy: { row: -1, col: 0 } }))];
const h = { isHeader: total };
return [C(item, 'left', h), C(dollar ? '\\$' : '', 'right', h), // `$` alone opens maths
C(amount ?? '', 'right', h), C(fraction ?? '', 'left', h), C(remark ?? '', 'left', h)];
});
rows.forEach((row, r) => row.forEach((cell) => cell.hiddenBy && (cell.hiddenBy.row = r)));
return { rows, headerRowCount: 0, columnWidths: [34, 2.4, 8, 3.6, 35] };
}
const accounts = String.raw`=== house
Boards, $ 8.03 ½ mostly shanty boards.
Refuse shingles for roof sides, 4.00
Laths, 1.25
Two second-hand windows with glass, 2.43
One thousand old brick, 4.00
Two casks of lime, 2.40 That was high.
Hair, 0.31 More than I needed.
Mantle-tree iron, 0.15
Nails, 3.90
Hinges and screws, 0.14
Latch, 0.10
Chalk, 0.01
Transportation, 1.40 I carried a good part on my back.
=In all, $ 28.12 ½
=== farm
$ 23.44
Deducting the outgoes, 14.72 ½
=There are left, $ 8.71 ½
=== food
Rice, $ 1.73 ½
Molasses, 1.73 Cheapest form of the saccharine.
Rye meal, 1.04 ¾
Indian meal, 0.99 ¾ Cheaper than rye.
Pork, 0.22
+All experiments which failed:
Flour, 0.88 Costs more than Indian meal, both money and trouble.
Sugar, 0.80
Lard, 0.65
Apples, 0.25
Dried apple, 0.22
Sweet potatoes, 0.10
One pumpkin, 0.06
One watermelon, 0.02
Salt, 0.03
=== outgoes
House, $ 28.12 ½
Farm one year, 14.72 ½
Food eight months, 8.74
Clothing, etc., eight months, 8.40 ¾
Oil, &c., eight months, 2.00
=In all, $ 61.99 ¾
=== income
$ 23.44
Earned by day-labor, 13.34
=In all, $ 36.78
`; // content.accounts.<lang>.md: one block per table
const tables = Object.fromEntries(accounts.split(/^=== /m).filter(Boolean)
.map((block) => [block.slice(0, block.indexOf('\n')).trim(), block.slice(block.indexOf('\n'))]));
Thoreau's accounts are kept as TSV in content.accounts.en.md, one block per account, so the Spanish edition translates the items and changes the decimal point to a comma without touching the code. Each row has an item, a dollar sign, the amount, a fraction and a remark. The halves and quarters have a column of their own: ½ is 713 units wide and ¾ is 689, more than a tabular figure, and Alegreya has no figure space to pad the other amounts with. A total is made of header cells, so groupRules draws a light rule above it, as Thoreau's printer drew a line before In all. The dollar sign is written \$, because a bare $ opens a formula in a cell as it does in the text. The resource type account has no number and no caption prefix, and its default placement is 'here', so each account sits where Thoreau's sentence introduces it.
#3 · The opener names its own figures
const at = (to, edge, y, x = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } });
// 'below' starts a text at the left edge of what it hangs from: the texts take the measure
// and start half the difference to the left of the vignette, so they centre on the page.
const centred = (to, y) => ({ ...at(to, 'below', y, (VIGNETTE - MEASURE) / 2),
size: { width: mm(MEASURE) } });
const opener = { enabled: true, minHeight: pt(19 * LEAD), slot: { elements: [
{ kind: 'image', id: 'cabin', resourceId: 'cabin',
placement: { ...at('container', 'top', 0), size: { width: mm(VIGNETTE) } } },
{ kind: 'text', id: 'kicker', content: '{attr.kicker}', fontFamily: LABEL, fontWeight: 500,
fontSize: pt(7.5), letterSpacing: pt(1.5), textTransform: 'uppercase', color: col('accent'),
align: 'center', placement: centred('#cabin', 6) },
{ kind: 'text', id: 'title', content: '{titleText}', fontFamily: CAPS, fontSize: pt(30),
letterSpacing: pt(1.2), lineHeight: 1, color: col('ink'), align: 'center',
placement: { ...at('#kicker', 'below', 2.5), size: { width: mm(MEASURE) } } },
// A design text names its own family, so it states its figures too.
{ kind: 'text', id: 'dates', content: '{attr.dates}', fontFamily: CAPS, fontSize: pt(11),
letterSpacing: pt(0.8), color: col('accent'), align: 'center', figureStyle: 'lining',
placement: { ...at('#title', 'below', 2.5), size: { width: mm(MEASURE) } } },
] } };
A design text sets its own family, so the figures of the body do not reach it. Concord, 1845–1847 is in Alegreya SC and asks for 'lining' itself, like the headings. The vignette is the engraving from the 1854 title page, cropped and levelled to black on white, and the kicker, the title and the dates take the measure and centre under it.
#4 · Running heads with the section on the recto
const head = (id, content, parity, edge, x, extra = {}) => ({
kind: 'text', id, content, parity, pages: 'body', fontFamily: LABEL, fontSize: pt(7.5),
fontWeight: 500, letterSpacing: pt(1.2), textTransform: 'uppercase', color: col('muted'),
placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(13) } }, ...extra });
const folio = { fontFamily: TEXT, fontSize: pt(9.5), fontWeight: 400, letterSpacing: pt(0),
color: col('ink') };
const header = { elements: [
head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio),
head('verso-title', '{title}', 'even', 'top-left', OUTER + 8),
head('recto-title', '{firstMark.h2}', 'odd', 'top-right', -(OUTER + 8)),
head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio),
] };
const footer = { elements: [head('drop-folio', '{pageNumber}', 'all', 'top', 0, {
...folio, pages: 'opener', placement: at('container', 'top', 8) })] };
The verso carries the book's title and the recto the section in force, {firstMark.h2}, so page 3 reads The Bean-Field, 1845. The folios are in Alegreya and keep its old-style figures.

The whole recipe
// ═══ Postext Cookbook · Nº 176 · Old-style, lining and tabular figures ═══════════════
// https://postext.dev/en/cookbook/old-style-and-tabular-figures
// Code: MIT · Text: H. D. Thoreau, Walden (1854, public domain) · Vignette: 1854 title page (PD)
// Fonts: Alegreya, Alegreya SC, Alegreya Sans (SIL OFL 1.1) · Needs postext ≥ 1.65.0
// Thoreau's accounts from Walden. Each text style asks Alegreya for one kind of figures:
// old-style in the prose, lining in the small-capital headings, tabular in the accounts.
import {
buildDocumentWithFonts, renderPageToCanvas, registerResourceImage, defaultResourceTypes,
} from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';
const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'old-style-and-tabular-figures';
// ─── 1 · Design ─────────────────────────────────────────────────────────────
const palette = {
ink: '#221e1a', // text: a warm near-black
accent: '#7d3a1f', // walnut: headings, kicker, rules of the opener
rule: '#8a7f73', // the light rules of the accounts
muted: '#6b6158', // running heads, the colophon
paper: '#ffffff',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
{ id: 'main-color', name: 'accent (defaults)', value: { hex: palette.accent, model: 'hex' } },
];
const TEXT = 'Alegreya', CAPS = 'Alegreya SC', LABEL = 'Alegreya Sans';
const TRIM = [140, 216], INNER = 19, OUTER = 16; // mm: a trade paperback
const MEASURE = TRIM[0] - INNER - OUTER, VIGNETTE = 50; // mm
const LEAD = 14.5; // pt: the body leading, the grid the headings keep to
// #region answer: one figure style per text style
// Alegreya's own figures are old-style, so the prose asks for none: the cut Fontsource
// serves has no onum, and 'oldstyle' would change nothing (see the write-up).
const bodyText = {
fontFamily: TEXT, fontSize: pt(10.8), lineHeight: pt(LEAD), color: col('ink'),
boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
textAlign: 'justify', firstLineIndent: mm(4.5), indentAfterHeading: false,
hyphenation: { enabled: true }, optimalLineBreaking: true,
avoidWidows: true, avoidOrphans: true, avoidRunts: true,
};
// Small capitals take lining figures (lnum), at the height of the capitals around them.
const section = { level: 2, fontFamily: CAPS, fontSize: pt(12.5), lineHeight: pt(LEAD),
fontWeight: 400, letterSpacing: pt(0.6), color: col('accent'), figureStyle: 'lining',
marginTop: pt(LEAD), marginBottom: pt(0) };
// The accounts: every figure on one width (tnum), so the cents stand in columns.
// 'tabular' keeps the font's form, old-style; 'lining-tabular' would set them at cap height.
The composed script.js runs as it is: paste it into any page’s module script, or open the recipe on CodePen. Recipe folder on GitHub ↗ (opens in a new tab)
Variations
#Set the accounts in lining figures
A ledger or a price list in a modern book often sets its figures at cap height.
- figureStyle: 'tabular', bodyFontFamily: TEXT, bodyFontSize: pt(9.8), bodyColor: col('ink'),
+ figureStyle: 'lining-tabular', bodyFontFamily: TEXT, bodyFontSize: pt(9.8), bodyColor: col('ink'),#Set the folios in lining figures
Folios in lining figures match running heads set in capitals.
-const folio = { fontFamily: TEXT, fontSize: pt(9.5), fontWeight: 400, letterSpacing: pt(0),
+const folio = { fontFamily: TEXT, fontSize: pt(9.5), fontWeight: 400, letterSpacing: pt(0),
+ figureStyle: 'lining',Pitfalls
- The latin files Fontsource serves often leave
onumout: Lora, EB Garamond, Source Serif 4, Crimson Pro, Newsreader and Literata keep onlypnumandtnum. With those filesfigureStyle: 'oldstyle'changes nothing and the text keeps lining figures. Pick a face whose own figures are old-style, such as Alegreya, Gelasio or Cormorant Garamond, which addlnumfor headings, or load the foundry's full files. - A
$in the Markdown or in a table cell opens an inline formula. Write\$14.72½in the text and\$in a cell.
Credits
- Recipe
- Ignacio Ferro
- Text
- The accounts and the passages around them from “Economy,” the first chapter of Walden; or, Life in the Woods (Boston: Ticknor and Fields, 1854) · Henry D. Thoreau · public domain
- The headnote, and the Spanish translation of the passages and the accounts · Ignacio Ferro · CC BY 4.0
- Images
- The vignette of Thoreau’s cabin from the title page of the first edition (1854), cropped and levelled to black on white · Ticknor and Fields, Boston, 1854 (engraver unknown) · public domain
- Fonts
- Alegreya (SIL OFL 1.1) · Alegreya SC (SIL OFL 1.1) · Alegreya Sans (SIL OFL 1.1)
Edit this write-up ↗ (opens in a new tab)Recipe folder on GitHub ↗ (opens in a new tab)


