pretext-text-measurement

Fast, accurate, DOM-free text measurement and layout library for JavaScript/TypeScript supporting multiline, rich-text, and variable-width layouts.

22 stars

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

$curl -o ~/.claude/skills/pretext-text-measurement/SKILL.md --create-dirs "https://raw.githubusercontent.com/Aradotso/trending-skills/main/skills/pretext-text-measurement/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/pretext-text-measurement/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How pretext-text-measurement Compares

Feature / Agentpretext-text-measurementStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/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

22
from Aradotso/trending-skills

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

22
from Aradotso/trending-skills

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

22
from Aradotso/trending-skills

AI가 쓴 한글 글을 사람이 쓴 것처럼 윤문해주는 Claude Code 스킬 — 번역투·관용구·구조적 AI 패턴 40+ 서브 패턴 탐지 및 수술적 수정

codesight-ai-context

22
from Aradotso/trending-skills

Universal AI context generator that compiles codebase maps, wiki knowledge bases, and MCP tools to save thousands of tokens per AI conversation.

```markdown

22
from Aradotso/trending-skills

---

zeroboot-vm-sandbox

22
from Aradotso/trending-skills

Sub-millisecond VM sandboxes for AI agents using copy-on-write KVM forking via Zeroboot

yourvpndead-vpn-detection

22
from Aradotso/trending-skills

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

22
from Aradotso/trending-skills

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

22
from Aradotso/trending-skills

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

22
from Aradotso/trending-skills

End-to-end pipeline to extract, decrypt, and visualize WeChat Mac favorites from encrypted SQLite DB into an interactive HTML report.

wterm-web-terminal

22
from Aradotso/trending-skills

Web terminal emulator with Zig/WASM core, DOM rendering, and React/vanilla JS bindings

worldmonitor-intelligence-dashboard

22
from Aradotso/trending-skills

Real-time global intelligence dashboard with AI-powered news aggregation, geopolitical monitoring, and infrastructure tracking