⚡ 纯 JavaScript / TypeScript

多行文本
测量与排版

快速、精确,支持你甚至不知道存在的各种语言。绕过 DOM 测量,零回流。可渲染到 DOM、Canvas、SVG。

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

// 一次预处理,反复排版
const prepared = prepare(
  'AGI 春天到了. بدأت الرحلة 🚀',
  '16px Inter'
)

// 纯算术,零 DOM 回流
const { height, lineCount } =
  layout(prepared, width, 20)
⚡ layout() ≈ 0.09ms / 500条
🚫 零 DOM 回流
~19ms
prepare() / 500条文本
~0.09ms
layout() / 500条文本
0
DOM 回流次数
支持语言数量

为什么选择 Pretext

绕过 getBoundingClientRectoffsetHeight,以浏览器自身的字体引擎作为基准真值。

📐

零回流文本测量

不依赖任何 DOM 测量 API。prepare() 一次预计算,layout() 纯算术复用——窗口大小变化时只需重新调用 layout(),无需重新 prepare。

🌍

全语言支持

中文、阿拉伯语、Emoji、混合双向文本……你能想到的所有语言和文字系统,并针对特定浏览器怪异行为做了处理。

🖼️

多渲染目标

渲染到 DOM、Canvas、SVG、WebGL,服务端渲染即将支持。手动排版每一行,自由控制输出方式。

📦

精确虚拟化

无需估算和缓存的精确虚拟化/遮挡剔除。已知高度让你的滚动列表如丝般顺滑。

🔧

灵活的排版 API

从简单的高度测量到逐行排版,从固定宽度到变宽排版(文字环绕浮动图片),从全量行信息到仅行宽回调——API 覆盖从简到繁的所有场景。

解锁 Web UI 的关键拼图

返回的高度信息让这些曾经棘手的问题变得简单。

精确虚拟化 / 遮挡剔除

无需估算行高、无需缓存,直接拿到精确高度用于虚拟滚动列表。

高级自定义布局

瀑布流、JS 驱动的类 flexbox 实现、无需 CSS hack 即可微调布局值。

开发阶段 AI 验证

AI 时代自动验证按钮文字不会溢出到下一行,无需打开浏览器。

防止布局偏移

新文本加载前即可知道高度,便于重新锚定滚动位置、消除 CLS。

两种使用场景

从简单的段落高度测量,到完整的手动行排版。

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

// 一次性预处理:规范化、分段、测量
const prepared = prepare(
  'AGI 春天到了. بدأت الرحلة 🚀',
  '16px Inter'
)

// 纯算术运算,无 DOM 布局和回流!
const { height, lineCount } = layout(prepared, textWidth, 20)

// pre-wrap 模式:保留空格、制表符和换行
const prepared2 = prepare(
  textareaValue, '16px Inter',
  { whiteSpace: 'pre-wrap' }
)
const { height: h2 } = layout(prepared2, textareaWidth, 20)
TypeScript
import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext'

const prepared = prepareWithSegments(
  'AGI 春天到了. بدأت الرحلة 🚀',
  '18px "Helvetica Neue"'
)

// 获取所有行信息
const { lines } = layoutWithLines(prepared, 320, 26)

// 渲染到 Canvas
for (let i = 0; i < lines.length; i++)
  ctx.fillText(lines[i].text, 0, i * 26)
TypeScript — 文字环绕浮动图片
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
}
TypeScript — 多行收缩包裹
// Web 一直缺失的功能:多行"收缩包裹"
let maxW = 0
walkLineRanges(prepared, 320, line => {
  if (line.width > maxW) maxW = line.width
})
// maxW 就是最紧凑容器宽度
// 可通过二分搜索 + walkLineRanges 找到理想宽度

完整接口一览

从简单测量到精细排版,覆盖所有场景。

场景一:测量段落高度

prepare(text, font, options?) → PreparedText

一次性文本分析 + 测量。规范化空白字符、分段、应用粘连规则、测量分段宽度。返回不透明句柄。font 格式同 canvas context,如 16px Inter。options 支持 { whiteSpace: 'pre-wrap' }

layout(prepared, maxWidth, lineHeight) → { height, lineCount }

根据最大宽度和行高计算文本高度。纯算术运算,是低开销热路径。窗口 resize 时只需重新调用 layout()。

场景二:手动行排版

prepareWithSegments(text, font, options?) → PreparedTextWithSegments

与 prepare() 相同,但返回更丰富的结构以满足手动行排版需求。

layoutWithLines(prepared, maxWidth, lineHeight) → { height, lineCount, lines }

接受固定最大宽度,返回所有行信息(text, width, start, end)。

walkLineRanges(prepared, maxWidth, onLine) → number

底层 API,每行调用回调传入行宽和游标,不构建文本字符串。适用于二分搜索理想宽度、收缩包裹、平衡排版等场景。

layoutNextLine(prepared, start, maxWidth) → LayoutLine | null

迭代器 API,每行可使用不同宽度。返回从 start 开始的一行,段落用尽返回 null。实现文字环绕等效果。

辅助函数

clearCache() → void

清除 Pretext 内部共享缓存。当应用循环使用大量不同字体或文本变体时可释放累积缓存。

setLocale(locale?) → void

为后续的 prepare() 和 prepareWithSegments() 设置语言环境。内部也会调用 clearCache()。不影响已有的预处理状态。

使用须知

Pretext 并不试图成为完整的字体渲染引擎,目前针对常见文本设置优化。