Pretext Lab
31Where this lands natively
Arc 10 · Ecosystem & Future

Where this lands natively

Pretext is a 15KB library that re-implements, in JavaScript, a pair of operations the browser already knows how to do internally: measure a run of text in a given font, and break it across lines at a given width. The reason it's a userland library at all is that the browser doesn't expose those operations. Pretext's whole existence is a patch over a missing primitive.

Below, the trajectory. On the left — what Pretext does today, with canvas measureText as its only real foothold into the font engine. On the right — what the proposed FontMetrics API (CSS Houdini drafts) would give us directly: structured metrics, not a library that reconstructs them. The right panel is illustrative, a mock of where the standards conversation is going.

what we do now
Pretext (~15KB)
// The only browser-exposed primitive we have. ctx.measureText("hello") // → { width: 28.4 } (one number.) // Pretext turns that into a whole layout engine: prepare(text, font) // analyze + measure layout(h, width, lh) // pure arithmetic layoutWithLines(...) // rich line data walkLineRanges(...) // streaming geometry // 15KB of JS for what should be a primitive.
Canvas measureText is what you get from the browser. Everything else — segmentation, glue rules, bidi, CJK keep-all, emoji graphemes, soft hyphens — Pretext rebuilds in JavaScript using the canvas widths as ground truth.
what native could give us
FontMetrics API (CSS Houdini draft)
// Structured metrics, native, no library. font.getMetrics() // → { capHeight: 0.71, xHeight: 0.48, ascent: 0.82, descent: 0.22, lineGap: 0.12, unitsPerEm: 1000, } // Break a text run natively. font.shape(text, { width: 320, whiteSpace: "normal", locale: "ja-JP", }) // → { lines: [...] }
A mock — the exact shape is still in draft. But the category is settled: browsers will eventually expose structured metrics and line-break APIs. When they do, Pretext becomes a polyfill.

Mechanism

Today the browser's text engine lives entirely behind the rendering pipeline. You can ask it to paint text (and you'll see what you get) but you can't ask it what the engine decided — not the cap height of the font, not the set widths of a segment, not where it will break a line at 320 pixels. Canvas's measureText(str) is the one narrow opening. It returns { width, actualBoundingBoxAscent, actualBoundingBoxDescent, ... } for a single run. That's enough for Pretext, because all of layout reduces to "what's the width of each segment?" — once you know that, line-breaking is arithmetic.

The FontMetrics API (a CSS Houdini draft) proposes exposing what the engine already knows. Font-level metrics (capHeight, xHeight, ascent, descent) would come back as structured objects. Paired proposals in the line-breaking space would let you ask the engine itself for wrapped lines — no canvas probe, no re-implementation of Unicode break classes in JS.

When those land — and "when" is the right word, not "if" — Pretext's 15KB shrinks to whatever polyfill glue is needed to round out browser coverage. The library doesn't become obsolete so much as reclassified: from "essential custom layout engine" to "pre-standards compatibility layer."

Application

The practical takeaway is about how you spend your budget today:

References: FontMetrics Level 1 draft (CSS Houdini WG). Also note uWrap.js by @leeoniya — an ASCII-only alternative to Pretext, much simpler scope, mentioned in the HN discussion.

"The things which are impossible are not absolutely impossible, but only impossible to the degree that we do not yet know how to do them. What is hereafter standard was once the private invention of an unusual mind."

Contemplative paraphrase, after Ralph Waldo Emerson, Essays (1841)

Direct Claude

"future-proof your layout thinking" use Pretext-like primitives today; the browser will commoditize them tomorrow "skate where the puck is going" the API surface matters more than the specific library — abstract the call "ship the workaround; watch the standards" Pretext is a polyfill-in-waiting for CSS Houdini FontMetrics + line-break APIs
one interface, two implementations — the migration shape
// Write to this shape today, regardless of implementation.
// Tomorrow, swap Pretext for a native call behind the same interface.
interface Measurer {
  prepare(text: string, font: string): Handle;
  layout(h: Handle, width: number, lh: number): { height: number; lineCount: number };
}

// Today: Pretext.
import { prepare, layout } from '@chenglou/pretext';
const pretextMeasurer: Measurer = { prepare, layout };

// Future (illustrative — API not final):
//   const nativeMeasurer: Measurer = {
//     prepare: (text, font) => new CSS.FontFace(font).shape(text),
//     layout:  (h, w, lh)  => h.breakLines({ width: w, lineHeight: lh }),
//   };

// Your app talks to the interface, not the library.
export function paragraphHeight(m: Measurer, text: string, font: string, w: number, lh: number) {
  const h = m.prepare(text, font);
  return m.layout(h, w, lh).height;
}