Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

Back to skills

Pretext Text Measurement

ASecurity

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

81 stars
0 votes
0 copies
0 views
Added 9/19/2026
developmentjavascripttypescriptjavanodeapi

Works with

cursorcliapi

Security Analysis

A96/100
mediumInstalls packages at runtime which could introduce malicious dependencies

Scanned 9/19/2026

Install to Claude Code

$npx -y skills add reason-machines/trending-skills --skill pretext-text-measurement --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Pretext Text Measurement?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Pretext Text Measurement
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/reason-machines-pretext-text-measurement/badge)](https://www.skillsdirectory.com/skills/reason-machines-pretext-text-measurement)

More formats (shields.io, HTML) on the badges page.

Download Zip
Files
SKILL.md
---
name: pretext-text-measurement
description: Fast, accurate, DOM-free text measurement and layout library for JavaScript/TypeScript supporting multiline, rich-text, and variable-width layouts.
triggers:
  - measure text without DOM
  - calculate paragraph height
  - text layout without reflow
  - multiline text measurement
  - wrap text in canvas
  - pretext text library
  - layout text around image
  - shrinkwrap text width
---

# 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`.

Attribution

reason-machinesreason-machines
View sourceMore from reason-machines →
SSkills DirectorySkills Directory

Your tool, in front of Claude Code builders.

3 founder slots · $299/mo · GSC-verified traffic · sponsors can never buy grades.

See placements

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Your tool, in front of Claude Code builders.

3 founder slots · $299/mo · GSC-verified traffic · sponsors can never buy grades.

See placements

Related Skills

Browser Extension Developer

Use this skill when developing or maintaining browser extension code in the `browser/` directory, including Chrome/Firefox/Edge compatibility, content scripts, background scripts, or i18n updates.

281612 votes

Seo Optimizer

SEO optimization with keyword analysis, readability assessment, technical validation, content quality. Use for search rankings, blog posts, content audits, or encountering keyword density, readability scores, meta tags, schema markup errors.

2132 votes

Google Official Seo Guide

Official Google SEO guide covering search optimization, best practices, Search Console, crawling, indexing, and improving website search visibility based on official Google documentation

1862 votes

Tanstack Start

Build a full-stack TanStack Start app on Cloudflare Workers from scratch — SSR, file-based routing, server functions, D1+Drizzle, better-auth, Tailwind v4+shadcn/ui. Use whenever the user mentions TanStack Start, asks to scaffold a full-stack Cloudflare app with SSR, wants an SSR dashboard, or asks for a React 19 + Cloudflare Workers app with file-based routing and server functions — even if they don't name TanStack Start specifically. No template repo — Claude generates every file fresh per ...

9881 votes

Pentest

PTES-aligned adversarial security audit for backend, frontend, and mobile applications. Produces a CVSS-scored Hacker Report with verified PoCs and phased remediation.

5491 votes
View all in development →