pretext-text-measurement
Fast, accurate, DOM-free text measurement and layout library for JavaScript/TypeScript supporting multiline, rich-text, and variable-width layouts.
Best use case
pretext-text-measurement is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Fast, accurate, DOM-free text measurement and layout library for JavaScript/TypeScript supporting multiline, rich-text, and variable-width layouts.
Teams using pretext-text-measurement should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/pretext-text-measurement/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How pretext-text-measurement Compares
| Feature / Agent | pretext-text-measurement | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Fast, accurate, DOM-free text measurement and layout library for JavaScript/TypeScript supporting multiline, rich-text, and variable-width layouts.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Pretext Text Measurement & Layout
> Skill by [ara.so](https://ara.so) — Daily 2026 Skills collection.
Pretext is a pure JavaScript/TypeScript library for fast, accurate, DOM-free multiline text measurement and layout. It avoids `getBoundingClientRect` and `offsetHeight` (which trigger expensive layout reflows) by implementing its own measurement logic using the browser's font engine as ground truth.
## Installation
```sh
npm install @chenglou/pretext
```
## Core Concepts
- **`prepare()` / `prepareWithSegments()`** — one-time analysis: normalize whitespace, segment text, measure via canvas. Cache and reuse this result.
- **`layout()` / `layoutWithLines()` etc.** — cheap hot path: pure arithmetic over cached widths. Call this on every resize, not `prepare()`.
- **Font string format** — same as `CanvasRenderingContext2D.font`, e.g. `'16px Inter'`, `'700 18px "Helvetica Neue"'`.
## Use Case 1: Measure Paragraph Height (No DOM)
```ts
import { prepare, layout } from '@chenglou/pretext'
// One-time per unique (text, font) combination
const prepared = prepare('AGI 春天到了. بدأت الرحلة 🚀', '16px Inter')
// Cheap — call on every resize
const { height, lineCount } = layout(prepared, containerWidth, 20)
// height: total pixel height; lineCount: number of wrapped lines
```
### With Pre-wrap (textarea-like)
```ts
const prepared = prepare(textareaValue, '16px Inter', { whiteSpace: 'pre-wrap' })
const { height } = layout(prepared, textareaWidth, 24)
```
### With CJK keep-all
```ts
const prepared = prepare(cjkText, '16px NotoSansCJK', { wordBreak: 'keep-all' })
const { height, lineCount } = layout(prepared, 300, 22)
```
## Use Case 2: Manual Line Layout
### Get All Lines at Fixed Width
```ts
import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext'
const prepared = prepareWithSegments('Hello world, this is Pretext!', '18px "Helvetica Neue"')
const { lines, height, lineCount } = layoutWithLines(prepared, 320, 26)
// Render to canvas
lines.forEach((line, i) => {
ctx.fillText(line.text, 0, i * 26)
})
// line shape: { text: string, width: number, start: LayoutCursor, end: LayoutCursor }
```
### Line Stats Without Building Strings
```ts
import { prepareWithSegments, measureLineStats, walkLineRanges } from '@chenglou/pretext'
const prepared = prepareWithSegments(article, '16px Inter')
// Just counts and widths — no string allocations
const { lineCount, maxLineWidth } = measureLineStats(prepared, 320)
// Walk line ranges for custom logic
let widest = 0
walkLineRanges(prepared, 320, line => {
if (line.width > widest) widest = line.width
})
// widest is now the tightest container that still fits the text (shrinkwrap!)
```
### Natural Width (No Wrap Constraint)
```ts
import { prepareWithSegments, measureNaturalWidth } from '@chenglou/pretext'
const prepared = prepareWithSegments('Short label', '14px Inter')
const naturalWidth = measureNaturalWidth(prepared)
// Width if text never wraps — useful for button sizing
```
### Variable-Width Layout (Text Around Floated Image)
```ts
import {
prepareWithSegments,
layoutNextLineRange,
materializeLineRange,
type LayoutCursor
} from '@chenglou/pretext'
const prepared = prepareWithSegments(article, '16px Inter')
let cursor: LayoutCursor = { segmentIndex: 0, graphemeIndex: 0 }
let y = 0
const lineHeight = 24
const image = { bottom: 200, width: 120 }
const columnWidth = 600
while (true) {
// Lines beside the image are narrower
const width = y < image.bottom ? columnWidth - image.width : columnWidth
const range = layoutNextLineRange(prepared, cursor, width)
if (range === null) break
const line = materializeLineRange(prepared, range)
ctx.fillText(line.text, 0, y)
cursor = range.end
y += lineHeight
}
```
### Iterator API (Fixed Width, With Text Strings)
```ts
import { prepareWithSegments, layoutNextLine, type LayoutCursor } from '@chenglou/pretext'
const prepared = prepareWithSegments(text, '16px Inter')
let cursor: LayoutCursor = { segmentIndex: 0, graphemeIndex: 0 }
let line = layoutNextLine(prepared, cursor, 400)
while (line !== null) {
console.log(line.text, line.width)
cursor = line.end
line = layoutNextLine(prepared, cursor, 400)
}
```
## Use Case 3: Rich Inline Text Flow
For mixed fonts, chips, @mentions, and inline code spans:
```ts
import {
prepareRichInline,
walkRichInlineLineRanges,
materializeRichInlineLineRange
} from '@chenglou/pretext/rich-inline'
const prepared = prepareRichInline([
{ text: 'Ship ', font: '500 17px Inter' },
{ text: '@maya', font: '700 12px Inter', break: 'never', extraWidth: 22 },
{ text: "'s feature", font: '500 17px Inter' },
{ text: 'urgent', font: '600 12px Inter', break: 'never', extraWidth: 16 },
])
walkRichInlineLineRanges(prepared, 320, range => {
const line = materializeRichInlineLineRange(prepared, range)
line.fragments.forEach(frag => {
// frag: { itemIndex, text, gapBefore, occupiedWidth, start, end }
const item = items[frag.itemIndex]
ctx.font = item.font
ctx.fillText(frag.text, x + frag.gapBefore, y)
})
})
```
### Rich Inline Stats
```ts
import { prepareRichInline, measureRichInlineStats } from '@chenglou/pretext/rich-inline'
const prepared = prepareRichInline(items)
const { lineCount, maxLineWidth } = measureRichInlineStats(prepared, containerWidth)
```
## Common Patterns
### Virtualized List Row Heights
```ts
import { prepare, layout } from '@chenglou/pretext'
// Pre-measure all items before virtualization
const rowHeights = items.map(item => {
const prepared = prepare(item.text, '14px Inter')
const { height } = layout(prepared, LIST_WIDTH, 20)
return height
})
```
### Binary Search for Balanced Text Width
```ts
import { prepareWithSegments, measureLineStats } from '@chenglou/pretext'
function findBalancedWidth(text: string, font: string, maxWidth: number): number {
const prepared = prepareWithSegments(text, font)
const { lineCount: targetLines } = measureLineStats(prepared, maxWidth)
let lo = 1, hi = maxWidth
while (hi - lo > 1) {
const mid = (lo + hi) / 2
const { lineCount } = measureLineStats(prepared, mid)
if (lineCount <= targetLines) hi = mid
else lo = mid
}
return hi
}
```
### Resize Handler Pattern
```ts
import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext'
// Prepare ONCE per text/font change
let prepared = prepareWithSegments(text, '16px Inter')
function onResize(containerWidth: number) {
// layout() is cheap — safe to call on every resize event
const { lines } = layoutWithLines(prepared, containerWidth, 24)
renderLines(lines)
}
// Only re-prepare when text or font changes
function onTextChange(newText: string) {
prepared = prepareWithSegments(newText, '16px Inter')
onResize(currentWidth)
}
```
### Prevent Layout Shift on Dynamic Content
```ts
import { prepare, layout } from '@chenglou/pretext'
async function loadAndRender(containerId: string, width: number) {
const container = document.getElementById(containerId)!
const text = await fetchText()
// Measure BEFORE inserting into DOM — no reflow needed
const prepared = prepare(text, '16px Inter')
const { height } = layout(prepared, width, 24)
// Reserve space first to prevent layout shift
container.style.height = `${height}px`
container.textContent = text
}
```
## API Quick Reference
### Use Case 1 (Height Only)
| Function | Description |
|---|---|
| `prepare(text, font, opts?)` | One-time analysis, returns `PreparedText` |
| `layout(prepared, maxWidth, lineHeight)` | Returns `{ height, lineCount }` |
### Use Case 2 (Manual Layout)
| Function | Description |
|---|---|
| `prepareWithSegments(text, font, opts?)` | One-time analysis, returns `PreparedTextWithSegments` |
| `layoutWithLines(prepared, maxWidth, lineHeight)` | Returns `{ height, lineCount, lines[] }` |
| `walkLineRanges(prepared, maxWidth, onLine)` | Calls `onLine` per line, no string allocs |
| `measureLineStats(prepared, maxWidth)` | Returns `{ lineCount, maxLineWidth }` only |
| `measureNaturalWidth(prepared)` | Width if text never wraps |
| `layoutNextLineRange(prepared, cursor, maxWidth)` | Iterator — one range at a time, variable width |
| `layoutNextLine(prepared, cursor, maxWidth)` | Iterator — one line + text at a time |
| `materializeLineRange(prepared, range)` | Range → `LayoutLine` with text string |
### Options
```ts
{
whiteSpace?: 'normal' | 'pre-wrap' // default: 'normal'
wordBreak?: 'normal' | 'keep-all' // default: 'normal'
}
```
## Troubleshooting
**Text height is wrong / doesn't match browser rendering**
- Ensure `font` string exactly matches your CSS `font` shorthand (weight, style, size, family all matter).
- Ensure `lineHeight` matches your CSS `line-height` in pixels.
- Font must be loaded before calling `prepare()` — use `document.fonts.ready` or `FontFace.load()`.
**`prepare()` is slow on every resize**
- Only call `prepare()` when text or font changes. For resizes, only call `layout()` or equivalent.
**Canvas not available (SSR / Node)**
- Server-side support is listed as "coming soon". For now, this library requires a browser environment (canvas API).
**CJK text not wrapping correctly**
- Try `{ wordBreak: 'keep-all' }` for Korean/Chinese text that should not break mid-word.
**Rich inline items breaking when they should be atomic**
- Add `break: 'never'` to the `RichInlineItem` for chips, mentions, badges.
**Getting widest line for shrinkwrap containers**
- Use `measureLineStats(prepared, maxWidth).maxLineWidth` or walk with `walkLineRanges` and track the max `line.width`.Related Skills
text-to-cad-harness
Open source harness for generating 3D CAD models from text using AI coding agents with build123d/OpenCascade, exporting STEP/STL/URDF, and previewing in a local CAD Explorer viewer.
openviking-context-database
Expert skill for using OpenViking, the open-source context database for AI Agents that manages memory, resources, and skills via a filesystem paradigm.
humanize-korean-ai-text
AI가 쓴 한글 글을 사람이 쓴 것처럼 윤문해주는 Claude Code 스킬 — 번역투·관용구·구조적 AI 패턴 40+ 서브 패턴 탐지 및 수술적 수정
codesight-ai-context
Universal AI context generator that compiles codebase maps, wiki knowledge bases, and MCP tools to save thousands of tokens per AI conversation.
```markdown
---
zeroboot-vm-sandbox
Sub-millisecond VM sandboxes for AI agents using copy-on-write KVM forking via Zeroboot
yourvpndead-vpn-detection
Android app that detects VPN/proxy servers (VLESS/xray/sing-box) via local SOCKS5 vulnerability, exposing exit IPs and server configs without root
xata-postgres-platform
Expert skill for Xata open-source cloud-native Postgres platform with copy-on-write branching, scale-to-zero, and Kubernetes deployment
x-mentor-skill-nuwa
AI-powered X (Twitter) content strategy skill that distills methodologies from 6 top creators + open-source algorithm data into actionable writing, growth, and monetization guidance.
wx-favorites-report
End-to-end pipeline to extract, decrypt, and visualize WeChat Mac favorites from encrypted SQLite DB into an interactive HTML report.
wterm-web-terminal
Web terminal emulator with Zig/WASM core, DOM rendering, and React/vanilla JS bindings
worldmonitor-intelligence-dashboard
Real-time global intelligence dashboard with AI-powered news aggregation, geopolitical monitoring, and infrastructure tracking