---
name: pretext
description: Use the @chenglou/pretext library for accurate, reflow-free multiline text measurement and layout in web projects. Use when you need to measure text height/width without triggering DOM layout reflow, implement virtualization, masonry layouts, canvas/SVG text rendering, or prevent layout shift. Supports all languages, emojis, and mixed-bidi text.
---

# Pretext — Multiline Text Measurement & Layout

**Repo:** https://github.com/chenglou/pretext  
**Demos:** https://chenglou.me/pretext/  
**Install:** `npm install @chenglou/pretext`

## What It Does

Pretext measures and lays out multiline text **without DOM reflow** (no `getBoundingClientRect`, no `offsetHeight`). It uses the browser's own font engine via Canvas `measureText` as ground truth, then does pure arithmetic for layout. This makes it:

- **3.8× faster** than DOM-based measurement approaches
- **Accurate across all languages** — Unicode, emojis, RTL/bidi, CJK, Arabic
- **Reflow-free** — `layout()` is pure arithmetic, ~0.09ms for 500-text batches
- **Multi-target** — renders to DOM, Canvas, SVG, WebGL (server-side coming)

## Core Concepts

### Two-phase architecture

1. **`prepare()`** — one-time work: normalize whitespace, segment text, measure with canvas, cache widths. ~19ms for 500-text batch.
2. **`layout()`** — cheap hot path: pure arithmetic over cached widths. ~0.09ms for same batch.

## Use Case 1: Height Measurement (Virtualization, etc.)

```ts
import { prepare, layout } from '@chenglou/pretext'

// One-time prep (cache this per text+font combo)
const prepared = prepare('AGI 春天到了. بدأت الرحلة 🚀', '16px Inter')

// Fast layout — no DOM, no reflow
const { height, lineCount } = layout(prepared, containerWidth, lineHeight)
```

**`font` format:** Same as `canvas.font` — e.g. `'16px Inter'`, `'bold 14px "Helvetica Neue"'`  
Must match your CSS `font` shorthand exactly.

### Pre-wrap (textarea-like)
```ts
const prepared = prepare(textareaValue, '16px Inter', { whiteSpace: 'pre-wrap' })
const { height } = layout(prepared, textareaWidth, 20)
```

## Use Case 2: Manual Line Layout (Canvas, SVG, WebGL)

### Get all lines at fixed width
```ts
import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext'

const prepared = prepareWithSegments('AGI 春天到了. بدأت الرحلة 🚀', '18px "Helvetica Neue"')
const { lines } = layoutWithLines(prepared, 320, 26) // 320px width, 26px line height
for (let i = 0; i < lines.length; i++) {
  ctx.fillText(lines[i].text, 0, i * 26)
}
```

### Line widths without building strings (fast speculative layout)
```ts
let maxW = 0
walkLineRanges(prepared, 320, line => { if (line.width > maxW) maxW = line.width })
// maxW = tightest container width that still fits — "shrink wrap" for multiline text
```

### Variable-width lines (text wrap around images, columns)
```ts
let cursor = { segmentIndex: 0, graphemeIndex: 0 }
let y = 0
while (true) {
  const width = y < image.bottom ? columnWidth - image.width : columnWidth
  const line = layoutNextLine(prepared, cursor, width)
  if (line === null) break
  ctx.fillText(line.text, 0, y)
  cursor = line.end
  y += 26
}
```

## Full API Reference

### Use Case 1 (height only)
| Function | Description |
|----------|-------------|
| `prepare(text, font, opts?)` | One-time prep. Returns `PreparedText` |
| `layout(prepared, maxWidth, lineHeight)` | → `{ height, lineCount }` |

### Use Case 2 (line-level control)
| Function | Description |
|----------|-------------|
| `prepareWithSegments(text, font, opts?)` | Like `prepare()` but richer output |
| `layoutWithLines(prepared, maxWidth, lineHeight)` | → `{ height, lineCount, lines[] }` |
| `walkLineRanges(prepared, maxWidth, onLine)` | Low-level, no string building |
| `layoutNextLine(prepared, cursor, maxWidth)` | Iterator — variable width per line |

### Utilities
| Function | Description |
|----------|-------------|
| `clearCache()` | Release internal font/measurement caches |
| `setLocale(locale?)` | Set locale for segmentation (also clears cache) |

## Types

```ts
type LayoutLine = {
  text: string        // 'hello world'
  width: number       // 87.5
  start: LayoutCursor
  end: LayoutCursor
}
type LayoutCursor = {
  segmentIndex: number   // segment index in prepared stream
  graphemeIndex: number  // grapheme index within segment (0 at boundaries)
}
```

## Key Use Cases in Practice

1. **Virtualization** — measure exact row heights without rendering, no guesstimates
2. **Masonry layouts** — know column heights in advance
3. **Canvas/SVG/WebGL rendering** — full line control without DOM
4. **Prevent layout shift** — know text height before it loads, anchor scroll position
5. **Overflow detection** — verify button labels don't overflow (great for AI-generated copy)
6. **Shrink-wrap text** — find tightest container width via `walkLineRanges` + binary search
7. **Balanced text** — equal line lengths via binary search on width
8. **Floated layouts** — `layoutNextLine` with per-line width changes

## Constraints

- Targets CSS defaults: `white-space: normal`, `word-break: normal`, `overflow-wrap: break-word`, `line-break: auto`
- `system-ui` font is **unsafe** for accuracy on macOS — use a named font
- Server-side rendering: coming soon (not yet supported)
- Not a full font rendering engine — covers the common web text setup

## Installation & Dev

```bash
npm install @chenglou/pretext

# Dev
git clone https://github.com/chenglou/pretext
bun install
bun start
# Open /demos in browser (no trailing slash)
```
