关于 pretext
Pretext 是一个纯 JavaScript/TypeScript 文本测量与排版库,无需访问 DOM 即可计算多行文本布局。它使用 Canvas 测量字体,并提供可复用的预处理结果、行布局及多字体内联排版 API。
主要功能
- 01无需 DOM 的文本测量
- 02Canvas 字体宽度测量
- 03复用预处理结果计算高度
- 04生成行布局与行范围
- 05支持多字体内联排版
适用场景
- 01计算虚拟列表的动态行高
- 02按尺寸变化调整文本高度
- 03在 Canvas 或 SVG 中绘制换行文本
- 04排列混合字体的标签和提及
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ asharibali/ pretext-skills --skill pretext安装后在 Claude Code 中输入/pretext确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/AGENTS.mdCHANGELOG.mdDEVELOPMENT.mdREADME.mdRESEARCH.mdSECURITY.mdSTATUS.mdTODO.mdreferences/examples/balanced-text.tsbasic-height.tsbubble-chat.tscanvas-manual-layout.tsflow-around-image.tslabel-overflow-check.tsmultilingual-layout.tsnatural-width.tspre-wrap-textarea.tsresize-handler.tsrich-inline.tsscroll-anchor.tsshrink-wrap.tssvg-text-render.tstwo-column-flow.tsvirtualized-list.ts详细介绍
Pretext – 无 DOM 文本测量与布局
Pretext 是一个纯 JavaScript/TypeScript 库,可在不接触 DOM 的情况下测量和布局多行文本。它使用浏览器自带的字体引擎(通过 canvas measureText)作为基准,然后通过纯算术完成所有换行和高度计算。支持渲染到 DOM、Canvas、SVG,以及(最终)服务端。
这为何重要
基于 DOM 的文本测量(getBoundingClientRect、offsetHeight)会触发布局重排——这是浏览器中开销最大的操作之一。每次调用都会迫使引擎重新计算样式和几何信息,可能涉及整个文档。在包含 10,000 行的虚拟列表中,这意味着仅为获取行高就要重排 10,000 次。
Pretext 以两阶段方案取而代之:调用一次 prepare() 完成文本分析、分段、粘连规则和 canvas 测量(开销较大的部分,只需执行一次),然后 layout() 根据缓存的宽度通过纯数学计算高度(开销较小的部分,每次调整大小时调用)。这比 DOM 测量快 300-600 倍,并彻底消除了布局抖动。
结果是在各浏览器中都能达到像素级精确(Chrome、Safari 和 Firefox 的准确性测试均为 7680/7680),并支持所有语言,包括表情符号、混合双向文本、CJK、泰语、阿拉伯语、高棉语、缅甸语、乌尔都语等。
安装
npm install @chenglou/pretext
两种路径:选择更简单的一种
快速路径(90% 的情况——只需要高度)
import { prepare, layout } from '@chenglou/pretext'
const prepared = prepare('AGI 春天到了. بدأت الرحلة 🚀', '16px Inter')
const { height, lineCount } = layout(prepared, maxWidth, 20)
这适用于虚拟列表、动态容器、滚动锚定等所有需要知道文本在给定宽度下高度的场景。prepared 句柄可重复使用:调整大小时,只需使用新宽度再次调用 layout()。
对于类似文本区域的文本,其中空格、制表符和 \n 硬换行符都会保留:
const prepared = prepare(textareaValue, '16px Inter', { whiteSpace: 'pre-wrap' })
const { height } = layout(prepared, textareaWidth, 20)
对于不应在词语内部换行的 CJK/Hangul 文本:
const prepared = prepare(text, '16px Inter', { wordBreak: 'keep-all' })
丰富路径(需要实际的行)
当你需要自行渲染文本(Canvas、SVG、WebGL),或进行可变宽度布局(绕图像流动)时,请切换到丰富 API:
import { prepareWithSegments, layoutWithLines, walkLineRanges, layoutNextLine, layoutNextLineRange, materializeLineRange, measureLineStats, measureNaturalWidth } from '@chenglou/pretext'
layoutWithLines()— 在固定宽度下获取所有行(用于 Canvas/SVG 渲染)walkLineRanges()— 获取行范围而不构建字符串(用于收缩适配、通过二分搜索寻找最佳宽度、平衡文本布局)measureLineStats()— 无需任何内存分配即可获取行数和最宽行的宽度measureNaturalWidth()— 在宽度并非导致换行的因素时,获取最宽强制换行行的宽度layoutNextLine()— 每次布局一行,宽度可变(文本绕开障碍物流动),返回包含文本的完整行layoutNextLineRange()— 与上面相同,但不构建文本字符串;需要文本时使用materializeLineRange()
富文本行内流式布局(多字体、标签、提及)
对于混合字体、代码片段、提及、标签以及类似浏览器的边界空白折叠等富文本行内流式布局:
import { prepareRichInline, walkRichInlineLineRanges, materializeRichInlineLineRange, measureRichInlineStats } 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 rich-note", font: '500 17px Inter' },
])
walkRichInlineLineRanges(prepared, 320, range => {
const line = materializeRichInlineLineRange(prepared, range)
// each fragment keeps its source item index, text slice, gapBefore, and cursors
})
富文本行内辅助工具有意保持精简:
- 输入原始行内文本,包括边界空格
- 由调用方负责
extraWidth,用于胶囊标签的装饰部分(内边距 + 边框宽度) - 对标签和提及等原子项使用
break: 'never' - 仅支持
white-space: normal - 不是嵌套标记树,也不是通用 CSS 行内格式化引擎
API 参考
快速路径 API
prepare(text: string, font: string, options?: {
whiteSpace?: 'normal' | 'pre-wrap',
wordBreak?: 'normal' | 'keep-all'
}): PreparedText
layout(prepared: PreparedText, maxWidth: number, lineHeight: number): {
height: number,
lineCount: number
}
丰富路径 API
prepareWithSegments(text: string, font: string, options?: {
whiteSpace?: 'normal' | 'pre-wrap',
wordBreak?: 'normal' | 'keep-all'
}): PreparedTextWithSegments
layoutWithLines(prepared: PreparedTextWithSegments, maxWidth: number, lineHeight: number): {
height: number, lineCount: number, lines: LayoutLine[]
}
walkLineRanges(prepared: PreparedTextWithSegments, maxWidth: number,
onLine: (line: LayoutLineRange) => void): number
measureLineStats(prepared: PreparedTextWithSegments, maxWidth: number): {
lineCount: number, maxLineWidth: number
}
measureNaturalWidth(prepared: PreparedTextWithSegments): number
layoutNextLineRange(prepared: PreparedTextWithSegments, start: LayoutCursor, maxWidth: number): LayoutLineRange | null
layoutNextLine(prepared: PreparedTextWithSegments, start: LayoutCursor, maxWidth: number): LayoutLine | null
materializeLineRange(prepared: PreparedTextWithSegments, line: LayoutLineRange): LayoutLine
富文本行内 API(@chenglou/pretext/rich-inline)
prepareRichInline(items: RichInlineItem[]): PreparedRichInline
layoutNextRichInlineLineRange(prepared: PreparedRichInline, maxWidth: number, start?: RichInlineCursor): RichInlineLineRange | null
walkRichInlineLineRanges(prepared: PreparedRichInline, maxWidth: number,
onLine: (line: RichInlineLineRange) => void): number
materializeRichInlineLineRange(prepared: PreparedRichInline, line: RichInlineLineRange): RichInlineLine
measureRichInlineStats(prepared: PreparedRichInline, maxWidth: number): {
lineCount: number, maxLineWidth: number
}
工具 API
clearCache(): void // release internal caches used by prepare/prepareWithSegments
setLocale(locale?: string): void // retarget word segmenter for future prepare() calls; also clears cache
关键类型
type LayoutLine = {
text: string // full text of the line
width: number // measured pixel width
start: LayoutCursor // inclusive start cursor
end: LayoutCursor // exclusive end cursor
}
type LayoutLineRange = {
width: number
start: LayoutCursor
end: LayoutCursor
}
type LayoutCursor = {
segmentIndex: number // position in prepared segments
graphemeIndex: number // grapheme within that segment
}
type RichInlineItem = {
text: string // raw text including boundary spaces
font: string // canvas font shorthand
break?: 'normal' | 'never' // 'never' = atomic (chips, mentions)
extraWidth?: number // horizontal chrome width (padding + border)
}
type RichInlineFragment = {
itemIndex: number // index into original items array
text: string
gapBefore: number // collapsed boundary gap
occupiedWidth: number // text width + extraWidth
start: LayoutCursor
end: LayoutCursor
}
type RichInlineLine = {
fragments: RichInlineFragment[]
width: number
end: RichInlineCursor
}
需要注意的关键事项
- 缓存
prepared句柄。 相同文本和相同字体应复用句柄。只有在文本或字体实际发生变化时,才再次调用prepare()。调整大小时,只需再次调用layout()。 - 与 CSS 完全匹配。
font字符串必须与 CSSfont简写属性匹配(字号、字重、样式、字体系列)。lineHeight必须与 CSSline-height匹配。不匹配会导致高度错误。 - 对于需要保留空格、制表符和换行符的文本区域,使用
{ whiteSpace: 'pre-wrap' }。 制表符遵循浏览器默认样式的tab-size: 8停靠点。 - 如果不希望在单词内部断行,处理 CJK/韩文时使用
{ wordBreak: 'keep-all' }。 - 在 macOS 上避免使用
system-ui——canvas 和 DOM 可能会解析为不同字体(SF Pro Text 与 SF Pro Display),导致精度下降。请使用具名字体。 - 很少需要
clearCache()——仅当在许多不同字体之间切换并且需要释放内存时才使用。 setLocale()是可选的——默认情况下,Pretext 使用当前区域设置。如果需要指定Intl.Segmenter区域设置,请在调用prepare()前调用它。
支持的 CSS 目标
white-space: normal(默认)和pre-wrapword-break: normal(默认)和keep-alloverflow-wrap: break-word——窄宽度可能会在单词内部断行,但只会在字素边界处断开line-break: auto- 制表符遵循浏览器默认样式的
tab-size: 8
Pretext 在底层处理的内容
- 分段:文本分析、规范化,以及通过
Intl.Segmenter进行的 Unicode 感知单词/字素分段 - CJK:增补平面表意文字、兼容区块、扩展区块、禁则(禁止出现在行首/行末的标点)
- 阿拉伯语/RTL:无空格标点簇合并、标点加组合标记簇,以及富文本路径上的双向文本元数据
- Emoji:按字体自动检测并校正 canvas/DOM 宽度差异,以及 ZWJ 序列
- 软连字符:不换行时不可见,选择在此处断行时会显示末尾的
- - NBSP/NNBSP/WJ:保留为可见内容,并阻止在单词边界处换行
- ZWSP:保留为零宽度断行机会
- 类似 URL 的文本段:建模为结构化的可断行单元(路径 + 查询),避免在路径中间不当断行
- 混合文字脚本:处理混合双向文本、以 CJK 开头的无空格文本段、上下文相关的 ASCII 引号,以及数字/时间范围表达式
即用型示例
完整、可直接复制粘贴的代码请参见 references/examples/ 目录:
快速路径(高度测量):
basic-height.ts— 测量容器中文本的高度,并使用wordBreak: 'keep-all'pre-wrap-textarea.ts— 自动调整文本区域大小,并保留空白字符和制表符resize-handler.ts— 标准调整大小模式:调用一次prepare(),每次调整大小时调用layout()virtualized-list.ts— 使用缓存句柄为虚拟化列表计算可变行高scroll-anchor.ts— 在插入 DOM 前预测高度,防止布局偏移label-overflow-check.ts— 在 CI/开发时检查标签是否会换行multilingual-layout.ts— 使用setLocale()对 CJK、阿拉伯语和泰语进行区域设置感知的测量
富文本路径(手动排版):
canvas-manual-layout.ts— 使用layoutWithLines在 canvas 上渲染换行文本svg-text-render.ts— 将换行文本渲染到 SVG<tspan>元素中flow-around-image.ts— 使用layoutNextLineRange让文本绕排浮动图像two-column-flow.ts— 使用layoutNextLineRange实现连续的双栏编辑式排版shrink-wrap.ts— 使用walkLineRanges和measureLineStats查找最紧凑的容器宽度balanced-text.ts— 通过二分搜索平衡行宽,避免出现孤行natural-width.ts— 使用measureNaturalWidth调整工具提示/弹出层的大小bubble-chat.ts— 聊天气泡收缩包裹:查找最紧凑的宽度,然后渲染文本行
富文本内联(多字体):
rich-inline.ts— 使用原子化标签和提及项实现多字体内联排版
深入参考
references/README.md— 库仓库中的完整 API:安装、完整类型定义、所有函数签名、用法示例和注意事项。references/AGENTS.md— Agent 专属指南:重要源文件、实现说明、实用模式、注意事项、待解决问题,以及使用 Pretext 构建时的决策启发。references/STATUS.md— 当前准确性概览和基准快照索引(浏览器测试结果、语料库哨兵)。references/RESEARCH.md— 研究日志:所有尝试、测量结果和经验——system-ui 发现、词数总和准确性、阿拉伯语边界、软连字符、emoji 校正、CJK/泰语/高棉语/缅甸语/日语/中文语料库哨兵,以及被否决的方法。references/DEVELOPMENT.md— 开发环境设置、日常命令、打包与发布检查、浏览器准确性和基准测试命令、语料库工具,以及深度性能分析工作流。references/CHANGELOG.md— 从 0.0.0(2026-03-26)起的版本历史,以及尚未发布的变更,包括keep-all支持和富行内 API。references/TODO.md— 当前优先事项:哨兵维护、下一步引擎工作、演示方向和待解决的设计问题。references/SECURITY.md— 安全政策,以及通过 GitHub advisories 私下报告漏洞的方式。