mcpField
P
pretext

pretext

github.com/asharibali开发13121 次安装30 周前更新
代码仓库

关于 pretext

Pretext 是一个纯 JavaScript/TypeScript 文本测量与排版库,无需访问 DOM 即可计算多行文本布局。它使用 Canvas 测量字体,并提供可复用的预处理结果、行布局及多字体内联排版 API。

主要功能

  1. 01无需 DOM 的文本测量
  2. 02Canvas 字体宽度测量
  3. 03复用预处理结果计算高度
  4. 04生成行布局与行范围
  5. 05支持多字体内联排版

适用场景

  1. 01计算虚拟列表的动态行高
  2. 02按尺寸变化调整文本高度
  3. 03在 Canvas 或 SVG 中绘制换行文本
  4. 04排列混合字体的标签和提及

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/asharibali/pretext-skills --skill pretext

安装后在 Claude Code 中输入/pretext确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
AGENTS.md
CHANGELOG.md
DEVELOPMENT.md
README.md
RESEARCH.md
SECURITY.md
STATUS.md
TODO.md
references/examples/
balanced-text.ts
basic-height.ts
bubble-chat.ts
canvas-manual-layout.ts
flow-around-image.ts
label-overflow-check.ts
multilingual-layout.ts
natural-width.ts
pre-wrap-textarea.ts
resize-handler.ts
rich-inline.ts
scroll-anchor.ts
shrink-wrap.ts
svg-text-render.ts
two-column-flow.ts
virtualized-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 字符串必须与 CSS font 简写属性匹配(字号、字重、样式、字体系列)。lineHeight 必须与 CSS line-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-wrap
  • word-break: normal(默认)和 keep-all
  • overflow-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 私下报告漏洞的方式。

评论

支持 Markdown