Mixed fonts and sizes
One paragraph can carry more than one voice. A running body serif can make room for a pull-quote set larger and italic, then drop a token of inline code in mono, then return. The browser handles this for static HTML; the challenge comes the moment you need to know the width of each run — for wrapping, for virtualization, for animation.
Pretext prepares each run in its own font. When you compose them, widths stay honest.
Mechanism
The public API offers prepareWithSegments(text, font) — note the per-call font argument. Rich inline with multiple fonts is just this call, once per run, with each run's own font string. The handle returned carries that run's widths, measured against the canvas font engine at the correct type specs.
Composition is a userland loop. We walk the items; for each text run we ask layoutNextLine(handle, cursor, remainingWidth); the returned line width is correct for that run's font, even though the previous fragment on the same line was measured in a different one. When a run can't fit on the current line, we break; the next line starts fresh with the full width.
The arithmetic is still pure. No DOM reads, no mid-handler reflow. Every run's width, at every line, comes back in microseconds.
Application
Typography stops being locked into one spec per paragraph:
- A magazine-style pull-quote inside running prose — larger, italic, maybe coloured — with the surrounding body text wrapping around the change in height.
- Inline
<code>tokens that don't throw off the line widths — mono segments measured exactly, not approximated. - Small-caps labels and abbreviations that use their own letter-spacing without breaking the wrap calculation.
- A footnote marker set at a smaller size, tracked tight, whose advance width the engine computes correctly.
The common unlock: the type specs you want on the page are the type specs Pretext measures with. No fallback to the body font for the hard cases.
"I went to the woods because I wished to live deliberately. Not as the crowd lives, but as Whitman urged, standing at ease in nature, among the voices that speak before speech."
Henry David Thoreau, Walden (1854), with Walt Whitman, Song of Myself (1855)Direct Claude
<code> run prepared with the mono font string
"small-caps label without breaking wrap"
→
prepare the label run in its tracked-out font; width is measured with that tracking
"no drift when sizes coexist"
→
one prepareWithSegments() per type spec; share the cursor math across all of them
import { prepareWithSegments, layoutNextLine } from '@chenglou/pretext';
const BODY_FONT = "400 19px 'EB Garamond', serif";
const PULL_FONT = "italic 600 28px 'EB Garamond', serif";
const CODE_FONT = "500 15px 'JetBrains Mono', ui-monospace, monospace";
// Each run is prepared in its own font. The handles are independent.
const items = [
{ kind: 'text', className: 'frag--body',
prepared: prepareWithSegments('I went to the woods ', BODY_FONT) },
{ kind: 'text', className: 'frag--pull',
prepared: prepareWithSegments('to live deliberately', PULL_FONT) },
{ kind: 'text', className: 'frag--body',
prepared: prepareWithSegments(', and to call the ', BODY_FONT) },
{ kind: 'text', className: 'frag--code',
prepared: prepareWithSegments('layout()', CODE_FONT) },
{ kind: 'text', className: 'frag--body',
prepared: prepareWithSegments(' of my days what it is.', BODY_FONT) },
];
// Composition — walk items, emitting lines that respect every run's font.
function layoutMixed(items, maxWidth) {
const lines = [];
let i = 0, textCursor = null;
while (i < items.length) {
const fragments = [];
let remaining = maxWidth;
while (i < items.length) {
const item = items[i];
const start = textCursor ?? { segmentIndex: 0, graphemeIndex: 0 };
const line = layoutNextLine(item.prepared, start, Math.max(1, remaining));
if (line === null) { i++; textCursor = null; continue; }
fragments.push({ text: line.text, className: item.className, width: line.width });
remaining -= line.width;
if (cursorsEqual(line.end, item.endCursor)) { i++; textCursor = null; }
else { textCursor = line.end; break; }
}
if (!fragments.length) break;
lines.push({ fragments });
}
return lines;
}