Skip to content

Pretext 开源项目解读

项目信息

解决的核心痛点

  1. DOM 回流的性能诅咒 — 传统的文本测量(getBoundingClientRectoffsetHeight)触发同步布局回流。500 个文本块可导致每帧 30ms+ 的卡顿。Pretext 将昂贵工作集中在一次性 prepare() 中,layout() 变成纯算术操作(~0.0002ms/文本)。

  2. 多语言文本布局的复杂性 — CSS 文本布局涉及 CJK 逐字断行、阿拉伯语双向文本、泰语/高棉语字典分词、Emoji 宽度修正等大量细节。Pretext 用浏览器的 Canvas 字体引擎作为 ground truth,支持所有 Unicode 语言。

  3. Web 平台上缺失的多行 shrinkwrap — CSS 没有原生的多行文本宽度收缩适配 API。Pretext 提供了 walkLineRanges()measureNaturalWidth() 来精确计算。

目标用户:前端应用开发者 — 文本虚拟化、自定义布局引擎(masonry、JS 驱动的 flexbox)、聊天 UI、富文本编辑器、Canvas/SVG 渲染、开发时文本溢出验证。

1. 项目概览

1.1 项目定位与核心价值

Pretext 是一个纯 JavaScript/TypeScript 文本布局与测量库,通过跳过 DOM 测量来实现高性能多行文本排版,同时保持与浏览器行为的高度一致。

一句话定位:面向前端的、与浏览器行为高度一致的高性能文本测量与布局引擎库。

解决的核心痛点

  1. DOM 回流的性能诅咒 — 传统的文本测量(getBoundingClientRectoffsetHeight)触发同步布局回流。500 个文本块可导致每帧 30ms+ 的卡顿。Pretext 将昂贵工作集中在一次性 prepare() 中,layout() 变成纯算术操作(~0.0002ms/文本)。

  2. 多语言文本布局的复杂性 — CSS 文本布局涉及 CJK 逐字断行、阿拉伯语双向文本、泰语/高棉语字典分词、Emoji 宽度修正等大量细节。Pretext 用浏览器的 Canvas 字体引擎作为 ground truth,支持所有 Unicode 语言。

  3. Web 平台上缺失的多行 shrinkwrap — CSS 没有原生的多行文本宽度收缩适配 API。Pretext 提供了 walkLineRanges()measureNaturalWidth() 来精确计算。

目标用户:前端应用开发者 — 文本虚拟化、自定义布局引擎(masonry、JS 驱动的 flexbox)、聊天 UI、富文本编辑器、Canvas/SVG 渲染、开发时文本溢出验证。

1.2 技术栈与选型对比

技术选择方案替代方案及放弃原因
文本测量Canvas measureText()DOM 测量 → 触发同步回流
文本分词Intl.Segmenter(浏览器原生)自建分词器 → 维护成本高,覆盖面窄
运行时ESM only + Bun 工具链CJS → 不支持 tree-shaking
Bidi 处理简化 Unicode Bidi(来自 pdf.js)完整 UBA → 属于渲染层,不在布局层范围
架构模式两阶段 Prepare-Layout 分离单次测量 → 无法在 resize 时复用预计算数据
字体指定字体名system-ui → Canvas/DOM 分辨率不一致

1.3 核心技术亮点

  • 两阶段分离prepare() 一次性分析+测量,layout() 纯算术(零 DOM/Canvas/字符串调用)
  • 8 种段类型:完整模拟 CSS 空白处理、不间断胶合、软连字符、零宽断行机会等
  • Emoji 自动修正:通过 Canvas vs DOM 对比自动检测并修正 Chrome/Firefox 的 Emoji 宽度差异
  • 浏览器自适应:自动检测引擎(Chrome/Safari/Firefox)并适配不同的行适配容差和行为策略
  • 支持多语言:20+ 语言的长文本语料测试,包括阿拉伯语、希伯来语、印地语、中文、日语、韩语、泰语、高棉语、缅甸语、乌尔都语
  • 渐进式 API 层次:快速路径(仅高度)、中等路径(含行文本)、低级路径(迭代器+无字符串分配)

2. 整体架构设计

2.1 架构概述

Pretext 采用两阶段分离 + 分层模块化架构。核心设计将一个文本布局问题拆解为三个阶段:

  1. 准备阶段 (Prepare):一次性昂贵的文本分析 + Canvas 测量,缓存所有中间结果
  2. 布局阶段 (Layout):纯算术的行行走,基于缓存的段宽度计算行数/高度/行文本
  3. 富文本辅助层 (Rich Inline):在核心引擎之上提供内联富文本的边界空白折叠和原子项处理

模块之间的依赖是单向的、严格分层的:

  • analysis.ts(文本分析)是底层,被 layout.tsbidi.ts 依赖
  • measurement.ts(Canvas 测量)是底层,被 layout.ts 依赖
  • line-break.ts(行行走引擎)是中间层,被 layout.ts 依赖
  • line-text.ts(行文本构造)是辅助层,被 layout.tsrich-inline.ts 依赖
  • bidi.ts(双向文本)是可选辅助层,仅被 layout.ts 在 rich 路径中依赖
  • layout.ts(核心库 API)是顶层,聚合所有模块并暴露公共 API
  • rich-inline.ts(富文本内联)是独立的辅助模块,依赖 layout.tsline-text.ts

2.2 整体架构图

text
+====================================================================================+
|                             Pretext 公共 API 层                                    |
|                                                                                   |
|  prepare()  prepareWithSegments()  layout()  layoutWithLines()  setLocale()       |
|  walkLineRanges()  measureLineStats()  measureNaturalWidth()                      |
|  layoutNextLine()  layoutNextLineRange()  materializeLineRange()                   |
|  clearCache()                                                                      |
+===================================+===============================================+
                                    |
                  +-----------------v------------------+
                  |         layout.ts                   |
                  |   (核心聚合 + API 暴露)             |
                  |   两阶段协调、类型定义、             |
                  |   locale 管理、缓存清除              |
                  +--+--------+--------+--------+------+
                     |        |        |        |
       +-------------v-+ +---v------+ +v-------+ +v-------------+
       |  analysis.ts  | | bidi.ts  | | line-  | | line-text.ts |
       |  (文本分析)    | | (双向)   | | break  | | (行文本构造)  |
       |               | |         | | .ts    | |              |
       | 空白规范化     | | Unicode | | (行行走| | 字形分割缓存  |
       | Intl.Segmenter| | Bidi    | | 引擎)  | | 段范围→字符串 |
       | 胶合规则       | | 分类+   | |        | | 软连字符处理   |
       | CJK/禁则处理  | | 层级计算 | | 段宽度 | | WeakMap 缓存   |
       | keep-all 策略  | |         | | 累加    | |              |
       +------+--------+ +---------+ | 断行判定| +--------------+
              |                      | 行范围   |
              |                      | 输出     |
              |                      +----------+
              |
    +---------v----------+
    | measurement.ts     |
    | (Canvas 测量引擎)   |
    |                    |
    | Canvas 上下文管理   |
    | 段宽度缓存          |
    | Emoji 宽度修正      |
    | 引擎 Profile 检测   |
    | 字形级宽度预计算     |
    | 字体尺寸解析         |
    +--------------------+

分层职责说明

模块职责依赖
API 层layout.ts公共 API 暴露、两阶段协调、类型定义所有下层模块
分析层analysis.ts文本规范化、Intl.Segmenter 分词、胶合规则、CJK/keep-all 预处理
测量层measurement.tsCanvas 测量、段宽度缓存、Emoji 修正、引擎 Profile 检测analysis.ts(isCJK)
行走层line-break.ts段宽度累加、断行判定、行范围输出(4 种模式)analysis.ts(类型), measurement.ts(Profile)
文本层line-text.ts字形缓存、段范围→字符串拼接、软连字符可视化analysis.ts(类型), layout.ts(类型)
Bidi 层bidi.tsUnicode Bidi 分类、嵌入层级计算无(纯数据驱动)
富文本层rich-inline.ts跨项空白折叠、原子项处理、行游标状态机layout.ts, line-text.ts

2.3 目录结构图

shell
pretext/
├── src/                         # 【核心基建】核心库源码 — 发布为 @chenglou/pretext
   ├── layout.ts                # 【核心基建】公共 API 聚合层 — prepare/layout 系列 20+ 导出函数
   ├── analysis.ts              # 【核心基建】文本分析引擎 — 空白规范化/Intl.Segmenter 分词/胶合规则/禁则/keep-all
   ├── measurement.ts           # 【核心基建】Canvas 测量引擎 — 段宽度缓存/Emoji 修正/引擎 Profile 检测/字形级宽度
   ├── line-break.ts            # 【核心基建】行行走引擎 — 段宽度累加/断行判定/行范围输出,四种输出模式
   ├── line-text.ts             # 【核心基建】行文本构造器 — 字形级段拆分缓存/段范围→字符串拼接/软连字符可视化
   ├── bidi.ts                  # 【核心基建】双向文本元数据 — Unicode Bidi 分类/嵌入层级计算,仅 rich 路径使用
   ├── rich-inline.ts           # 【核心基建】富文本内联布局 — 跨项空白折叠/原子 chip/extraWidth,独立 export 路径
   ├── test-data.ts             # 【工具集】测试数据共享 — 精度检查和基准面复用同一语料
   ├── layout.test.ts           # 【质量保证】小型持久不变测试 — Bun Test 套件
   ├── text-modules.d.ts        # 【配置】文本模块类型声明 — .txt/.html 导入类型
   └── generated/               # 【工具集】自动生成数据
       └── bidi-data.ts         # 【工具集】Unicode 17.0 Bidi 分类范围 — 由 generate:bidi-data 生成
├── pages/                       # 【UI 视图】浏览器端页面 — 开发服务器 + 精度验证 + Demo
   ├── accuracy.html            # 【UI 视图】精度检查页 — 多浏览器多宽度行级对比
   ├── accuracy.ts              # 【质量保证】精度检查逻辑 — 浏览器 sweep + 逐行诊断
   ├── benchmark.html           # 【UI 视图】基准面页 — 性能吞吐量对比
   ├── benchmark.ts             # 【质量保证】基准面逻辑 — Chrome/Safari 性能快照
   ├── corpus.html              # 【UI 视图】语料诊断页 — 长文本断行对比
   ├── corpus.ts                # 【质量保证】语料诊断逻辑 — 段级偏移计算 + 行级对比
   ├── probe.html               # 【UI 视图】探针诊断页 — 单宽度精确断行
   ├── probe.ts                 # 【质量保证】探针诊断逻辑 — 细粒度断行追踪
   ├── diagnostic-utils.ts      # 【工具集】字形安全诊断工具 — Range/span 提取器
   ├── report-utils.ts          # 【工具集】报告传输工具 — hash 通道 + POST 侧通道
   ├── emoji-test.html          # 【UI 视图】Emoji 测试页
   ├── justification-comparison.html  # 【UI 视图】对齐比较页
   ├── assets/                  # 【UI 视图】静态资源
   ├── claude-symbol.svg    # 【UI 视图】Claude 符号
   └── openai-symbol.svg    # 【UI 视图】OpenAI 符号
   └── demos/                   # 【UI 视图】功能 Demo 合集 — GitHub Pages 站点
       ├── index.html           # 【UI 视图】Demo 索引页 — 站点根页面
       ├── bubbles.html         # 【UI 视图】气泡排版 Demo — shrinkwrap 动态布局
       ├── bubbles.ts           # 【UI 视图】气泡排版逻辑 — walkLineRanges 狗粮
       ├── bubbles-shared.ts    # 【工具集】气泡共享逻辑 — cell/glyph 类型
       ├── dynamic-layout.html  # 【UI 视图】动态布局 Demo — 双栏浮动绕排
       ├── dynamic-layout.ts    # 【UI 视图】动态布局逻辑 — layoutNextLineRange 狗粮
       ├── dynamic-layout-text.ts # 【工具集】动态布局文本数据
       ├── editorial-engine.html  # 【UI 视图】编辑引擎 Demo — 行级渲染引擎
       ├── editorial-engine.ts  # 【UI 视图】编辑引擎逻辑 — 光学对齐/断行优化
       ├── markdown-chat.html   # 【UI 视图】Markdown 聊天 Demo — 虚拟化聊天
       ├── markdown-chat.ts     # 【UI 视图】Markdown 聊天逻辑 — rich-inline + pre-wrap 狗粮
       ├── markdown-chat.model.ts # 【UI 视图】聊天状态模型 — 消息/渲染状态机
       ├── markdown-chat.data.ts  # 【工具集】聊天测试数据
       ├── rich-note.html       # 【UI 视图】富文本笔记 Demo — rich-inline 狗粮
       ├── rich-note.ts         # 【UI 视图】富文本笔记逻辑 — 内联富文本渲染
       ├── rich-note.model.ts   # 【UI 视图】笔记状态模型
       ├── accordion.html       # 【UI 视图】手风琴 Demo
       ├── accordion.ts         # 【UI 视图】手风琴逻辑
       ├── justification-comparison.html  # 【UI 视图】对齐比较 Demo
       ├── justification-comparison.ts    # 【UI 视图】对齐比较逻辑
       ├── justification-comparison.model.ts  # 【UI 视图】对齐比较模型
       ├── justification-comparison.data.ts   # 【工具集】对齐比较数据
       ├── justification-comparison.ui.ts     # 【UI 视图】对齐比较 UI
       ├── variable-typographic-ascii.html  # 【UI 视图】可变排版 ASCII Demo
       ├── variable-typographic-ascii.ts    # 【UI 视图】可变排版 ASCII 逻辑
       ├── wrap-geometry.ts      # 【工具集】换行几何计算
       ├── svg.d.ts              # 【配置】SVG 类型声明
       └── masonry/              # 【UI 视图】瀑布流 Demo
           ├── index.html        # 【UI 视图】瀑布流 HTML
           ├── index.ts          # 【UI 视图】瀑布流逻辑 — 动态 masonry
           └── shower-thoughts.json  # 【工具集】瀑布流数据
├── scripts/                      # 【工具集】自动化脚本 — 构建/检查/基准面/语料
   ├── accuracy-check.ts        # 【质量保证】精度检查 — Chrome 浏览器 sweep
   ├── benchmark-check.ts       # 【质量保证】基准面检查 — Chrome 性能快照
   ├── browser-automation.ts    # 【核心基建】浏览器自动化 — CDP 连接/页面管理/报告通道
   ├── report-server.ts         # 【工具集】报告 HTTP 端点 — POST 侧通道接收
   ├── corpus-check.ts          # 【质量保证】语料诊断 — 单语料单宽度
   ├── corpus-sweep.ts          # 【质量保证】语料扫频 — step=10 宽度遍历
   ├── corpus-status.ts         # 【工具集】语料状态 — 生成 corpora/dashboard.json
   ├── corpus-font-matrix.ts    # 【工具集】字体矩阵 — 多字体语料对比
   ├── corpus-taxonomy.ts       # 【工具集】差异分类 — mismatch 语义桶
   ├── pre-wrap-check.ts        # 【质量保证】pre-wrap 模式检查
   ├── keep-all-check.ts        # 【质量保证】keep-all 模式检查
   ├── symbol-check.ts          # 【质量保证】符号断行检查
   ├── letter-spacing-check.ts  # 【质量保证】letter-spacing 检查
   ├── probe-check.ts           # 【质量保证】探针诊断 — 断行追踪
   ├── build-demo-site.ts       # 【工具集】构建静态 Demo 站点 → site/
   ├── package-smoke-test.ts    # 【质量保证】包冒烟测试 — tarball JS/TS 消费端验证
   ├── generate-bidi-data.ts    # 【工具集】生成 Bidi 数据 — Unicode 17.0
   ├── status-dashboard.ts      # 【工具集】状态面板生成 → status/dashboard.json
   └── unicode/                 # 【工具集】Unicode 元数据
       └── DerivedBidiClass-17.0.0.txt  # 【工具集】Unicode Bidi 分类原始数据
├── corpora/                      # 【工具集】多语言长文本语料 — 18 种语言 20+ 语料
   ├── ar-al-bukhala.txt        # 【质量保证】阿拉伯语 — 古典文学
   ├── ar-risalat-al-ghufran-part-1.txt  # 【质量保证】阿拉伯语 — 古典文学
   ├── en-gatsby-opening.txt    # 【质量保证】英语 — 现代文学
   ├── he-masaot-binyamin-metudela.txt  # 【质量保证】希伯来语 — 历史文本
   ├── hi-eidgah.txt            # 【质量保证】印地语 — 现代文学
   ├── ja-kumo-no-ito.txt       # 【质量保证】日语 — 芥川龙之介《蜘蛛の糸》
   ├── ja-rashomon.txt          # 【质量保证】日语 — 芥川龙之介《羅生門》
   ├── km-prachum-reuang-preng-khmer-volume-7-stories-1-10.txt  # 【质量保证】高棉语
   ├── ko-sonagi.txt            # 【质量保证】韩语 — 现代文学
   ├── ko-unsu-joh-eun-nal.txt  # 【质量保证】韩语 — 现代文学
   ├── my-bad-deeds-return-to-you-teacher.txt  # 【质量保证】缅甸语
   ├── my-cunning-heron-teacher.txt  # 【质量保证】缅甸语
   ├── th-nithan-vetal-story-1.txt  # 【质量保证】泰语
   ├── th-nithan-vetal-story-7.txt  # 【质量保证】泰语
   ├── ur-chughd.txt            # 【质量保证】乌尔都语 — Nastaliq/Naskh
   ├── zh-guxiang.txt           # 【质量保证】中文 — 鲁迅《故鄉》
   ├── zh-zhufu.txt             # 【质量保证】中文 — 鲁迅《祝福》
   ├── mixed-app-text.txt       # 【质量保证】混合应用文本 — URL/emoji/符号/软连字符
   ├── chrome-step10.json       # 【工具集】Chrome step=10 语料扫频快照
   ├── safari-step10.json       # 【工具集】Safari step=10 语料扫频快照
   ├── dashboard.json           # 【工具集】语料面板 — 机读状态
   ├── sources.json             # 【配置】语料来源元数据
   ├── README.md                # 【配置】语料说明
   ├── STATUS.md                # 【配置】语料状态指针
   └── TAXONOMY.md              # 【配置】差异分类词汇 — mismatch 语义桶定义
├── accuracy/                     # 【工具集】浏览器精度快照 — 三浏览器数据
   ├── chrome.json              # 【工具集】Chrome 精度数据
   ├── firefox.json             # 【工具集】Firefox 精度数据
   ├── safari.json              # 【工具集】Safari 精度数据
   └── letter-spacing.json      # 【工具集】letter-spacing 精度数据
├── benchmarks/                   # 【工具集】性能基准面快照
   ├── chrome.json              # 【工具集】Chrome 基准面
   └── safari.json              # 【工具集】Safari 基准面
├── status/                       # 【工具集】主状态面板
   └── dashboard.json           # 【工具集】聚合精度 + 基准面状态
├── shared/                       # 【工具集】全局共享
   └── navigation-state.ts      # 【工具集】页面导航状态管理
├── research-data/                # 【工具集】研究数据
   └── system-ui-size-scan.json  # 【工具集】system-ui 尺寸扫描 — font resolution mismatch 证据
├── .github/                      # 【配置】CI/CD
   └── workflows/
       └── pages.yml            # 【配置】GitHub Pages 自动部署
├── .cursor/                      # 【配置】编辑器
   └── rules/
       └── use-bun-instead-of-node-vite-npm-pnpm.mdc  # 【配置】Cursor 规则 — 强制使用 Bun
├── package.json                  # 【配置】NPM 包元数据 — @chenglou/pretext v0.0.7
├── tsconfig.json                 # 【配置】TS 项目配置 — 开发模式 (noEmit)
├── tsconfig.build.json           # 【配置】TS 构建配置 — 发布模式 (emit dist/)
├── oxlintrc.json                 # 【配置】Oxlint 规则
├── knip.config.ts                # 【配置】Knip 死代码扫描
├── bun.lock                      # 【配置】Bun 依赖锁
├── README.md                     # 【配置】项目说明 — 公共 API 参考
├── DEVELOPMENT.md                # 【配置】开发指南 — 命令面/快照/分析流程
├── TODO.md                       # 【配置】优先级 — 当前和下一步
├── CHANGELOG.md                  # 【配置】变更日志 — 版本历史
├── STATUS.md                     # 【配置】状态面板指针
├── RESEARCH.md                   # 【配置】研究日志 — 所有实验/发现/结论
├── SECURITY.md                   # 【配置】安全策略
├── AGENTS.md                     # 【配置】Claude Code 项目指令
├── thoughts.md                   # 【配置】设计思维记录
├── LICENSE                       # 【配置】MIT 许可证
└── .gitignore                    # 【配置】Git 忽略规则

3. 模块依赖与调用关系

3.1 全局入口与核心路由

逻辑说明:用户通过 prepare()prepareWithSegments() 进入系统。这两个函数内部协调 analysis.ts(文本分析)、measurement.ts(Canvas 测量)、和可选的 bidi.ts(Bidi 元数据)。返回的不透明句柄(PreparedTextPreparedTextWithSegments)包含所有 layout 热路径需要的预计算数据。后续的 layout()layoutWithLines()walkLineRanges() 等 API 接收准备好的句柄,委托给 line-break.ts 做纯算术的行行走,需要时调用 line-text.ts 构造文本字符串。

  • 调用拓扑 (plainText)
text
用户代码
  |
  v
prepare(text, font, options?)
  |
  +---> analyzeText(text, options)         [analysis.ts]
  |     +---> Intl.Segmenter (word)        词级分段
  |     +---> Intl.Segmenter (grapheme)    字形级分段
  |     +---> normalizeWhitespaceNormal()  空白规范化
  |     +---> CJK/禁则/keep-all 预处理
  |     +---> 返回 TextAnalysis
  |
  +---> getSegmentMetrics() × N            [measurement.ts]
  |     +---> canvas.measureText(segment)  各段宽度
  |     +---> Emoji 修正                   (如需要)
  |     +---> 缓存到 Map<font, Map<segment, metrics>>
  |
  +---> computeSegmentLevels()             [bidi.ts] (仅 rich 路径)
  |     +---> Unicode Bidi 分类
  |     +---> 嵌入层级计算
  |     +---> 映射到段
  |
  +---> 返回 PreparedText / PreparedTextWithSegments

layout(prepared, maxWidth, lineHeight)
  |
  +---> countPreparedLines()               [line-break.ts]
  |     +---> 宽度累加
  |     +---> 断行判定 (epsilon 容差)
  |     +---> 返回 { lineCount, height }
  |
  +---> 返回 LayoutResult

layoutWithLines(prepared, maxWidth, lineHeight)
  |
  +---> walkPreparedLinesRaw()             [line-break.ts]
  |     +---> 逐行宽度累加
  |     +---> 每行触发回调: (start, end, width)
  |
  +---> buildLineTextFromRange() × N       [line-text.ts]
  |     +---> Intl.Segmenter (grapheme)    按需字形拆分
  |     +---> 段文本拼接
  |     +---> 软连字符可见化
  |
  +---> 返回 LayoutLinesResult { lines: LayoutLine[] }

2.2 核心业务实体与关联

实体定义

  • PreparedText:不透明句柄,包含所有布局热路径需要的预计算数据(段宽度、段类型、字形宽度数组等)。快速路径使用,不暴露内部结构。
  • PreparedTextWithSegments:富文本句柄,在 PreparedText 基础上额外暴露段文本和 Bidi 元数据,用于手动行布局。
  • LayoutLine / LayoutLineRange:一行文本的表示。LayoutLine 包含实际文本字符串,LayoutLineRange 仅包含游标范围(更轻量)。
  • LayoutCursor:段/字形双级游标,用于在准备好的文本中精确定位。
  • SegmentMetrics:单个段的 Canvas 测量结果,包含宽度、是否含 CJK、Emoji 计数、字形级宽度数组。
  • TextAnalysis:文本分析产物,包含规范化文本、段数组、段类型数组、以及预计算的分块信息。

实体引用拓扑 (plainText)

text
[PrepareOptions]
      |
      v
[TextAnalysis] 1 ---> 1 [normalized: string]
      |                  [segments: string[]]
      |                  [kinds: SegmentBreakKind[]]
      |                  [chunks: AnalysisChunk[]]
      |
      v
[PreparedText] 1 ---> 1 [PreparedCore]
      |                  [widths: number[]]
      |                  [kinds: SegmentBreakKind[]]
      |                  [breakableFitAdvances: number[][]]
      |                  [letterSpacing: number]
      |                  [segLevels: Int8Array] (仅 rich)
      |
      +--- PreparedTextWithSegments (扩展)
      |     [segments: string[]]
      |     [starts: number[]]
      |
      v
[layout()] ---> [LayoutResult { height, lineCount }]
[layoutWithLines()] ---> [LayoutLinesResult { height, lineCount, lines: LayoutLine[] }]
[walkLineRanges()] ---> N × [LayoutLineRange { width, start, end }]
      |
      v
[layoutNextLineRange()] + [materializeLineRange()]
      |                           |
      v                           v
[LayoutLineRange] -------> [LayoutLine { text, width, start, end }]
{cursor: LayoutCursor}    {cursor: LayoutCursor}

4. 核心模块详解

模块一:文本分析引擎 (analysis.ts)

  • 设计说明:文本分析是 prepare() 的第一步,负责将原始文本转化为结构化的段数组。核心流程是:

    1. 空白规范化(normalizeWhitespaceNormal):white-space: normal 下将连续的空白折叠为单个空格,pre-wrap 下保留空格和硬换行
    2. 词级分段(Intl.Segmenter,granularity: 'word'):按语言规则将文本切分为"词"和"非词"段
    3. 标点胶合:将跟随的标点合并到前一个词段(如 "hello." 合并为一个段)
    4. CJK 字形级再分段:将 CJK 词段拆分为单字,然后在禁则规则下重新合并(行首/行尾禁则字符)
    5. keep-all 策略:CJK/Hangul 文本中禁止词内断行
    6. 特殊段处理:软连字符(­)、零宽空格()、不间断空格( )等
    7. 输出结构化的 TextAnalysis,以并行数组方式存储段文本、段类型、起始位置
  • 内部结构图 (plainText)

text
+---------------------------+     +---------------------------+
| normalizeWhitespaceNormal |     | getWhiteSpaceProfile       |
| (空白字符规范化)           |     | (模式配置解析)              |
+-----------+---------------+     +---------------------------+
            |
            v
+---------------------------+
| Intl.Segmenter (word)     |
| (浏览器原生的词级分词)     |
+-----------+---------------+
            |
            v
+-----------+---------------+
| mergePunctuationIntoWords |
| (标点→前词胶合)            |
+-----------+---------------+
            |
            v
+-----------+---------------+
| splitCJKGraphemes +       |
| applyKinsoku              |  <--- kinsokuStart / kinsokuEnd
| (CJK 字形拆分 + 禁则合并)  |
+-----------+---------------+
            |
            v
+-----------+---------------+
| applyKeepAllPreprocessing |  <--- canContinueKeepAllTextRun
| (keep-all 断行策略)        |
+-----------+---------------+
            |
            v
+-----------+---------------+
| classifySegmentKinds      |
| (段类型分类: text/space/   |
|  glue/soft-hyphen 等)     |
+-----------+---------------+
            |
            v
+---------------------------+
| TextAnalysis              |
| { normalized, segments[], |
|   kinds[], starts[],      |
|   chunks[] }              |
+---------------------------+

模块二:Canvas 测量引擎 (measurement.ts)

  • 设计说明:Canvas 测量是 prepare() 的第二步,负责测量每个段的精确宽度。核心设计要点:

    1. 延迟初始化:Canvas 上下文在首次测量时才创建(OffscreenCanvas 优先,回退到 DOM Canvas)
    2. 两级缓存Map<font, Map<segment, SegmentMetrics>>,跨文本共享,可通过 clearCache() 重置
    3. Emoji 修正:Chrome/Firefox 在字号 < 24px 时 Canvas 测量的 Emoji 宽度比 DOM 宽。修正量通过一次 DOM 比较读取自动检测。Safari 的 Canvas 和 DOM 一致,修正量为 0
    4. 字形级宽度预计算:对于可断行的长段(如 URL),预先测量每个字形的累积宽度(breakableFitAdvances),供行行走引擎使用
    5. 引擎 Profile 检测:自动检测当前浏览器的断行特性(行适配容差、CJK 引号后处理等),使算法适配 Chrome/Safari/Firefox 的差异
  • 内部结构图 (plainText)

text
+---------------------------+
| getMeasureContext()        |
| (Canvas 2D 上下文单例)     |
+-----------+---------------+
            |
            v
+---------------------------+     +---------------------------+
| getSegmentMetrics()        | --> | segmentMetricCaches        |
| (段宽度 + 字形宽度测量)     |     | Map<font, Map<seg, metrics>|
+-----------+---------------+     +---------------------------+
            |
            +---> canvas.measureText(seg).width
            +---> 字形级 measureText(prefix) 累加 (大段用 pair-context)
            +---> Emoji 修正 (如需要)
            |
            v
+-----------+---------------+
| getEngineProfile()         |     缓存到 cachedEngineProfile
| (浏览器引擎特征检测)        |
| - lineFitEpsilon           |     检测 Chrome(0.005) vs Safari(1/64)
| - preferPrefixWidths       |     Safari 前缀宽度策略
| - preferEarlySoftHyphen    |     Chrome 软连字符策略
| - carryCJKAfterClosingQuote|
+-----------+---------------+
            |
            v
+---------------------------+
| getCorrectedSegmentWidth   |
| (应用 Emoji 修正的段宽度)   |
+---------------------------+

模块三:行行走引擎 (line-break.ts)

  • 设计说明:行行走是布局的热路径核心。它接收预计算的段数据(宽度、类型、字形级宽度数组),通过纯算术方式模拟浏览器的断行行为。核心算法:

    1. 快速路径:对于纯普通文本(无特殊段类型、无 letter-spacing),使用简化的老式行走器
    2. 通用路径:逐段累加宽度,在宽度超过 maxWidth 时回溯找到合法的断点
    3. 断点规则space 段消费后断行、soft-hyphen 处可选择断行、glue(不间断)段不可断、zero-width-break 处零宽断行、tab 段跳跃到下一个 tab stop、hard-break 强制断行
    4. Overflow 处理:当单个字形超过行宽时,使用预计算的字形级 breakableFitAdvances 在字形边界做紧急断行
    5. 行尾字符处理letter-spacing 在行尾正确扣除最后一个字形的额外间距
    6. 四种输出模式countPreparedLines(仅计数)、walkPreparedLinesRaw(行范围回调)、stepPreparedLineGeometry(单步迭代)、measurePreparedLineGeometry(几何测量)
  • 内部结构图 (plainText)

text
+---------------------------+     +---------------------------+
| PreparedLineBreakData      |     | getEngineProfile()         |
| (预编译的段级数据)          |     | (引擎特征,决定容差/策略)   |
+-----------+---------------+     +---------------------------+
            |
            v
+---------------------------+
| 行行走主循环                |
| (逐段累加宽度)              |
+-----------+---------------+
            |
            +-- 宽度 < maxWidth?  --> 继续累加
            +-- 宽度 > maxWidth?  --> 回溯找断点
            |     +-- space/zero-width-break: 在段前断行
            |     +-- soft-hyphen: 可选择在段前/段内断行
            |     +-- 普通文本: 在段内字形边界断行
            |     +-- glue (不间断): 强制推进 (overflow)
            |     +-- hard-break: 强制在段后断行
            |
            v
+-----------+---------------+
| 四种输出模式                |
| - countPreparedLines       | --> { lineCount }
| - walkPreparedLinesRaw     | --> callback(每行范围)
| - stepPreparedLineGeometry | --> 单行范围 (迭代器模式)
| - measurePreparedLineGeometry | --> { lineCount, maxWidth }
+---------------------------+

模块四:核心聚合层 (layout.ts)

  • 设计说明layout.ts 是库的公共 API 提供者。它本身不实现核心算法,而是协调 analysis.tsmeasurement.tsbidi.tsline-break.tsline-text.ts 五个模块。核心设计要点:

    1. prepare() / prepareWithSegments() 的内部流程被拆分为 analyzeText()(文本分析阶段)和测量阶段,保持接缝清晰
    2. layout() 是 resize 热路径:直接调用 countPreparedLines(),无 DOM 读、无 Canvas 调用、无字符串操作
    3. layoutWithLines() 先遍历行范围,再调用 buildLineTextFromRange() 逐行构建文本
    4. walkLineRanges()measureLineStats()** 不构建字符串,避免分配,适合 shrinkwrap 计算
    5. layoutNextLineRange() / materializeLineRange() 提供迭代器式 API,支持可变行宽的手工布局
    6. Locale 管理setLocale() 重置 Word Segmenter 和所有缓存
  • 内部结构图 (plainText)

text
+===========================================================+
|                      layout.ts                             |
|                                                           |
|  prepare(text, font, opts?)                               |
|    +-- analyzeText(text, opts)           [analysis.ts]     |
|    +-- for each segment:                                   |
|    |     getSegmentMetrics(seg, cache)   [measurement.ts]  |
|    +-- computeSegmentLevels()            [bidi.ts] (rich)  |
|    +-- return PreparedText / PreparedTextWithSegments      |
|                                                           |
|  layout(prepared, w, lh)                                  |
|    +-- countPreparedLines(prepared, w)  [line-break.ts]    |
|    +-- return { height: lc * lh, lineCount }              |
|                                                           |
|  layoutWithLines(prepared, w, lh)                         |
|    +-- walkPreparedLinesRaw(prepared, w, cb)              |
|    +-- for each emitted line:                             |
|    |     buildLineTextFromRange(...)     [line-text.ts]    |
|    +-- return { height, lineCount, lines }                |
+===========================================================+

模块五:富文本内联辅助 (rich-inline.ts)

  • 设计说明rich-inline.ts 是一个独立的辅助模块,提供窄范围的内联富文本布局。它不处理块级布局、嵌套标记树、或通用 CSS 内联格式化。核心功能:

    1. 跨项空白折叠:模拟浏览器的 white-space: normal 空白折叠行为(相邻项之间的边界空白折叠)
    2. 原子项break: 'never' 的项(如 chip、mention)不会被拆分到多行
    3. 额外宽度extraWidth 允许调用者添加 padding/border 等额外水平空间
    4. 行游标:使用 RichInlineCursor(包含 itemIndexsegmentIndexgraphemeIndex)精确定位
  • 内部结构图 (plainText)

text
+---------------------------+
| prepareRichInline(items[]) |
| - 逐项调用 prepareWithSegments()
| - 计算自然宽度 (带 extraWidth)
| - 返回 PreparedRichInline
+-----------+---------------+
            |
            v
+---------------------------+
| walkRichInlineLineRanges() |
| - 逐行遍历
| - 跨项空白折叠
| - 原子项保持完整
| - 每行回调 RichInlineLineRange
+-----------+---------------+
            |
            v
+---------------------------+
| materializeRichInlineLineRange()
| - 将行范围转为完整文本
| - 含 gapBefore, occupiedWidth
+---------------------------+

5. 关键数据流程

场景一:快速高度测量 (prepare + layout)

  • 场景说明:用户调用 prepare() 预计算文本数据,然后多次调用 layout() 在不同宽度下计算高度。这是 Pretext 最高频的使用模式。

  • 流转时序图 (Mermaid)

场景二:手工逐行布局 + 可变行宽 (layoutNextLineRange + materializeLineRange)

  • 场景说明:用户需要将文本精确分配到各行,每行宽度可能不同(例如浮动图片绕排)。使用迭代器式 API 逐行提取。

  • 流转时序图 (Mermaid)

场景三:多行 Shrinkwrap (walkLineRanges)

  • 场景说明:用户需要在不知道宽度的情况下,找到刚好容纳所有文本的最小宽度(shrinkwrap)。通过 walkLineRanges() 不构建文本字符串地遍历所有行,统计最宽行。

  • 流转时序图 (Mermaid)

6. 接口与契约规范

6.1 核心类型定义

typescript
/**
 * ============================================
 * analysis.ts - 文本分析引擎契约
 * ============================================
 */

/** 空白处理模式 */
export type WhiteSpaceMode = 'normal' | 'pre-wrap'

/** 断词模式 */
export type WordBreakMode = 'normal' | 'keep-all'

/** 段断行行为分类(8 种) */
export type SegmentBreakKind =
  | 'text'              // 普通文本段(可断行)
  | 'space'             // 可折叠空格(white-space: normal 下)
  | 'preserved-space'   // 保留空格(pre-wrap 下)
  | 'tab'               // 制表符
  | 'glue'              // 不间断胶合(NBSP/NNBSP/WJ 类)
  | 'zero-width-break'  // 零宽断行机会(ZWSP)
  | 'soft-hyphen'       // 软连字符(可选断行点)
  | 'hard-break'        // 硬换行(\n)

/** 文本分析产物 */
export type TextAnalysis = {
  normalized: string           // 规范化后的文本
  chunks: AnalysisChunk[]      // 硬换行分块信息
  len: number                  // 段数量
  texts: string[]              // 各段文本
  isWordLike: boolean[]        // 各段是否为词类型
  kinds: SegmentBreakKind[]    // 各段断行类型
  starts: number[]             // 各段在规范化文本中的起始偏移
}

/**
 * ============================================
 * measurement.ts - Canvas 测量引擎契约
 * ============================================
 */

/** 段的 Canvas 测量结果 */
export type SegmentMetrics = {
  width: number                        // 段宽(含 Emoji 修正)
  containsCJK: boolean                 // 是否含 CJK 字符
  emojiCount?: number                  // Emoji 数量
  breakableFitMode?: BreakableFitMode  // 字形级宽度计算策略
  breakableFitAdvances?: number[] | null // 字形级累积宽度
}

/** 浏览器引擎特征 */
export type EngineProfile = {
  lineFitEpsilon: number               // 行适配容差
  carryCJKAfterClosingQuote: boolean   // CJK 引号后处理
  breakKeepAllAfterPunctuation: boolean // keep-all 下标点后断行
  preferPrefixWidthsForBreakableRuns: boolean // Safari 前缀宽度策略
  preferEarlySoftHyphenBreak: boolean  // Chrome 软连字符策略
}

/** 字形级宽度计算策略 */
export type BreakableFitMode = 'sum-graphemes' | 'segment-prefixes' | 'pair-context'

/**
 * ============================================
 * layout.ts - 核心公共 API 契约
 * ============================================
 */

/** 准备选项 */
export type PrepareOptions = {
  whiteSpace?: WhiteSpaceMode
  wordBreak?: WordBreakMode
  letterSpacing?: number  // CSS px 值
}

/** 布局游标(双级定位) */
export type LayoutCursor = {
  segmentIndex: number   // 段索引
  graphemeIndex: number  // 段内字形索引
}

/** 布局结果(快速路径) */
export type LayoutResult = {
  height: number
  lineCount: number
}

/** 行统计(无字符串分配) */
export type LineStats = {
  lineCount: number
  maxLineWidth: number
}

/** 布局行(含文本内容) */
export type LayoutLine = {
  text: string
  width: number
  start: LayoutCursor
  end: LayoutCursor
}

/** 布局行范围(无文本内容,更轻量) */
export type LayoutLineRange = {
  width: number
  start: LayoutCursor
  end: LayoutCursor
}

/** 布局结果(含行文本) */
export type LayoutLinesResult = LayoutResult & {
  lines: LayoutLine[]
}

/**
 * ============================================
 * rich-inline.ts - 富文本内联辅助契约
 * ============================================
 */

/** 富文本内联项 */
export type RichInlineItem = {
  text: string              // 原始文本(含前后空白)
  font: string              // Canvas 字体简写
  letterSpacing?: number    // 字形间额外间距
  break?: 'normal' | 'never' // 'never' = 原子项(chip/mention)
  extraWidth?: number       // 额外水平 Chrome(padding/border)
}

/** 富文本内联游标 */
export type RichInlineCursor = {
  itemIndex: number
  segmentIndex: number
  graphemeIndex: number
}

6.2公共 API 函数签名汇总

typescript
/**
 * 核心 API (layout.ts)
 */

// 准备阶段(快速路径 + 富文本路径)
function prepare(text: string, font: string, options?: PrepareOptions): PreparedText
function prepareWithSegments(text: string, font: string, options?: PrepareOptions): PreparedTextWithSegments

// 布局阶段(快速路径)
function layout(prepared: PreparedText, maxWidth: number, lineHeight: number): LayoutResult

// 布局阶段(富文本路径 - 批量)
function layoutWithLines(prepared: PreparedTextWithSegments, maxWidth: number, lineHeight: number): LayoutLinesResult

// 布局阶段(富文本路径 - 无字符串分配)
function walkLineRanges(prepared: PreparedTextWithSegments, maxWidth: number, onLine: (line: LayoutLineRange) => void): number
function measureLineStats(prepared: PreparedTextWithSegments, maxWidth: number): LineStats
function measureNaturalWidth(prepared: PreparedTextWithSegments): number

// 布局阶段(富文本路径 - 迭代器模式,可变行宽)
function layoutNextLine(prepared: PreparedTextWithSegments, start: LayoutCursor, maxWidth: number): LayoutLine | null
function layoutNextLineRange(prepared: PreparedTextWithSegments, start: LayoutCursor, maxWidth: number): LayoutLineRange | null
function materializeLineRange(prepared: PreparedTextWithSegments, line: LayoutLineRange): LayoutLine

// 缓存控制
function clearCache(): void
function setLocale(locale?: string): void

/**
 * 富文本内联 API (rich-inline.ts)
 */
function prepareRichInline(items: RichInlineItem[]): PreparedRichInline
function layoutNextRichInlineLineRange(prepared: PreparedRichInline, maxWidth: number, start?: RichInlineCursor): RichInlineLineRange | null
function walkRichInlineLineRanges(prepared: PreparedRichInline, maxWidth: number, onLine: (line: RichInlineLineRange) => void): number
function materializeRichInlineLineRange(prepared: PreparedRichInline, line: RichInlineLineRange): RichInlineLine
function measureRichInlineStats(prepared: PreparedRichInline, maxWidth: number): RichInlineStats

8. 快速开始

8.1 环境配置

sh
# 安装依赖
bun install

# 启动开发服务器
bun start        # → http://localhost:3000

# 运行类型检查 + 代码检查
bun run check    # tsc + oxlint + knip

# 运行测试
bun test

8.2 安装与使用

sh
npm install @chenglou/pretext
typescript
import { prepare, layout } from '@chenglou/pretext'

const prepared = prepare('AGI 春天到了. بدأت الرحلة 🚀‎', '16px Inter')
const { height, lineCount } = layout(prepared, 320, 20)
// height 和 lineCount 精确匹配浏览器行为

8.3 典型用例

用例推荐 API
虚拟滚动高度预测prepare() + layout()
Canvas 自定义渲染prepareWithSegments() + layoutWithLines()
浮动图片绕排prepareWithSegments() + layoutNextLineRange() + materializeLineRange()
多行 shrinkwrapprepareWithSegments() + walkLineRanges()
内联富文本 (chip/mention)@chenglou/pretext/rich-inline