3.2 KiB
第三章:分析阶段如何把文字变成可布局的分段
3.1 三个容易混淆的单位
| 名称 | 含义 | 例子 |
|---|---|---|
| UTF-16 码元 | JavaScript 字符串索引使用的存储单位 | 一些 emoji 的 length 大于 1 |
| 字素簇,简称字素 | 用户通常感知的一个文字单位 | 字母加组合音标、由多个码点组成的 emoji |
| 分段 segment | Pretext 为测量与换行建立的单位 | 一个词、一段文本、空格或特殊控制段 |
分段并不总等于单词。字素也不总等于一个 JavaScript 字符。理解这两个区别,后面的超长单词断行和 emoji 处理才容易读懂。
3.2 先找哪里允许断行
对 hello world,可以先想象成:
['hello', ' ', 'world']
这个示意足以解释普通英文,但真实的中文标点、日文、泰文、软连字符和不可断空格需要更多规则。
analysis.ts 的 analyzeText()先处理空白与源文本,再按引擎选择扫描器,最后形成分段及标志。其中主要的引擎对应关系是:
| 浏览器引擎 | 主要实现入口 |
|---|---|
| Blink,例如 Chrome | line-breaks.ts 中的 getBlinkLineBreaks() |
| WebKit,例如 Safari | line-breaks.ts 中的 getWebKitLineBreaks() |
| Gecko,例如 Firefox | gecko-line-breaks.ts 中的 getGeckoLineBreaks() |
选择由 measurement.ts 的 getEngineProfile() 完成,并缓存 profile。未知引擎采用 Blink 的模型。
3.3 表驱动扫描是什么意思
可以把扫描器想象成一个不断读取字符的机器:
当前状态 + 当前字符类别 → 新状态 + 是否出现边界
字符属于哪一类、某种状态遇到某一类应该怎么走,都放在表里。运行时主要是读字符、查类别、查转换表、更新状态。
规则数据来自对应引擎及其 Unicode/ICU 数据,保存在 src/generated/。数据经过压缩;页面按需解开自己的引擎所用的表。这样能把复杂规则变成较直接的扫描,但也增加了包体积与初始化成本。这是有代价的工程选择。
graphemes.ts 的 findGraphemeEnds()也采用类似方式寻找字素边界。这个版本不是对所有文字都调用 Intl.Segmenter:词典分词主要留给泰语、老挝语、高棉语、缅甸语等特定文字的运行片段。
3.4 为什么空白处理要先做
white-space: normal 下,连续普通空格通常被折叠;pre-wrap 则保留空格、换行与制表符的相关行为。若测量的是原始字符串,绘制时浏览器却折叠了它,预测自然会错。
所以分析阶段必须建立与目标 CSS 相符的文本视图。这个版本对孤立 \r 有明确限制:pre-wrap 下绘制前也应把它规范成 \n,详见根目录 README。
本章检查:为什么不能直接 text.split(' ')?因为断行机会远不止空格,而且许多语言没有用空格分隔每一个词。