Pretext Lab
04Font as a string
Arc 2 · Basic Measurement

Font as a string

Pretext uses the browser's own canvas font engine to measure. That means the font string you pass to prepare() is the contract between your application and Pretext's idea of width. Weight, style, size, family — if any of those drift from the CSS the browser paints with, the line breaks drift too.

Pick a face. The specimen restyles in CSS and is re-prepared against an identical font string. The line count and computed height update to match what the browser would produce.

lines · —
height · —px

A foolish consistency is the hobgoblin of little minds, adored by little statesmen and philosophers and divines. With consistency a great soul has simply nothing to do. Speak what you think now in hard words, and to-morrow speak what to-morrow thinks in hard words again, though it contradict every thing you said to-day.

Mechanism

The font string is exactly what you'd assign to canvasCtx.font: an optional weight (400, 600, …), an optional style (italic, oblique), a size (20px), and a family stack ('EB Garamond', serif). Pretext feeds that string to its measurement canvas. Every segment's width is whatever that configured canvas reports — which is also what the browser will render with.

When the font changes, the cached measurements are stale. You call prepare(text, newFont) again, get a fresh handle, and layout() it at your current width. The rule is plain: measure what you'll render. The font string on the page and the font string passed to prepare() must match in weight, style, size, and family — or the line breaks won't.

Pretext's README flags one quirk: system-ui is unsafe on macOS, because canvas and DOM can resolve to different faces. Use a named family you've loaded yourself.

Application

The unlock here is subtle but constant: you can honor the author's type choice inside a measured pipeline without losing the pipeline. Swap a display face, re-prepare, keep your layout math.

"A foolish consistency is the hobgoblin of little minds… Speak what you think now in hard words, and to-morrow speak what to-morrow thinks in hard words again."

Ralph Waldo Emerson, Self-Reliance (1841)

Direct Claude

"match the author's face" include family, weight, and style in the font string passed to prepare() "measure what you'll render" use the same font string on the element's CSS and in prepare() "swap the face without re-measuring the world" re-prepare() only the affected text; keep the layout pipeline "italic on hover, no flash" hold a second handle prepared with 'italic'; layout() it, then swap class
Next: one analyze, many fits — reuse a single handle across every width you'll ever show.
the font string is the contract
import { prepare, layout } from '@chenglou/pretext';

// One font string. Used for the CSS and for prepare() — in lock-step.
function setFont(fontStr, lineHeightRatio) {
  specimen.style.font = fontStr;

  // New face → stale measurements. Re-prepare.
  const handle = prepare(TEXT, fontStr);

  // Extract size from the font string so lineHeight stays in sync.
  const sizePx = parseFloat(fontStr.match(/(\d+(?:\.\d+)?)px/)[1]);
  const { height, lineCount } = layout(
    handle, textRegion.clientWidth, sizePx * lineHeightRatio
  );

  textRegion.style.minHeight = height + 'px';
  return { handle, height, lineCount };
}