Pretext Text Measurement

作者 reason-machines2384a003145a无许可证83 个星标收录于 2026年10月8日更新于 2026年10月8日仓库3个月前更新

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

AI 生成的概览

Pretext JavaScript/TypeScript 库的参考,用于无需 DOM 的多行文本测量与布局。

功能
该技能介绍 Pretext 库的 API,用于在 JavaScript/TypeScript 中测量和布局文本而不触发 DOM 重排。内容涵盖文本与字体的一次性准备、用于获取高度和行数的低成本重复布局调用、手动逐行布局、围绕浮动图片的变宽布局,以及混合字体和原子标签的富内联文本流。还列出选项、虚拟化行高和 resize 处理等常见模式,以及故障排查说明。
适用场景
当需要在浏览器中测量段落高度、换行或计算行布局,且不想使用 getBoundingClientRect 或 offsetHeight 时使用。也适用于虚拟化列表、收缩包裹尺寸、文字环绕图片,以及提及或徽章等富内联内容。
运行要求
需要 @chenglou/pretext npm 包以及支持 canvas API 的浏览器环境;服务端支持被描述为即将推出。字体必须在准备前加载。不包含脚本,仅提供说明和代码示例。

Pretext Text Measurement & Layout

Skill by 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) combinationconst prepared = prepare('AGI 春天到了. بدأت الرحلة 🚀', '16px Inter')
// Cheap — call on every resizeconst { 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 canvaslines.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 allocationsconst { lineCount, maxLineWidth } = measureLineStats(prepared, 320)
// Walk line ranges for custom logiclet widest = 0walkLineRanges(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 = 0const lineHeight = 24const 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 virtualizationconst 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 changelet 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 changesfunction 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)

FunctionDescription
prepare(text, font, opts?)One-time analysis, returns PreparedText
layout(prepared, maxWidth, lineHeight)Returns { height, lineCount }

Use Case 2 (Manual Layout)

FunctionDescription
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.

来源与署名

来源:reason-machines/trending-skills位于skills/pretext-text-measurement提交2384a00

许可证: 无许可证

内容归原作者所有。SourceWeft 从公开仓库中收录这些内容。

举报或申请下架